@cloudflare/workers-oauth-provider 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -0
- package/dist/oauth-provider.d.ts +92 -1
- package/dist/oauth-provider.js +234 -79
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -306,6 +306,52 @@ The `accessTokenTTL` override is particularly useful when the application is als
|
|
|
306
306
|
|
|
307
307
|
The `props` values are end-to-end encrypted, so they can safely contain sensitive information.
|
|
308
308
|
|
|
309
|
+
### Reporting errors from the callback
|
|
310
|
+
|
|
311
|
+
Throw `OAuthError` from `tokenExchangeCallback` to return a structured OAuth `/token` error (`{ error, error_description }`) instead of a generic 500:
|
|
312
|
+
|
|
313
|
+
```ts
|
|
314
|
+
import { OAuthError, OAuthProvider } from '@cloudflare/workers-oauth-provider';
|
|
315
|
+
|
|
316
|
+
new OAuthProvider({
|
|
317
|
+
// …
|
|
318
|
+
tokenExchangeCallback: async (options) => {
|
|
319
|
+
if (options.grantType === 'refresh_token') {
|
|
320
|
+
return { newProps: await refreshUpstream(options.props) };
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
async function refreshUpstream(props) {
|
|
326
|
+
const res = await fetch(/* upstream token endpoint */);
|
|
327
|
+
|
|
328
|
+
if (res.status === 401) {
|
|
329
|
+
throw new OAuthError('invalid_grant', {
|
|
330
|
+
description: 'upstream refresh token is invalid',
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (res.status === 429) {
|
|
335
|
+
throw new OAuthError('temporarily_unavailable', {
|
|
336
|
+
description: 'upstream rate limited',
|
|
337
|
+
statusCode: 429,
|
|
338
|
+
headers: { 'Retry-After': res.headers.get('retry-after') ?? '60' },
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return await res.json();
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
`OAuthError(code, options)` takes:
|
|
347
|
+
|
|
348
|
+
- `code` — OAuth error code returned in the `error` field. This may be a standard code (`OAuthTokenErrorCode`) or an application-defined string.
|
|
349
|
+
- `options.description` — human-readable text returned in `error_description`.
|
|
350
|
+
- `options.statusCode` — HTTP status code (default `400`).
|
|
351
|
+
- `options.headers` — additional response headers, such as `Retry-After` for transient failures. There is no implicit `Retry-After` default for callback-thrown errors.
|
|
352
|
+
|
|
353
|
+
Only `OAuthError` from this package is converted into a structured `/token` response. Plain errors, plain objects with a `code` field, and app-local error classes continue to surface as 500s so unexpected failures stay visible. Import `OAuthError` from `@cloudflare/workers-oauth-provider` rather than copying or re-implementing it.
|
|
354
|
+
|
|
309
355
|
## Custom Error Responses
|
|
310
356
|
|
|
311
357
|
By using the `onError` option, you can emit notifications or take other actions when an error response was to be emitted:
|
package/dist/oauth-provider.d.ts
CHANGED
|
@@ -20,6 +20,16 @@ type WorkerEntrypointWithFetch<Env = Cloudflare.Env> = WorkerEntrypoint<Env> & {
|
|
|
20
20
|
/**
|
|
21
21
|
* Configuration options for the OAuth Provider
|
|
22
22
|
*/
|
|
23
|
+
/**
|
|
24
|
+
* Registered OAuth 2.0 error codes that the `/token` endpoint may return.
|
|
25
|
+
*
|
|
26
|
+
* Union of:
|
|
27
|
+
* - RFC 6749 §5.2 (token endpoint)
|
|
28
|
+
* - RFC 6750 §3.1 (bearer / resource server) — included so callbacks
|
|
29
|
+
* doing audience validation can use them
|
|
30
|
+
* - RFC 8693 §2.2.2 (token exchange)
|
|
31
|
+
*/
|
|
32
|
+
type OAuthTokenErrorCode = 'invalid_request' | 'invalid_client' | 'invalid_grant' | 'unauthorized_client' | 'unsupported_grant_type' | 'invalid_scope' | 'invalid_token' | 'insufficient_scope' | 'invalid_target' | 'server_error' | 'temporarily_unavailable';
|
|
23
33
|
/**
|
|
24
34
|
* Result of a token exchange callback function.
|
|
25
35
|
* Allows updating the props stored in both the access token and the grant.
|
|
@@ -889,5 +899,86 @@ declare class OAuthProvider<Env = Cloudflare.Env> {
|
|
|
889
899
|
* @returns An instance of OAuthHelpers
|
|
890
900
|
*/
|
|
891
901
|
declare function getOAuthApi<Env = Cloudflare.Env>(options: OAuthProviderOptions<Env>, env: Env): OAuthHelpers;
|
|
902
|
+
/**
|
|
903
|
+
* Error class for OAuth operations
|
|
904
|
+
* Carries OAuth error code and description for proper error responses
|
|
905
|
+
*/
|
|
906
|
+
/**
|
|
907
|
+
* Options accepted by the {@link OAuthError} constructor.
|
|
908
|
+
*/
|
|
909
|
+
interface OAuthErrorOptions {
|
|
910
|
+
/**
|
|
911
|
+
* Human-readable text returned in the `error_description` field.
|
|
912
|
+
*/
|
|
913
|
+
description: string;
|
|
914
|
+
/**
|
|
915
|
+
* HTTP status code for the error response. Defaults to `400`.
|
|
916
|
+
*/
|
|
917
|
+
statusCode?: number;
|
|
918
|
+
/**
|
|
919
|
+
* Additional response headers.
|
|
920
|
+
*
|
|
921
|
+
* For transient failures (e.g. upstream rate limits), set
|
|
922
|
+
* `Retry-After` here so well-behaved clients back off instead of
|
|
923
|
+
* retry-storming. Per RFC 7231 §7.1.3 the value may be either a
|
|
924
|
+
* number of seconds or an HTTP-date.
|
|
925
|
+
*/
|
|
926
|
+
headers?: Record<string, string>;
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Structured OAuth 2.0 error.
|
|
930
|
+
*
|
|
931
|
+
* Throw from a `tokenExchangeCallback` (or any code it calls — the error
|
|
932
|
+
* propagates naturally up through deep call stacks) to surface a standard
|
|
933
|
+
* `/token` error response (`{ error, error_description }`) instead of a
|
|
934
|
+
* generic `500 Internal Server Error`.
|
|
935
|
+
*
|
|
936
|
+
* Anything thrown that is **not** an `OAuthError` continues to surface as
|
|
937
|
+
* a 500 so unexpected failures remain visible — the provider does not
|
|
938
|
+
* catch-everything-and-return-400.
|
|
939
|
+
*
|
|
940
|
+
* @example
|
|
941
|
+
* ```ts
|
|
942
|
+
* import { OAuthError } from '@cloudflare/workers-oauth-provider';
|
|
943
|
+
*
|
|
944
|
+
* tokenExchangeCallback: async (options) => {
|
|
945
|
+
* if (options.grantType === 'refresh_token') {
|
|
946
|
+
* // refreshUpstream() may throw OAuthError from any depth
|
|
947
|
+
* return { newProps: await refreshUpstream(options.props) };
|
|
948
|
+
* }
|
|
949
|
+
* }
|
|
950
|
+
*
|
|
951
|
+
* async function refreshUpstream(props) {
|
|
952
|
+
* const res = await fetch(...);
|
|
953
|
+
* if (res.status === 401) {
|
|
954
|
+
* throw new OAuthError('invalid_grant', { description: 'upstream refresh token is invalid' });
|
|
955
|
+
* }
|
|
956
|
+
* if (res.status === 429) {
|
|
957
|
+
* // Mirror upstream's Retry-After if present, otherwise pick a default.
|
|
958
|
+
* throw new OAuthError('temporarily_unavailable', {
|
|
959
|
+
* description: 'upstream rate limited',
|
|
960
|
+
* statusCode: 429,
|
|
961
|
+
* headers: { 'Retry-After': res.headers.get('retry-after') ?? '60' },
|
|
962
|
+
* });
|
|
963
|
+
* }
|
|
964
|
+
* return await res.json();
|
|
965
|
+
* }
|
|
966
|
+
* ```
|
|
967
|
+
*/
|
|
968
|
+
declare class OAuthError extends Error {
|
|
969
|
+
/** OAuth 2.0 error code. */
|
|
970
|
+
readonly code: string;
|
|
971
|
+
/** Options controlling the OAuth error response. */
|
|
972
|
+
readonly options: OAuthErrorOptions & {
|
|
973
|
+
statusCode: number;
|
|
974
|
+
};
|
|
975
|
+
/** Human-readable description sent in the `error_description` field. */
|
|
976
|
+
readonly description: string;
|
|
977
|
+
/** HTTP status code for the error response. */
|
|
978
|
+
readonly statusCode: number;
|
|
979
|
+
/** Additional response headers. */
|
|
980
|
+
readonly headers?: Record<string, string>;
|
|
981
|
+
constructor(code: string, options: OAuthErrorOptions);
|
|
982
|
+
}
|
|
892
983
|
//#endregion
|
|
893
|
-
export { AuthRequest, ClientInfo, CompleteAuthorizationOptions, ExchangeTokenOptions, Grant, GrantSummary, GrantType, ListOptions, ListResult, OAuthHelpers, OAuthProvider, OAuthProvider as default, OAuthProviderOptions, PurgeOptions, PurgeResult, ResolveExternalTokenInput, ResolveExternalTokenResult, Token, TokenBase, TokenExchangeCallbackOptions, TokenExchangeCallbackResult, TokenSummary, getOAuthApi };
|
|
984
|
+
export { AuthRequest, ClientInfo, CompleteAuthorizationOptions, ExchangeTokenOptions, Grant, GrantSummary, GrantType, ListOptions, ListResult, OAuthError, OAuthErrorOptions, OAuthHelpers, OAuthProvider, OAuthProvider as default, OAuthProviderOptions, OAuthTokenErrorCode, PurgeOptions, PurgeResult, ResolveExternalTokenInput, ResolveExternalTokenResult, Token, TokenBase, TokenExchangeCallbackOptions, TokenExchangeCallbackResult, TokenSummary, getOAuthApi };
|
package/dist/oauth-provider.js
CHANGED
|
@@ -285,10 +285,16 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
285
285
|
* @returns Promise with parsed body and client info, or error response
|
|
286
286
|
*/
|
|
287
287
|
async parseTokenEndpointRequest(request, env) {
|
|
288
|
-
if (request.method !== "POST") return this.createErrorResponse("invalid_request",
|
|
288
|
+
if (request.method !== "POST") return this.createErrorResponse("invalid_request", {
|
|
289
|
+
description: "Method not allowed",
|
|
290
|
+
statusCode: 405
|
|
291
|
+
});
|
|
289
292
|
let contentType = request.headers.get("Content-Type") || "";
|
|
290
293
|
let body = {};
|
|
291
|
-
if (!contentType.includes("application/x-www-form-urlencoded")) return this.createErrorResponse("invalid_request",
|
|
294
|
+
if (!contentType.includes("application/x-www-form-urlencoded")) return this.createErrorResponse("invalid_request", {
|
|
295
|
+
description: "Content-Type must be application/x-www-form-urlencoded",
|
|
296
|
+
statusCode: 400
|
|
297
|
+
});
|
|
292
298
|
const formData = await request.formData();
|
|
293
299
|
for (const [key, value] of formData.entries()) {
|
|
294
300
|
const allValues = formData.getAll(key);
|
|
@@ -305,13 +311,28 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
305
311
|
clientId = body.client_id;
|
|
306
312
|
clientSecret = body.client_secret || "";
|
|
307
313
|
}
|
|
308
|
-
if (!clientId) return this.createErrorResponse("invalid_client",
|
|
314
|
+
if (!clientId) return this.createErrorResponse("invalid_client", {
|
|
315
|
+
description: "Client ID is required",
|
|
316
|
+
statusCode: 401
|
|
317
|
+
});
|
|
309
318
|
const clientInfo = await this.getClient(env, clientId);
|
|
310
|
-
if (!clientInfo) return this.createErrorResponse("invalid_client",
|
|
319
|
+
if (!clientInfo) return this.createErrorResponse("invalid_client", {
|
|
320
|
+
description: "Client not found",
|
|
321
|
+
statusCode: 401
|
|
322
|
+
});
|
|
311
323
|
if (!(clientInfo.tokenEndpointAuthMethod === "none")) {
|
|
312
|
-
if (!clientSecret) return this.createErrorResponse("invalid_client",
|
|
313
|
-
|
|
314
|
-
|
|
324
|
+
if (!clientSecret) return this.createErrorResponse("invalid_client", {
|
|
325
|
+
description: "Client authentication failed: missing client_secret",
|
|
326
|
+
statusCode: 401
|
|
327
|
+
});
|
|
328
|
+
if (!clientInfo.clientSecret) return this.createErrorResponse("invalid_client", {
|
|
329
|
+
description: "Client authentication failed: client has no registered secret",
|
|
330
|
+
statusCode: 401
|
|
331
|
+
});
|
|
332
|
+
if (await hashSecret(clientSecret) !== clientInfo.clientSecret) return this.createErrorResponse("invalid_client", {
|
|
333
|
+
description: "Client authentication failed: invalid client_secret",
|
|
334
|
+
statusCode: 401
|
|
335
|
+
});
|
|
315
336
|
}
|
|
316
337
|
return {
|
|
317
338
|
body,
|
|
@@ -441,11 +462,31 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
441
462
|
* @returns Response with token data or error
|
|
442
463
|
*/
|
|
443
464
|
async handleTokenRequest(body, clientInfo, env) {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
465
|
+
try {
|
|
466
|
+
const grantType = body.grant_type;
|
|
467
|
+
if (grantType === GrantType.AUTHORIZATION_CODE) return await this.handleAuthorizationCodeGrant(body, clientInfo, env);
|
|
468
|
+
else if (grantType === GrantType.REFRESH_TOKEN) return await this.handleRefreshTokenGrant(body, clientInfo, env);
|
|
469
|
+
else if (grantType === GrantType.TOKEN_EXCHANGE && this.options.allowTokenExchangeGrant) return await this.handleTokenExchangeGrant(body, clientInfo, env);
|
|
470
|
+
else return this.createErrorResponse("unsupported_grant_type", { description: "Grant type not supported" });
|
|
471
|
+
} catch (error) {
|
|
472
|
+
const response = this.createOAuthErrorResponse(error);
|
|
473
|
+
if (response) return response;
|
|
474
|
+
throw error;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Build a structured OAuth `/token` error response from an OAuth error.
|
|
479
|
+
*
|
|
480
|
+
* The supported form is throwing this package's exported `OAuthError`.
|
|
481
|
+
* Anything else is re-thrown so unexpected failures still surface as 500s.
|
|
482
|
+
*
|
|
483
|
+
* Use `headers['Retry-After']` for rate-limit / transient-failure backoff
|
|
484
|
+
* hints (see RFC 7231 §7.1.3 — either an integer seconds value or an
|
|
485
|
+
* HTTP-date is allowed).
|
|
486
|
+
*/
|
|
487
|
+
createOAuthErrorResponse(error) {
|
|
488
|
+
if (!(error instanceof OAuthError)) return void 0;
|
|
489
|
+
return this.createErrorResponse(error.code, error.options);
|
|
449
490
|
}
|
|
450
491
|
/**
|
|
451
492
|
* Handles the authorization code grant type
|
|
@@ -459,27 +500,27 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
459
500
|
const code = body.code;
|
|
460
501
|
const redirectUri = body.redirect_uri;
|
|
461
502
|
const codeVerifier = body.code_verifier;
|
|
462
|
-
if (!code) return this.createErrorResponse("invalid_request", "Authorization code is required");
|
|
503
|
+
if (!code) return this.createErrorResponse("invalid_request", { description: "Authorization code is required" });
|
|
463
504
|
const codeParts = code.split(":");
|
|
464
|
-
if (codeParts.length !== 3) return this.createErrorResponse("invalid_grant", "Invalid authorization code format");
|
|
505
|
+
if (codeParts.length !== 3) return this.createErrorResponse("invalid_grant", { description: "Invalid authorization code format" });
|
|
465
506
|
const [userId, grantId, _] = codeParts;
|
|
466
507
|
const grantKey = `grant:${userId}:${grantId}`;
|
|
467
508
|
const grantData = await env.OAUTH_KV.get(grantKey, { type: "json" });
|
|
468
|
-
if (!grantData) return this.createErrorResponse("invalid_grant", "Grant not found or authorization code expired");
|
|
509
|
+
if (!grantData) return this.createErrorResponse("invalid_grant", { description: "Grant not found or authorization code expired" });
|
|
469
510
|
if (!grantData.authCodeId) {
|
|
470
511
|
try {
|
|
471
512
|
await this.createOAuthHelpers(env).revokeGrant(grantId, userId);
|
|
472
513
|
} catch {}
|
|
473
|
-
return this.createErrorResponse("invalid_grant", "Authorization code already used");
|
|
514
|
+
return this.createErrorResponse("invalid_grant", { description: "Authorization code already used" });
|
|
474
515
|
}
|
|
475
|
-
if (await hashSecret(code) !== grantData.authCodeId) return this.createErrorResponse("invalid_grant", "Invalid authorization code");
|
|
476
|
-
if (grantData.clientId !== clientInfo.clientId) return this.createErrorResponse("invalid_grant", "Client ID mismatch");
|
|
516
|
+
if (await hashSecret(code) !== grantData.authCodeId) return this.createErrorResponse("invalid_grant", { description: "Invalid authorization code" });
|
|
517
|
+
if (grantData.clientId !== clientInfo.clientId) return this.createErrorResponse("invalid_grant", { description: "Client ID mismatch" });
|
|
477
518
|
const isPkceEnabled = !!grantData.codeChallenge;
|
|
478
|
-
if (!redirectUri && !isPkceEnabled) return this.createErrorResponse("invalid_request", "redirect_uri is required when not using PKCE");
|
|
479
|
-
if (redirectUri && !isValidRedirectUri(redirectUri, clientInfo.redirectUris)) return this.createErrorResponse("invalid_grant", "Invalid redirect URI");
|
|
480
|
-
if (!isPkceEnabled && codeVerifier) return this.createErrorResponse("invalid_request", "code_verifier provided for a flow that did not use PKCE");
|
|
519
|
+
if (!redirectUri && !isPkceEnabled) return this.createErrorResponse("invalid_request", { description: "redirect_uri is required when not using PKCE" });
|
|
520
|
+
if (redirectUri && !isValidRedirectUri(redirectUri, clientInfo.redirectUris)) return this.createErrorResponse("invalid_grant", { description: "Invalid redirect URI" });
|
|
521
|
+
if (!isPkceEnabled && codeVerifier) return this.createErrorResponse("invalid_request", { description: "code_verifier provided for a flow that did not use PKCE" });
|
|
481
522
|
if (isPkceEnabled) {
|
|
482
|
-
if (!codeVerifier) return this.createErrorResponse("invalid_request", "code_verifier is required for PKCE");
|
|
523
|
+
if (!codeVerifier) return this.createErrorResponse("invalid_request", { description: "code_verifier is required for PKCE" });
|
|
483
524
|
let calculatedChallenge;
|
|
484
525
|
if (grantData.codeChallengeMethod === "S256") {
|
|
485
526
|
const data = new TextEncoder().encode(codeVerifier);
|
|
@@ -487,7 +528,7 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
487
528
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
488
529
|
calculatedChallenge = base64UrlEncode(String.fromCharCode(...hashArray));
|
|
489
530
|
} else calculatedChallenge = codeVerifier;
|
|
490
|
-
if (calculatedChallenge !== grantData.codeChallenge) return this.createErrorResponse("invalid_grant", "Invalid PKCE code_verifier");
|
|
531
|
+
if (calculatedChallenge !== grantData.codeChallenge) return this.createErrorResponse("invalid_grant", { description: "Invalid PKCE code_verifier" });
|
|
491
532
|
}
|
|
492
533
|
let accessTokenTTL = this.options.accessTokenTTL;
|
|
493
534
|
let refreshTokenTTL = this.options.refreshTokenTTL;
|
|
@@ -554,10 +595,10 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
554
595
|
if (body.resource && grantData.resource) {
|
|
555
596
|
const requestedResources = Array.isArray(body.resource) ? body.resource : [body.resource];
|
|
556
597
|
const grantedResources = Array.isArray(grantData.resource) ? grantData.resource : [grantData.resource];
|
|
557
|
-
for (const requested of requestedResources) if (!grantedResources.some((granted) => resourceMatches(requested, granted, originOnly))) return this.createErrorResponse("invalid_target", "Requested resource was not included in the authorization request");
|
|
598
|
+
for (const requested of requestedResources) if (!grantedResources.some((granted) => resourceMatches(requested, granted, originOnly))) return this.createErrorResponse("invalid_target", { description: "Requested resource was not included in the authorization request" });
|
|
558
599
|
}
|
|
559
600
|
const audience = parseResourceParameter(body.resource || grantData.resource);
|
|
560
|
-
if ((body.resource || grantData.resource) && !audience) return this.createErrorResponse("invalid_target", "The resource parameter must be a valid absolute URI without a fragment");
|
|
601
|
+
if ((body.resource || grantData.resource) && !audience) return this.createErrorResponse("invalid_target", { description: "The resource parameter must be a valid absolute URI without a fragment" });
|
|
561
602
|
const tokenResponse = {
|
|
562
603
|
access_token: await this.createAccessToken({
|
|
563
604
|
userId,
|
|
@@ -588,20 +629,20 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
588
629
|
*/
|
|
589
630
|
async handleRefreshTokenGrant(body, clientInfo, env) {
|
|
590
631
|
const refreshToken = body.refresh_token;
|
|
591
|
-
if (!refreshToken) return this.createErrorResponse("invalid_request", "Refresh token is required");
|
|
632
|
+
if (!refreshToken) return this.createErrorResponse("invalid_request", { description: "Refresh token is required" });
|
|
592
633
|
const tokenParts = refreshToken.split(":");
|
|
593
|
-
if (tokenParts.length !== 3) return this.createErrorResponse("invalid_grant", "Invalid token format");
|
|
634
|
+
if (tokenParts.length !== 3) return this.createErrorResponse("invalid_grant", { description: "Invalid token format" });
|
|
594
635
|
const [userId, grantId, _] = tokenParts;
|
|
595
636
|
const providedTokenHash = await generateTokenId(refreshToken);
|
|
596
637
|
const grantKey = `grant:${userId}:${grantId}`;
|
|
597
638
|
const grantData = await env.OAUTH_KV.get(grantKey, { type: "json" });
|
|
598
|
-
if (!grantData) return this.createErrorResponse("invalid_grant", "Grant not found");
|
|
639
|
+
if (!grantData) return this.createErrorResponse("invalid_grant", { description: "Grant not found" });
|
|
599
640
|
const isCurrentToken = grantData.refreshTokenId === providedTokenHash;
|
|
600
641
|
const isPreviousToken = grantData.previousRefreshTokenId === providedTokenHash;
|
|
601
|
-
if (!isCurrentToken && !isPreviousToken) return this.createErrorResponse("invalid_grant", "Invalid refresh token");
|
|
602
|
-
if (grantData.clientId !== clientInfo.clientId) return this.createErrorResponse("invalid_grant", "Client ID mismatch");
|
|
642
|
+
if (!isCurrentToken && !isPreviousToken) return this.createErrorResponse("invalid_grant", { description: "Invalid refresh token" });
|
|
643
|
+
if (grantData.clientId !== clientInfo.clientId) return this.createErrorResponse("invalid_grant", { description: "Client ID mismatch" });
|
|
603
644
|
if (grantData.expiresAt !== void 0) {
|
|
604
|
-
if (Math.floor(Date.now() / 1e3) >= grantData.expiresAt) return this.createErrorResponse("invalid_grant", "Refresh token has expired");
|
|
645
|
+
if (Math.floor(Date.now() / 1e3) >= grantData.expiresAt) return this.createErrorResponse("invalid_grant", { description: "Refresh token has expired" });
|
|
605
646
|
}
|
|
606
647
|
const newAccessToken = `${userId}:${grantId}:${generateRandomString(TOKEN_LENGTH)}`;
|
|
607
648
|
const accessTokenId = await generateTokenId(newAccessToken);
|
|
@@ -636,7 +677,7 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
636
677
|
}
|
|
637
678
|
if (callbackResult.accessTokenProps) accessTokenProps = callbackResult.accessTokenProps;
|
|
638
679
|
if (callbackResult.accessTokenTTL !== void 0) accessTokenTTL = callbackResult.accessTokenTTL;
|
|
639
|
-
if ("refreshTokenTTL" in callbackResult) return this.createErrorResponse("invalid_request", "refreshTokenTTL cannot be changed during refresh token exchange");
|
|
680
|
+
if ("refreshTokenTTL" in callbackResult) return this.createErrorResponse("invalid_request", { description: "refreshTokenTTL cannot be changed during refresh token exchange" });
|
|
640
681
|
if (callbackResult.accessTokenScope) tokenScopes = this.downscope(callbackResult.accessTokenScope, grantData.scope);
|
|
641
682
|
}
|
|
642
683
|
if (grantPropsChanged) {
|
|
@@ -675,10 +716,10 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
675
716
|
if (body.resource && grantData.resource) {
|
|
676
717
|
const requestedResources = Array.isArray(body.resource) ? body.resource : [body.resource];
|
|
677
718
|
const grantedResources = Array.isArray(grantData.resource) ? grantData.resource : [grantData.resource];
|
|
678
|
-
for (const requested of requestedResources) if (!grantedResources.some((granted) => resourceMatches(requested, granted, originOnly))) return this.createErrorResponse("invalid_target", "Requested resource was not included in the authorization request");
|
|
719
|
+
for (const requested of requestedResources) if (!grantedResources.some((granted) => resourceMatches(requested, granted, originOnly))) return this.createErrorResponse("invalid_target", { description: "Requested resource was not included in the authorization request" });
|
|
679
720
|
}
|
|
680
721
|
const audience = parseResourceParameter(body.resource || grantData.resource);
|
|
681
|
-
if ((body.resource || grantData.resource) && !audience) return this.createErrorResponse("invalid_target", "The resource parameter must be a valid absolute URI without a fragment");
|
|
722
|
+
if ((body.resource || grantData.resource) && !audience) return this.createErrorResponse("invalid_target", { description: "The resource parameter must be a valid absolute URI without a fragment" });
|
|
682
723
|
const accessTokenData = {
|
|
683
724
|
id: accessTokenId,
|
|
684
725
|
grantId,
|
|
@@ -694,7 +735,12 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
694
735
|
encryptedProps: encryptedAccessTokenProps
|
|
695
736
|
}
|
|
696
737
|
};
|
|
697
|
-
|
|
738
|
+
try {
|
|
739
|
+
await env.OAUTH_KV.put(`token:${userId}:${grantId}:${accessTokenId}`, JSON.stringify(accessTokenData), { expirationTtl: accessTokenTTL });
|
|
740
|
+
} catch (error) {
|
|
741
|
+
this.throwRetryableTokenStorageErrorIfKvRateLimited(error);
|
|
742
|
+
throw error;
|
|
743
|
+
}
|
|
698
744
|
const tokenResponse = {
|
|
699
745
|
access_token: newAccessToken,
|
|
700
746
|
token_type: "bearer",
|
|
@@ -722,10 +768,10 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
722
768
|
*/
|
|
723
769
|
async exchangeToken(subjectToken, requestedScopes, requestedResource, expiresIn, clientInfo, env) {
|
|
724
770
|
const tokenSummary = await this.unwrapToken(subjectToken, env);
|
|
725
|
-
if (!tokenSummary) throw new OAuthError("invalid_grant", "Invalid or expired subject token");
|
|
771
|
+
if (!tokenSummary) throw new OAuthError("invalid_grant", { description: "Invalid or expired subject token" });
|
|
726
772
|
const grantKey = `grant:${tokenSummary.userId}:${tokenSummary.grantId}`;
|
|
727
773
|
const grantData = await env.OAUTH_KV.get(grantKey, { type: "json" });
|
|
728
|
-
if (!grantData) throw new OAuthError("invalid_grant", "Grant not found");
|
|
774
|
+
if (!grantData) throw new OAuthError("invalid_grant", { description: "Grant not found" });
|
|
729
775
|
let tokenScopes = this.downscope(requestedScopes, grantData.scope);
|
|
730
776
|
const originOnly = !!this.options.resourceMatchOriginOnly;
|
|
731
777
|
let newAudience = tokenSummary.audience;
|
|
@@ -733,21 +779,21 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
733
779
|
if (grantData.resource) {
|
|
734
780
|
const requestedResources = Array.isArray(requestedResource) ? requestedResource : [requestedResource];
|
|
735
781
|
const grantedResources = Array.isArray(grantData.resource) ? grantData.resource : [grantData.resource];
|
|
736
|
-
for (const requested of requestedResources) if (!grantedResources.some((granted) => resourceMatches(requested, granted, originOnly))) throw new OAuthError("invalid_target", "Requested resource was not included in the authorization request");
|
|
782
|
+
for (const requested of requestedResources) if (!grantedResources.some((granted) => resourceMatches(requested, granted, originOnly))) throw new OAuthError("invalid_target", { description: "Requested resource was not included in the authorization request" });
|
|
737
783
|
}
|
|
738
784
|
const parsedResource = parseResourceParameter(requestedResource);
|
|
739
|
-
if (!parsedResource) throw new OAuthError("invalid_target", "The resource parameter must be a valid absolute URI without a fragment");
|
|
785
|
+
if (!parsedResource) throw new OAuthError("invalid_target", { description: "The resource parameter must be a valid absolute URI without a fragment" });
|
|
740
786
|
newAudience = parsedResource;
|
|
741
787
|
}
|
|
742
788
|
const now = Math.floor(Date.now() / 1e3);
|
|
743
789
|
const subjectTokenRemainingLifetime = tokenSummary.expiresAt - now;
|
|
744
790
|
let accessTokenTTL = this.options.accessTokenTTL ?? DEFAULT_ACCESS_TOKEN_TTL;
|
|
745
791
|
if (expiresIn !== void 0) {
|
|
746
|
-
if (expiresIn <= 0) throw new OAuthError("invalid_request", "Invalid expires_in parameter");
|
|
792
|
+
if (expiresIn <= 0) throw new OAuthError("invalid_request", { description: "Invalid expires_in parameter" });
|
|
747
793
|
accessTokenTTL = Math.min(expiresIn, subjectTokenRemainingLifetime);
|
|
748
794
|
} else accessTokenTTL = Math.min(accessTokenTTL, subjectTokenRemainingLifetime);
|
|
749
795
|
const subjectTokenData = await env.OAUTH_KV.get(`token:${tokenSummary.userId}:${tokenSummary.grantId}:${tokenSummary.id}`, { type: "json" });
|
|
750
|
-
if (!subjectTokenData) throw new OAuthError("invalid_grant", "Subject token data not found");
|
|
796
|
+
if (!subjectTokenData) throw new OAuthError("invalid_grant", { description: "Subject token data not found" });
|
|
751
797
|
const encryptionKey = await unwrapKeyWithToken(subjectToken, subjectTokenData.wrappedEncryptionKey);
|
|
752
798
|
let accessTokenEncryptionKey = encryptionKey;
|
|
753
799
|
let encryptedAccessTokenProps = subjectTokenData.grant.encryptedProps;
|
|
@@ -811,25 +857,26 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
811
857
|
const requestedTokenType = body.requested_token_type || "urn:ietf:params:oauth:token-type:access_token";
|
|
812
858
|
const requestedScope = body.scope;
|
|
813
859
|
const requestedResource = body.resource;
|
|
814
|
-
if (!subjectToken) return this.createErrorResponse("invalid_request", "subject_token is required");
|
|
815
|
-
if (!subjectTokenType) return this.createErrorResponse("invalid_request", "subject_token_type is required");
|
|
816
|
-
if (subjectTokenType !== "urn:ietf:params:oauth:token-type:access_token") return this.createErrorResponse("invalid_request", "Only access_token subject_token_type is supported");
|
|
817
|
-
if (requestedTokenType !== "urn:ietf:params:oauth:token-type:access_token") return this.createErrorResponse("invalid_request", "Only access_token requested_token_type is supported");
|
|
860
|
+
if (!subjectToken) return this.createErrorResponse("invalid_request", { description: "subject_token is required" });
|
|
861
|
+
if (!subjectTokenType) return this.createErrorResponse("invalid_request", { description: "subject_token_type is required" });
|
|
862
|
+
if (subjectTokenType !== "urn:ietf:params:oauth:token-type:access_token") return this.createErrorResponse("invalid_request", { description: "Only access_token subject_token_type is supported" });
|
|
863
|
+
if (requestedTokenType !== "urn:ietf:params:oauth:token-type:access_token") return this.createErrorResponse("invalid_request", { description: "Only access_token requested_token_type is supported" });
|
|
818
864
|
let requestedScopes;
|
|
819
865
|
if (requestedScope) if (typeof requestedScope === "string") requestedScopes = requestedScope.split(" ").filter(Boolean);
|
|
820
866
|
else if (Array.isArray(requestedScope)) requestedScopes = requestedScope;
|
|
821
|
-
else return this.createErrorResponse("invalid_request", "Invalid scope parameter format");
|
|
867
|
+
else return this.createErrorResponse("invalid_request", { description: "Invalid scope parameter format" });
|
|
822
868
|
let expiresIn;
|
|
823
869
|
if (body.expires_in !== void 0) {
|
|
824
870
|
const requestedTTL = parseInt(body.expires_in, 10);
|
|
825
|
-
if (isNaN(requestedTTL) || requestedTTL <= 0) return this.createErrorResponse("invalid_request", "Invalid expires_in parameter");
|
|
871
|
+
if (isNaN(requestedTTL) || requestedTTL <= 0) return this.createErrorResponse("invalid_request", { description: "Invalid expires_in parameter" });
|
|
826
872
|
expiresIn = requestedTTL;
|
|
827
873
|
}
|
|
828
874
|
try {
|
|
829
875
|
const tokenResponse = await this.exchangeToken(subjectToken, requestedScopes, requestedResource, expiresIn, clientInfo, env);
|
|
830
876
|
return new Response(JSON.stringify(tokenResponse), { headers: { "Content-Type": "application/json" } });
|
|
831
877
|
} catch (error) {
|
|
832
|
-
|
|
878
|
+
const response = this.createOAuthErrorResponse(error);
|
|
879
|
+
if (response) return response;
|
|
833
880
|
throw error;
|
|
834
881
|
}
|
|
835
882
|
}
|
|
@@ -851,7 +898,7 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
851
898
|
*/
|
|
852
899
|
async revokeToken(body, env) {
|
|
853
900
|
const token = body.token;
|
|
854
|
-
if (!token) return this.createErrorResponse("invalid_request", "Token parameter is required");
|
|
901
|
+
if (!token) return this.createErrorResponse("invalid_request", { description: "Token parameter is required" });
|
|
855
902
|
const tokenParts = token.split(":");
|
|
856
903
|
if (tokenParts.length !== 3) return new Response("", { status: 200 });
|
|
857
904
|
const [userId, grantId, _] = tokenParts;
|
|
@@ -909,20 +956,35 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
909
956
|
* @returns Response with client registration data or error
|
|
910
957
|
*/
|
|
911
958
|
async handleClientRegistration(request, env) {
|
|
912
|
-
if (!this.options.clientRegistrationEndpoint) return this.createErrorResponse("not_implemented",
|
|
913
|
-
|
|
914
|
-
|
|
959
|
+
if (!this.options.clientRegistrationEndpoint) return this.createErrorResponse("not_implemented", {
|
|
960
|
+
description: "Client registration is not enabled",
|
|
961
|
+
statusCode: 501
|
|
962
|
+
});
|
|
963
|
+
if (request.method !== "POST") return this.createErrorResponse("invalid_request", {
|
|
964
|
+
description: "Method not allowed",
|
|
965
|
+
statusCode: 405
|
|
966
|
+
});
|
|
967
|
+
if (parseInt(request.headers.get("Content-Length") || "0", 10) > 1048576) return this.createErrorResponse("invalid_request", {
|
|
968
|
+
description: "Request payload too large, must be under 1 MiB",
|
|
969
|
+
statusCode: 413
|
|
970
|
+
});
|
|
915
971
|
let clientMetadata;
|
|
916
972
|
try {
|
|
917
973
|
const text = await request.text();
|
|
918
|
-
if (text.length > 1048576) return this.createErrorResponse("invalid_request",
|
|
974
|
+
if (text.length > 1048576) return this.createErrorResponse("invalid_request", {
|
|
975
|
+
description: "Request payload too large, must be under 1 MiB",
|
|
976
|
+
statusCode: 413
|
|
977
|
+
});
|
|
919
978
|
clientMetadata = JSON.parse(text);
|
|
920
979
|
} catch (error) {
|
|
921
|
-
return this.createErrorResponse("invalid_request",
|
|
980
|
+
return this.createErrorResponse("invalid_request", {
|
|
981
|
+
description: "Invalid JSON payload",
|
|
982
|
+
statusCode: 400
|
|
983
|
+
});
|
|
922
984
|
}
|
|
923
985
|
const authMethod = OAuthProviderImpl.validateStringField(clientMetadata.token_endpoint_auth_method) || "client_secret_basic";
|
|
924
986
|
const isPublicClient = authMethod === "none";
|
|
925
|
-
if (isPublicClient && this.options.disallowPublicClientRegistration) return this.createErrorResponse("invalid_client_metadata", "Public client registration is not allowed");
|
|
987
|
+
if (isPublicClient && this.options.disallowPublicClientRegistration) return this.createErrorResponse("invalid_client_metadata", { description: "Public client registration is not allowed" });
|
|
926
988
|
const clientId = generateRandomString(16);
|
|
927
989
|
let clientSecret;
|
|
928
990
|
let hashedSecret;
|
|
@@ -956,7 +1018,7 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
956
1018
|
};
|
|
957
1019
|
if (!isPublicClient && hashedSecret) clientInfo.clientSecret = hashedSecret;
|
|
958
1020
|
} catch (error) {
|
|
959
|
-
return this.createErrorResponse("invalid_client_metadata", error instanceof Error ? error.message : "Invalid client metadata");
|
|
1021
|
+
return this.createErrorResponse("invalid_client_metadata", { description: error instanceof Error ? error.message : "Invalid client metadata" });
|
|
960
1022
|
}
|
|
961
1023
|
const clientKvOptions = {};
|
|
962
1024
|
if (this.options.clientRegistrationTTL !== void 0) clientKvOptions.expirationTtl = this.options.clientRegistrationTTL;
|
|
@@ -998,7 +1060,11 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
998
1060
|
const url = new URL(request.url);
|
|
999
1061
|
const resourceMetadataUrl = `${url.origin}/.well-known/oauth-protected-resource${url.pathname}`;
|
|
1000
1062
|
const authHeader = request.headers.get("Authorization");
|
|
1001
|
-
if (!authHeader || !authHeader.startsWith("Bearer ")) return this.createErrorResponse("invalid_token",
|
|
1063
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) return this.createErrorResponse("invalid_token", {
|
|
1064
|
+
description: "Missing or invalid access token",
|
|
1065
|
+
statusCode: 401,
|
|
1066
|
+
headers: { "WWW-Authenticate": this.buildWwwAuthenticateHeader(resourceMetadataUrl, "invalid_token", "Missing or invalid access token") }
|
|
1067
|
+
});
|
|
1002
1068
|
const accessToken = authHeader.substring(7);
|
|
1003
1069
|
const parts = accessToken.split(":");
|
|
1004
1070
|
const isPossiblyInternalFormat = parts.length === 3;
|
|
@@ -1010,14 +1076,26 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
1010
1076
|
const id = await generateTokenId(accessToken);
|
|
1011
1077
|
tokenData = await env.OAUTH_KV.get(`token:${userId}:${grantId}:${id}`, { type: "json" });
|
|
1012
1078
|
}
|
|
1013
|
-
if (!tokenData && !this.options.resolveExternalToken) return this.createErrorResponse("invalid_token",
|
|
1079
|
+
if (!tokenData && !this.options.resolveExternalToken) return this.createErrorResponse("invalid_token", {
|
|
1080
|
+
description: "Invalid access token",
|
|
1081
|
+
statusCode: 401,
|
|
1082
|
+
headers: { "WWW-Authenticate": this.buildWwwAuthenticateHeader(resourceMetadataUrl, "invalid_token") }
|
|
1083
|
+
});
|
|
1014
1084
|
if (tokenData) {
|
|
1015
1085
|
const now = Math.floor(Date.now() / 1e3);
|
|
1016
|
-
if (tokenData.expiresAt < now) return this.createErrorResponse("invalid_token",
|
|
1086
|
+
if (tokenData.expiresAt < now) return this.createErrorResponse("invalid_token", {
|
|
1087
|
+
description: "Access token expired",
|
|
1088
|
+
statusCode: 401,
|
|
1089
|
+
headers: { "WWW-Authenticate": this.buildWwwAuthenticateHeader(resourceMetadataUrl, "invalid_token") }
|
|
1090
|
+
});
|
|
1017
1091
|
if (tokenData.audience) {
|
|
1018
1092
|
const requestUrl = new URL(request.url);
|
|
1019
1093
|
const resourceServer = `${requestUrl.protocol}//${requestUrl.host}${requestUrl.pathname}`;
|
|
1020
|
-
if (!(Array.isArray(tokenData.audience) ? tokenData.audience : [tokenData.audience]).some((aud) => audienceMatches(resourceServer, aud))) return this.createErrorResponse("invalid_token",
|
|
1094
|
+
if (!(Array.isArray(tokenData.audience) ? tokenData.audience : [tokenData.audience]).some((aud) => audienceMatches(resourceServer, aud))) return this.createErrorResponse("invalid_token", {
|
|
1095
|
+
description: "Token audience does not match resource server",
|
|
1096
|
+
statusCode: 401,
|
|
1097
|
+
headers: { "WWW-Authenticate": this.buildWwwAuthenticateHeader(resourceMetadataUrl, "invalid_token", "Invalid audience") }
|
|
1098
|
+
});
|
|
1021
1099
|
}
|
|
1022
1100
|
ctx.props = await decryptProps(await unwrapKeyWithToken(accessToken, tokenData.wrappedEncryptionKey), tokenData.grant.encryptedProps);
|
|
1023
1101
|
} else if (this.options.resolveExternalToken) {
|
|
@@ -1026,17 +1104,28 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
1026
1104
|
request,
|
|
1027
1105
|
env
|
|
1028
1106
|
});
|
|
1029
|
-
if (!ext) return this.createErrorResponse("invalid_token",
|
|
1107
|
+
if (!ext) return this.createErrorResponse("invalid_token", {
|
|
1108
|
+
description: "Invalid access token",
|
|
1109
|
+
statusCode: 401,
|
|
1110
|
+
headers: { "WWW-Authenticate": this.buildWwwAuthenticateHeader(resourceMetadataUrl, "invalid_token") }
|
|
1111
|
+
});
|
|
1030
1112
|
if (ext.audience) {
|
|
1031
1113
|
const requestUrl = new URL(request.url);
|
|
1032
1114
|
const resourceServer = `${requestUrl.protocol}//${requestUrl.host}${requestUrl.pathname}`;
|
|
1033
|
-
if (!(Array.isArray(ext.audience) ? ext.audience : [ext.audience]).some((aud) => audienceMatches(resourceServer, aud))) return this.createErrorResponse("invalid_token",
|
|
1115
|
+
if (!(Array.isArray(ext.audience) ? ext.audience : [ext.audience]).some((aud) => audienceMatches(resourceServer, aud))) return this.createErrorResponse("invalid_token", {
|
|
1116
|
+
description: "Token audience does not match resource server",
|
|
1117
|
+
statusCode: 401,
|
|
1118
|
+
headers: { "WWW-Authenticate": this.buildWwwAuthenticateHeader(resourceMetadataUrl, "invalid_token", "Invalid audience") }
|
|
1119
|
+
});
|
|
1034
1120
|
}
|
|
1035
1121
|
ctx.props = ext.props;
|
|
1036
1122
|
}
|
|
1037
1123
|
if (!env.OAUTH_PROVIDER) env.OAUTH_PROVIDER = this.createOAuthHelpers(env);
|
|
1038
1124
|
const apiHandler = this.findApiHandlerForUrl(url);
|
|
1039
|
-
if (!apiHandler) return this.createErrorResponse("invalid_request",
|
|
1125
|
+
if (!apiHandler) return this.createErrorResponse("invalid_request", {
|
|
1126
|
+
description: "No handler found for API route",
|
|
1127
|
+
statusCode: 404
|
|
1128
|
+
});
|
|
1040
1129
|
if (apiHandler.type === HandlerType.EXPORTED_HANDLER) return apiHandler.handler.fetch(request, env, ctx);
|
|
1041
1130
|
else return new apiHandler.handler(ctx, env).fetch(request);
|
|
1042
1131
|
}
|
|
@@ -1058,7 +1147,24 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
1058
1147
|
*/
|
|
1059
1148
|
async saveGrantWithTTL(env, grantKey, grantData, now) {
|
|
1060
1149
|
const kvOptions = grantData.expiresAt !== void 0 ? { expiration: grantData.expiresAt } : {};
|
|
1061
|
-
|
|
1150
|
+
try {
|
|
1151
|
+
await env.OAUTH_KV.put(grantKey, JSON.stringify(grantData), kvOptions);
|
|
1152
|
+
} catch (error) {
|
|
1153
|
+
this.throwRetryableTokenStorageErrorIfKvRateLimited(error);
|
|
1154
|
+
throw error;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
throwRetryableTokenStorageErrorIfKvRateLimited(error) {
|
|
1158
|
+
if (!this.isKvRateLimitError(error)) return;
|
|
1159
|
+
throw new OAuthError("temporarily_unavailable", {
|
|
1160
|
+
description: "Token issuance is temporarily unavailable; retry shortly",
|
|
1161
|
+
statusCode: 429,
|
|
1162
|
+
headers: { "Retry-After": "30" }
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
isKvRateLimitError(error) {
|
|
1166
|
+
if (!(error instanceof Error)) return false;
|
|
1167
|
+
return /KV .*failed: 429 Too Many Requests/i.test(error.message) || /429 Too Many Requests/i.test(error.message);
|
|
1062
1168
|
}
|
|
1063
1169
|
/**
|
|
1064
1170
|
* Fetches client information from KV storage or via CIMD (Client ID Metadata Document)
|
|
@@ -1115,7 +1221,12 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
1115
1221
|
encryptedProps
|
|
1116
1222
|
}
|
|
1117
1223
|
};
|
|
1118
|
-
|
|
1224
|
+
try {
|
|
1225
|
+
await env.OAUTH_KV.put(`token:${userId}:${grantId}:${accessTokenId}`, JSON.stringify(accessTokenData), { expirationTtl: expiresIn });
|
|
1226
|
+
} catch (error) {
|
|
1227
|
+
this.throwRetryableTokenStorageErrorIfKvRateLimited(error);
|
|
1228
|
+
throw error;
|
|
1229
|
+
}
|
|
1119
1230
|
return accessToken;
|
|
1120
1231
|
}
|
|
1121
1232
|
/**
|
|
@@ -1278,17 +1389,18 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
1278
1389
|
/**
|
|
1279
1390
|
* Helper function to create OAuth error responses
|
|
1280
1391
|
* @param code - OAuth error code (e.g., 'invalid_request', 'invalid_token')
|
|
1281
|
-
* @param
|
|
1282
|
-
* @param status - HTTP status code (default: 400)
|
|
1283
|
-
* @param headers - Additional headers to include
|
|
1392
|
+
* @param options - Error response options
|
|
1284
1393
|
* @returns A Response object with the error
|
|
1285
1394
|
*/
|
|
1286
|
-
createErrorResponse(code,
|
|
1395
|
+
createErrorResponse(code, options) {
|
|
1396
|
+
const { description } = options;
|
|
1397
|
+
const responseStatus = options.statusCode ?? 400;
|
|
1398
|
+
const responseHeaders = options.headers ?? {};
|
|
1287
1399
|
const customErrorResponse = this.options.onError?.({
|
|
1288
1400
|
code,
|
|
1289
1401
|
description,
|
|
1290
|
-
status,
|
|
1291
|
-
headers
|
|
1402
|
+
status: responseStatus,
|
|
1403
|
+
headers: responseHeaders
|
|
1292
1404
|
});
|
|
1293
1405
|
if (customErrorResponse) return customErrorResponse;
|
|
1294
1406
|
const body = JSON.stringify({
|
|
@@ -1296,23 +1408,66 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
1296
1408
|
error_description: description
|
|
1297
1409
|
});
|
|
1298
1410
|
return new Response(body, {
|
|
1299
|
-
status,
|
|
1411
|
+
status: responseStatus,
|
|
1300
1412
|
headers: {
|
|
1301
1413
|
"Content-Type": "application/json",
|
|
1302
|
-
...
|
|
1414
|
+
...responseHeaders
|
|
1303
1415
|
}
|
|
1304
1416
|
});
|
|
1305
1417
|
}
|
|
1306
1418
|
};
|
|
1307
1419
|
/**
|
|
1308
|
-
*
|
|
1309
|
-
*
|
|
1420
|
+
* Structured OAuth 2.0 error.
|
|
1421
|
+
*
|
|
1422
|
+
* Throw from a `tokenExchangeCallback` (or any code it calls — the error
|
|
1423
|
+
* propagates naturally up through deep call stacks) to surface a standard
|
|
1424
|
+
* `/token` error response (`{ error, error_description }`) instead of a
|
|
1425
|
+
* generic `500 Internal Server Error`.
|
|
1426
|
+
*
|
|
1427
|
+
* Anything thrown that is **not** an `OAuthError` continues to surface as
|
|
1428
|
+
* a 500 so unexpected failures remain visible — the provider does not
|
|
1429
|
+
* catch-everything-and-return-400.
|
|
1430
|
+
*
|
|
1431
|
+
* @example
|
|
1432
|
+
* ```ts
|
|
1433
|
+
* import { OAuthError } from '@cloudflare/workers-oauth-provider';
|
|
1434
|
+
*
|
|
1435
|
+
* tokenExchangeCallback: async (options) => {
|
|
1436
|
+
* if (options.grantType === 'refresh_token') {
|
|
1437
|
+
* // refreshUpstream() may throw OAuthError from any depth
|
|
1438
|
+
* return { newProps: await refreshUpstream(options.props) };
|
|
1439
|
+
* }
|
|
1440
|
+
* }
|
|
1441
|
+
*
|
|
1442
|
+
* async function refreshUpstream(props) {
|
|
1443
|
+
* const res = await fetch(...);
|
|
1444
|
+
* if (res.status === 401) {
|
|
1445
|
+
* throw new OAuthError('invalid_grant', { description: 'upstream refresh token is invalid' });
|
|
1446
|
+
* }
|
|
1447
|
+
* if (res.status === 429) {
|
|
1448
|
+
* // Mirror upstream's Retry-After if present, otherwise pick a default.
|
|
1449
|
+
* throw new OAuthError('temporarily_unavailable', {
|
|
1450
|
+
* description: 'upstream rate limited',
|
|
1451
|
+
* statusCode: 429,
|
|
1452
|
+
* headers: { 'Retry-After': res.headers.get('retry-after') ?? '60' },
|
|
1453
|
+
* });
|
|
1454
|
+
* }
|
|
1455
|
+
* return await res.json();
|
|
1456
|
+
* }
|
|
1457
|
+
* ```
|
|
1310
1458
|
*/
|
|
1311
1459
|
var OAuthError = class extends Error {
|
|
1312
|
-
constructor(code,
|
|
1313
|
-
super(
|
|
1314
|
-
this.code = code;
|
|
1460
|
+
constructor(code, options) {
|
|
1461
|
+
super(options.description);
|
|
1315
1462
|
this.name = "OAuthError";
|
|
1463
|
+
this.code = code;
|
|
1464
|
+
this.options = {
|
|
1465
|
+
...options,
|
|
1466
|
+
statusCode: options.statusCode ?? 400
|
|
1467
|
+
};
|
|
1468
|
+
this.description = this.options.description;
|
|
1469
|
+
this.statusCode = this.options.statusCode;
|
|
1470
|
+
this.headers = this.options.headers;
|
|
1316
1471
|
}
|
|
1317
1472
|
};
|
|
1318
1473
|
/**
|
|
@@ -2075,4 +2230,4 @@ var OAuthHelpersImpl = class {
|
|
|
2075
2230
|
var oauth_provider_default = OAuthProvider;
|
|
2076
2231
|
|
|
2077
2232
|
//#endregion
|
|
2078
|
-
export { GrantType, OAuthProvider, oauth_provider_default as default, getOAuthApi };
|
|
2233
|
+
export { GrantType, OAuthError, OAuthProvider, oauth_provider_default as default, getOAuthApi };
|