Skip to content

Commit 5b556ac

Browse files
feat: Adding M2M Quota Support (#788)
### Changes Support Added for M2M Rate Limit Quotas : 1. Authentication - Added a new generic helper function `HttpResponse::parseQuotaHeaders` 2. Management - Added extensive tests for `Tenant`, `Client` and `Organization` API Endpoints which will give a overview of the query and responses related token quota ### References - Internal - [https://oktawiki.atlassian.net/wiki/spaces/IAMPS/pages/3192719041/RFC+-+M2M+Token+Quota](RFC) - Management API - [Update Tenant Settings](https://auth0.com/docs/api/management/v2/tenants/patch-settings) ### Testing - [x] This change adds test coverage - [x] This change has been tested on the latest version of the platform/language or why not ### Contributor Checklist - [x] I agree to adhere to the [Auth0 General Contribution Guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md). - [x] I agree to uphold the [Auth0 Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md).
1 parent 03579f7 commit 5b556ac

File tree

8 files changed

+930
-0
lines changed

8 files changed

+930
-0
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
################################################################################
2+
#
3+
# For guidance on setting up an Auth0 application and collecting these
4+
# values,please see the README in the root of this repository:
5+
#
6+
# https://github.com/auth0/auth0-PHP/blob/main/README.md#configure-auth0
7+
#
8+
################################################################################
9+
10+
# Your Auth0 tenant domain.
11+
DOMAIN=
12+
13+
# Your Auth0 application's Client ID.
14+
CLIENT_ID=
15+
16+
# Your Auth0 application's Client Secret.
17+
CLIENT_SECRET=
18+
19+
# A random string used to encrypt your session cookie.
20+
COOKIE_SECRET=
21+
22+
# Your Auth0 Custom Domain, if applicable.
23+
CUSTOM_DOMAIN=
24+
25+
# Your Auth0 API Identifier/Audience, if applicable.
26+
API_IDENTIFIER=
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "auth0-samples/m2m-quota-headers",
3+
"description": "Example of using M2M Quota Headers with Auth0 PHP",
4+
"require": {
5+
"php": "^8.1",
6+
"auth0/auth0-php": "8.13",
7+
"nyholm/psr7": "^1.5",
8+
"symfony/http-client": "^6.2",
9+
"vlucas/phpdotenv": "^5.5"
10+
},
11+
"prefer-stable": true,
12+
"config": {
13+
"optimize-autoloader": true,
14+
"preferred-install": "dist",
15+
"sort-packages": true,
16+
"process-timeout": 0,
17+
"allow-plugins": {
18+
"php-http/discovery": false
19+
}
20+
},
21+
"scripts": {
22+
"serve": [
23+
"php -S 127.0.0.1:3000 index.php"
24+
],
25+
"pre-install-cmd": [
26+
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
27+
],
28+
"pre-update-cmd": [
29+
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
30+
]
31+
}
32+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
declare(strict_types=1);
3+
header('Content-Type: text/plain; charset=utf-8');
4+
5+
/**
6+
* This example demonstrates how to use the Auth0 SDK for PHP to get M2M Quota Headers.
7+
*
8+
* You should invoke this app using routed URLs like http://localhost:3000 or http://localhost:3000/login.
9+
* Using the PHP built-in web server, you can start this app with the following command: php -S 127.0.0.1:3000 index.php
10+
*/
11+
12+
use Auth0\SDK\Auth0;
13+
use Auth0\SDK\Configuration\SdkConfiguration;
14+
use Auth0\SDK\Utility\HttpResponse;
15+
use Dotenv\Dotenv;
16+
17+
// 1. Bootstrap: load Composer autoloader and environment
18+
require __DIR__ . '/vendor/autoload.php';
19+
$env = Dotenv::createImmutable(__DIR__);
20+
$env->load();
21+
22+
// 2. Configure the SDK
23+
$config = new SdkConfiguration([
24+
'domain' => $_ENV['DOMAIN'] ?? '',
25+
'clientId' => $_ENV['CLIENT_ID'] ?? '',
26+
'clientSecret' => $_ENV['CLIENT_SECRET']?? '',
27+
'cookieSecret' => $_ENV['COOKIE_SECRET']?? ''
28+
]);
29+
$auth0 = new Auth0($config);
30+
31+
// 3. Perform a Client Credentials (M2M) request
32+
echo "Performing client-credentials flow...\n";
33+
$response = $auth0->authentication()->clientCredentials([
34+
'audience' => $_ENV['API_IDENTIFIER'] ?? '',
35+
]);
36+
37+
$status = $response->getStatusCode();
38+
echo "HTTP status code: {$status}\n\n";
39+
40+
// 4. Decode the JSON body
41+
$body = HttpResponse::decodeContent($response);
42+
43+
// ------------------------
44+
// Successful 2xx response
45+
// ------------------------
46+
if ($status >= 200 && $status < 300 && isset($body['access_token'])) {
47+
echo "Access token received:\n";
48+
echo substr($body['access_token'], 0, 20) . "... (truncated)\n\n";
49+
50+
// Parse and display quota/quota headers
51+
echo "Quota headers (parsed):\n";
52+
$quotaData = HttpResponse::parseQuotaHeaders($response);
53+
print_r($quotaData);
54+
55+
exit(0);
56+
}
57+
58+
// -----------------------------------
59+
// Handle 429 Too Many Requests error
60+
// -----------------------------------
61+
if ($status === 429) {
62+
echo "Error: Too Many Requests (429)\n";
63+
64+
// Your new structured quota headers
65+
echo "Quota headers (parsed):\n";
66+
$quotaData = HttpResponse::parseQuotaHeaders($response);
67+
print_r($quotaData);
68+
69+
exit(1);
70+
}
71+
72+
// -------------------------------
73+
// Any other non-2xx / non-429
74+
// -------------------------------
75+
echo "Unexpected response ({$status}):\n";
76+
print_r($body);
77+
exit(1);

src/Utility/HttpResponse.php

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,146 @@ public static function getStatusCode(
6969
return $response->getStatusCode();
7070
}
7171

72+
/**
73+
* Helper function to parse "b=per_hour;q=100;r=99;t=1,b=per_day;q=300;r=299;t=1"
74+
* into an array like:
75+
* [
76+
* 'per_hour' => [ 'quota' => 100, 'remaining' => 99, 'resetAfter' => 1 ],
77+
* 'per_day' => [ 'quota' => 300, 'remaining' => 299, 'resetAfter' => 1 ]
78+
* ].
79+
*
80+
* @param string $rawValue
81+
*
82+
* @return array<string,array{quota:null|int,remaining:null|int,resetAfter:null|int}>
83+
*/
84+
public static function parseQuotaBuckets(string $rawValue): array
85+
{
86+
$result = [];
87+
88+
// Example: "b=per_hour;q=100;r=99;t=1,b=per_day;q=300;r=299;t=1"
89+
$buckets = explode(',', $rawValue);
90+
91+
foreach ($buckets as $bucket) {
92+
$pairs = explode(';', $bucket);
93+
94+
$bucketName = null;
95+
$bucketData = [
96+
'quota' => null,
97+
'remaining' => null,
98+
'resetAfter' => null,
99+
];
100+
101+
foreach ($pairs as $pair) {
102+
$rawParts = explode('=', $pair, 2) + [null, null];
103+
$parts = array_map(
104+
/** @param null|string $v */
105+
static fn (?string $v): string => null !== $v ? trim($v) : '',
106+
$rawParts,
107+
);
108+
$key = $parts[0];
109+
$value = $parts[1] ?? null;
110+
111+
if ('b' === $key) {
112+
// e.g. "per_hour", "per_day", "per_minute", etc.
113+
$bucketName = $value;
114+
} elseif ('q' === $key) {
115+
// "quota"
116+
if (is_numeric($value)) {
117+
$bucketData['quota'] = (int) $value;
118+
}
119+
} elseif ('r' === $key) {
120+
// "remaining"
121+
if (is_numeric($value)) {
122+
$bucketData['remaining'] = (int) $value;
123+
}
124+
} elseif ('t' === $key) {
125+
// "resetAfter"
126+
if (is_numeric($value)) {
127+
$bucketData['resetAfter'] = (int) $value;
128+
}
129+
}
130+
}
131+
132+
// If we got a bucket name like "per_hour", store it
133+
if (null !== $bucketName) {
134+
$result[$bucketName] = $bucketData;
135+
}
136+
}
137+
138+
// e.g. { "per_hour" => {...}, "per_day" => {...} }
139+
return $result;
140+
}
141+
142+
/**
143+
* Parse Auth0's quota headers into the desired 'client'/'organization' structure.
144+
* The returned array looks like:
145+
* [
146+
* 'client' => [
147+
* 'per_hour' => [ 'quota' => ..., 'remaining' => ..., 'resetAfter' => ... ],
148+
* 'per_day' => [ 'quota' => ..., 'remaining' => ..., 'resetAfter' => ... ],
149+
* ],
150+
* 'organization' => [
151+
* 'per_hour' => [...],
152+
* 'per_day' => [...],
153+
* ]
154+
* ].
155+
*
156+
* Buckets or keys missing from the header won't appear in the array.
157+
*
158+
* @param ResponseInterface $response a ResponseInterface instance to extract from
159+
*
160+
* @return array{
161+
* client?: array<string,array{quota:int|null,remaining:int|null,resetAfter:int|null}>,
162+
* organization?: array<string,array{quota:int|null,remaining:int|null,resetAfter:int|null}>,
163+
* retryAfter?: int,
164+
* rateLimit?: array{limit?:int,remaining?:int,reset?:int}
165+
* }
166+
*/
167+
public static function parseQuotaHeaders(ResponseInterface $response): array
168+
{
169+
// Retrieve the raw header strings (they might be arrays of strings from getHeaders()).
170+
$headers = array_change_key_case($response->getHeaders(), CASE_LOWER);
171+
$clientLimit = $headers['auth0-client-quota-limit'][0] ?? null;
172+
$orgLimit = $headers['auth0-organization-quota-limit'][0] ?? null;
173+
$retryAfteValue = $headers['retry-after'][0] ?? null;
174+
175+
$client = null !== $clientLimit && '' !== $clientLimit ? self::parseQuotaBuckets($clientLimit) : [];
176+
$org = null !== $orgLimit && '' !== $orgLimit ? self::parseQuotaBuckets($orgLimit) : [];
177+
$retryAfter = (is_numeric($retryAfteValue)) ? (int) $retryAfteValue : null;
178+
179+
$result = [];
180+
if ([] !== $client) {
181+
$result['client'] = $client;
182+
}
183+
if ([] !== $org) {
184+
$result['organization'] = $org;
185+
}
186+
if (null !== $retryAfter) {
187+
$result['retryAfter'] = $retryAfter;
188+
189+
$limitRaw = $headers['x-ratelimit-limit'][0] ?? null;
190+
$remainingRaw = $headers['x-ratelimit-remaining'][0] ?? null;
191+
$resetRaw = $headers['x-ratelimit-reset'][0] ?? null;
192+
193+
$rateLimit = [];
194+
if (is_numeric($limitRaw)) {
195+
$rateLimit['limit'] = (int) $limitRaw;
196+
}
197+
if (is_numeric($remainingRaw)) {
198+
$rateLimit['remaining'] = (int) $remainingRaw;
199+
}
200+
if (is_numeric($resetRaw)) {
201+
$rateLimit['reset'] = (int) $resetRaw;
202+
}
203+
204+
if ([] !== $rateLimit) {
205+
$result['rateLimit'] = $rateLimit;
206+
}
207+
}
208+
209+
return $result;
210+
}
211+
72212
/**
73213
* Returns true when the ResponseInterface identifies a 200 status code; otherwise false.
74214
*

0 commit comments

Comments
 (0)