@cloudflare/workers-oauth-provider 0.9.1 → 0.10.1
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 +29 -14
- package/dist/oauth-provider.d.ts +37 -11
- package/dist/oauth-provider.js +137 -43
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,7 +36,12 @@ See [Client registration](#client-registration) for the matching provider option
|
|
|
36
36
|
The provider accepts either plain `ExportedHandler` objects or classes extending `WorkerEntrypoint`. This example uses both.
|
|
37
37
|
|
|
38
38
|
```ts
|
|
39
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
AuthorizationError,
|
|
41
|
+
OAuthProvider,
|
|
42
|
+
type AuthRequest,
|
|
43
|
+
type OAuthHelpers,
|
|
44
|
+
} from '@cloudflare/workers-oauth-provider';
|
|
40
45
|
import { WorkerEntrypoint } from 'cloudflare:workers';
|
|
41
46
|
|
|
42
47
|
interface AuthProps {
|
|
@@ -72,9 +77,18 @@ const defaultHandler: ExportedHandler<Env> = {
|
|
|
72
77
|
let oauthRequest: AuthRequest;
|
|
73
78
|
try {
|
|
74
79
|
oauthRequest = await env.OAUTH_PROVIDER.parseAuthRequest(request);
|
|
75
|
-
} catch {
|
|
76
|
-
|
|
77
|
-
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (!(error instanceof AuthorizationError)) throw error;
|
|
82
|
+
if (!error.redirectUri) {
|
|
83
|
+
// Unknown clients and invalid redirects must be rendered locally.
|
|
84
|
+
return new Response(error.description, { status: 400 });
|
|
85
|
+
}
|
|
86
|
+
const redirect = new URL(error.redirectUri);
|
|
87
|
+
redirect.searchParams.set('error', error.code);
|
|
88
|
+
redirect.searchParams.set('error_description', error.description);
|
|
89
|
+
if (error.state) redirect.searchParams.set('state', error.state);
|
|
90
|
+
if (error.issuer) redirect.searchParams.set('iss', error.issuer);
|
|
91
|
+
return Response.redirect(redirect, 302);
|
|
78
92
|
}
|
|
79
93
|
|
|
80
94
|
const client = await env.OAUTH_PROVIDER.lookupClient(oauthRequest.clientId);
|
|
@@ -228,7 +242,9 @@ A typical flow has three steps:
|
|
|
228
242
|
2. Authenticate the user, show consent, and decide which scopes to grant.
|
|
229
243
|
3. Call `completeAuthorization()` and redirect to its returned `redirectTo` URL.
|
|
230
244
|
|
|
231
|
-
`
|
|
245
|
+
`parseAuthRequest()` throws an exported `AuthorizationError` for expected request validation failures. Its optional `redirectUri` is present only after the client and exact registered redirect URI have been validated. Without it, render the error locally and never redirect. With it, the application can safely construct an OAuth error redirect using the error's `code`, `description`, original `state`, and RFC 9207 `issuer`, as shown in the quick start.
|
|
246
|
+
|
|
247
|
+
`completeAuthorization()` repeats response-type validation before writing a grant or revoking existing grants. Validation errors from reconstructed requests are also typed as `AuthorizationError`, but applications should not construct redirects from untrusted reconstructed values; the redirect context is attached only by `parseAuthRequest()`.
|
|
232
248
|
|
|
233
249
|
`completeAuthorization()` stores a new grant and, by default, revokes existing grants for the same user and client after the new grant is safely stored. Set `revokeExistingGrants: false` only when the application intentionally allows concurrent grants for the same user and client.
|
|
234
250
|
|
|
@@ -305,6 +321,10 @@ MCP 2026-07-28 deprecates DCR for new implementations in favor of CIMD. The endp
|
|
|
305
321
|
|
|
306
322
|
Registration accepts only authentication methods, grants, and response types implemented by the configured provider, and rejects inconsistent grant/response combinations before storage. Omitted metadata uses the RFC 7591 defaults: `client_secret_basic`, `grant_types: ["authorization_code"]`, and `response_types: ["code"]`.
|
|
307
323
|
|
|
324
|
+
An explicitly supplied `token_endpoint_auth_method` is enforced exactly. When it is omitted, no explicit-method marker is stored and the client may use either `client_secret_basic` or `client_secret_post`, provided the same stored secret validates. Client records written by earlier releases have no marker and receive the same compatibility. This never crosses between `none` and a secret method and does not apply to CIMD clients.
|
|
325
|
+
|
|
326
|
+
Calling `OAuthHelpers.updateClient()` with `tokenEndpointAuthMethod` adds the marker; unrelated updates leave it unchanged.
|
|
327
|
+
|
|
308
328
|
Related options:
|
|
309
329
|
|
|
310
330
|
- `clientRegistrationTTL` controls the lifetime of dynamically registered clients. The default is 90 days.
|
|
@@ -329,16 +349,11 @@ The provider owns `tokenEndpoint`. It exchanges authorization codes for tokens,
|
|
|
329
349
|
|
|
330
350
|
## Resources and token audiences
|
|
331
351
|
|
|
332
|
-
MCP clients
|
|
333
|
-
|
|
334
|
-
Resource policy follows `resourceMetadata.resource`:
|
|
335
|
-
|
|
336
|
-
- When configured, authorization requests, token requests, and externally resolved tokens must use that one exact resource. `resourceMatchOriginOnly` cannot weaken this policy.
|
|
337
|
-
- When omitted, valid resources are accepted. Token requests may inherit the authorization resource. If the authorization request also omits it, the provider uses the request origin as the default and issues an origin-bound token.
|
|
352
|
+
MCP clients are required to send the canonical MCP server URI as `resource` in authorization and token requests. The provider tolerates omission for compatibility: when `resourceMetadata.resource` is configured, it is used as the canonical default and inherited by later token requests; otherwise a token request inherits any resource already stored on the grant. An explicit resource that does not match a bound grant is rejected with `invalid_target`.
|
|
338
353
|
|
|
339
|
-
Path-aware audiences use path-boundary prefix matching
|
|
354
|
+
Legacy grants may have no stored resource. With no configured canonical resource, omitting `resource` preserves that unbound state. If a client supplies a resource during code exchange or refresh, it applies to that issued token but is not persisted as a new grant binding. Path-aware audiences use path-boundary prefix matching, so a token for `https://example.com/mcp` can be used at `/mcp/tools`, but not at `/mcp-other`.
|
|
340
355
|
|
|
341
|
-
`resourceMatchOriginOnly`
|
|
356
|
+
`resourceMatchOriginOnly` is deprecated; its existing behavior is unchanged. Prefer `resourceMetadata.resource` for new deployments.
|
|
342
357
|
|
|
343
358
|
## Scopes and step-up authorization
|
|
344
359
|
|
|
@@ -415,7 +430,7 @@ Deleting a client through `OAuthHelpers.deleteClient()` also revokes its grants
|
|
|
415
430
|
| `allowTokenExchangeGrant` | Enable RFC 8693 | `false` |
|
|
416
431
|
| `tokenExchangeCallback` | Update props, scopes, or lifetimes during token exchange | None |
|
|
417
432
|
| `resolveExternalToken` | Validate external bearer credentials (advanced) | None |
|
|
418
|
-
| `resourceMatchOriginOnly` |
|
|
433
|
+
| `resourceMatchOriginOnly` | Deprecated origin-only resource comparison | `false` |
|
|
419
434
|
| `enterpriseManagedAuthorization` | Enable experimental ID-JAG grant support | Disabled |
|
|
420
435
|
| `onError` | Observe or replace OAuth error responses | Logs a warning |
|
|
421
436
|
|
package/dist/oauth-provider.d.ts
CHANGED
|
@@ -299,6 +299,29 @@ interface EmaOptions<Env = Cloudflare.Env> {
|
|
|
299
299
|
}
|
|
300
300
|
//#endregion
|
|
301
301
|
//#region src/oauth-capabilities.d.ts
|
|
302
|
+
type AuthorizationErrorCode = 'invalid_request' | 'invalid_target' | 'unauthorized_client' | 'access_denied' | 'unsupported_response_type' | 'invalid_scope' | 'server_error' | 'temporarily_unavailable';
|
|
303
|
+
interface AuthorizationErrorOptions {
|
|
304
|
+
/** Wire-safe OAuth authorization error description. */
|
|
305
|
+
description: string;
|
|
306
|
+
/** Exact registered redirect URI. Present only after client and redirect validation. */
|
|
307
|
+
redirectUri?: string;
|
|
308
|
+
/** Original client state, when supplied. */
|
|
309
|
+
state?: string;
|
|
310
|
+
/** Authorization server issuer for RFC 9207 error responses. */
|
|
311
|
+
issuer?: string;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Expected authorization-request validation failure. Absence of `redirectUri`
|
|
315
|
+
* means a caller MUST render locally and MUST NOT redirect.
|
|
316
|
+
*/
|
|
317
|
+
declare class AuthorizationError extends Error {
|
|
318
|
+
readonly code: AuthorizationErrorCode;
|
|
319
|
+
readonly description: string;
|
|
320
|
+
readonly redirectUri?: string;
|
|
321
|
+
readonly state?: string;
|
|
322
|
+
readonly issuer?: string;
|
|
323
|
+
constructor(code: AuthorizationErrorCode, options: AuthorizationErrorOptions);
|
|
324
|
+
}
|
|
302
325
|
declare function isValidOAuthScopeToken(scopeToken: string): boolean;
|
|
303
326
|
//#endregion
|
|
304
327
|
//#region src/oauth-provider.d.ts
|
|
@@ -662,14 +685,16 @@ interface OAuthProviderOptions<Env = Cloudflare.Env> {
|
|
|
662
685
|
*/
|
|
663
686
|
clientIdMetadataDocumentEnabled?: boolean;
|
|
664
687
|
/**
|
|
665
|
-
* When true, resource validation
|
|
666
|
-
* (scheme + host + port) instead of exact
|
|
667
|
-
*
|
|
668
|
-
* path
|
|
669
|
-
*
|
|
670
|
-
* Explicit `resourceMetadata.resource` configuration always uses exact matching.
|
|
688
|
+
* When true, requested-vs-granted resource validation compares origins only
|
|
689
|
+
* (scheme + host + port) instead of exact URIs. This allows an origin-only
|
|
690
|
+
* grant such as `https://server.com` to accept `https://server.com/mcp`, but
|
|
691
|
+
* also ignores path and query differences. Configured canonical resources
|
|
692
|
+
* always use exact matching.
|
|
671
693
|
*
|
|
672
694
|
* Defaults to false (strict exact matching per RFC 8707).
|
|
695
|
+
*
|
|
696
|
+
* @deprecated This comparison is unsafe for shared-origin multi-path or
|
|
697
|
+
* multi-tenant deployments. Prefer configuring `resourceMetadata.resource`.
|
|
673
698
|
*/
|
|
674
699
|
resourceMatchOriginOnly?: boolean;
|
|
675
700
|
/**
|
|
@@ -683,10 +708,11 @@ interface OAuthProviderOptions<Env = Cloudflare.Env> {
|
|
|
683
708
|
/**
|
|
684
709
|
* The protected resource identifier URL (RFC 9728 `resource` field).
|
|
685
710
|
*
|
|
686
|
-
* Configuring this value pins
|
|
687
|
-
*
|
|
688
|
-
*
|
|
689
|
-
*
|
|
711
|
+
* Configuring this value pins grants and access-token audiences to this
|
|
712
|
+
* exact resource. An omitted authorization resource defaults to this value,
|
|
713
|
+
* and an omitted token-request resource inherits it from the grant. Without
|
|
714
|
+
* configuration, explicit RFC 8707 resource indicators are accepted and
|
|
715
|
+
* omission remains unbound for backwards compatibility.
|
|
690
716
|
*/
|
|
691
717
|
resource?: string;
|
|
692
718
|
/**
|
|
@@ -1511,4 +1537,4 @@ declare function getJwtCryptoAlgorithms(alg: string): {
|
|
|
1511
1537
|
verifyAlgorithm: Parameters<SubtleCrypto['verify']>[0];
|
|
1512
1538
|
};
|
|
1513
1539
|
//#endregion
|
|
1514
|
-
export { AuthRequest, CimdFetchError, ClientInfo, ClientRegistrationCallbackOptions, ClientRegistrationCallbackResult, CompleteAuthorizationOptions, type EmaClaimsMapper, type EmaClaimsMapperInput, type EmaClaimsMapperResult, type EmaIdJagClaims, type EmaOptions, type EmaTrustedIssuer, type EmaTrustedIssuerResolver, type EmaTrustedIssuerResolverInput, type EmaValidationError, ExchangeTokenOptions, ExternalTokenError, ExternalTokenErrorOptions, Grant, GrantSummary, GrantType, ListOptions, ListResult, OAuthError, OAuthErrorOptions, OAuthHelpers, OAuthProvider, OAuthProvider as default, OAuthProviderOptions, OAuthTokenErrorCode, PurgeOptions, PurgeResult, ResolveExternalTokenInput, ResolveExternalTokenResult, Token, TokenBase, TokenExchangeCallbackOptions, TokenExchangeCallbackResult, TokenSummary, base64UrlToBytes, getJwtCryptoAlgorithms, getOAuthApi, isValidOAuthScopeToken, parseJwtJsonPart, resourceMatches, validateResourceUri };
|
|
1540
|
+
export { AuthRequest, AuthorizationError, type AuthorizationErrorCode, type AuthorizationErrorOptions, CimdFetchError, ClientInfo, ClientRegistrationCallbackOptions, ClientRegistrationCallbackResult, CompleteAuthorizationOptions, type EmaClaimsMapper, type EmaClaimsMapperInput, type EmaClaimsMapperResult, type EmaIdJagClaims, type EmaOptions, type EmaTrustedIssuer, type EmaTrustedIssuerResolver, type EmaTrustedIssuerResolverInput, type EmaValidationError, ExchangeTokenOptions, ExternalTokenError, ExternalTokenErrorOptions, Grant, GrantSummary, GrantType, ListOptions, ListResult, OAuthError, OAuthErrorOptions, OAuthHelpers, OAuthProvider, OAuthProvider as default, OAuthProviderOptions, OAuthTokenErrorCode, PurgeOptions, PurgeResult, ResolveExternalTokenInput, ResolveExternalTokenResult, Token, TokenBase, TokenExchangeCallbackOptions, TokenExchangeCallbackResult, TokenSummary, base64UrlToBytes, getJwtCryptoAlgorithms, getOAuthApi, isValidOAuthScopeToken, parseJwtJsonPart, resourceMatches, validateResourceUri };
|
package/dist/oauth-provider.js
CHANGED
|
@@ -2,6 +2,30 @@ import { WorkerEntrypoint } from "cloudflare:workers";
|
|
|
2
2
|
|
|
3
3
|
//#region src/oauth-capabilities.ts
|
|
4
4
|
const OAUTH_SCOPE_TOKEN_PATTERN = /^[\x21\x23-\x5B\x5D-\x7E]+$/;
|
|
5
|
+
/**
|
|
6
|
+
* Expected authorization-request validation failure. Absence of `redirectUri`
|
|
7
|
+
* means a caller MUST render locally and MUST NOT redirect.
|
|
8
|
+
*/
|
|
9
|
+
var AuthorizationError = class extends Error {
|
|
10
|
+
constructor(code, options) {
|
|
11
|
+
super(options.description);
|
|
12
|
+
this.name = "AuthorizationError";
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.description = options.description;
|
|
15
|
+
this.redirectUri = options.redirectUri;
|
|
16
|
+
this.state = options.state;
|
|
17
|
+
this.issuer = options.issuer;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
/** @internal Attach context after exact client redirect validation succeeds. */
|
|
21
|
+
function withAuthorizationRedirect(error, redirectUri, state, issuer) {
|
|
22
|
+
return new AuthorizationError(error.code, {
|
|
23
|
+
description: error.description,
|
|
24
|
+
redirectUri,
|
|
25
|
+
state,
|
|
26
|
+
issuer
|
|
27
|
+
});
|
|
28
|
+
}
|
|
5
29
|
function buildOAuthServerCapabilities(options) {
|
|
6
30
|
return {
|
|
7
31
|
grantTypes: [
|
|
@@ -29,21 +53,40 @@ function validateClientCapabilities(server, client) {
|
|
|
29
53
|
if (client.grantTypes.includes("authorization_code") !== client.responseTypes.includes("code")) throw new Error("grant_types authorization_code and response_types code must be registered together");
|
|
30
54
|
if (client.grantTypes.includes("implicit") !== client.responseTypes.includes("token")) throw new Error("grant_types implicit and response_types token must be registered together");
|
|
31
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Selects the capabilities from a Client ID Metadata Document that this
|
|
58
|
+
* authorization server supports. CIMD documents may advertise extension
|
|
59
|
+
* capabilities alongside the flow used with this server, so unsupported grant
|
|
60
|
+
* and response types are omitted from the effective client metadata instead of
|
|
61
|
+
* invalidating an otherwise usable client.
|
|
62
|
+
*
|
|
63
|
+
* The effective subset is still checked for grant/response consistency, and
|
|
64
|
+
* token endpoint authentication must have a mutually supported method.
|
|
65
|
+
*/
|
|
66
|
+
function negotiateCimdClientCapabilities(server, client) {
|
|
67
|
+
const effective = {
|
|
68
|
+
grantTypes: client.grantTypes.filter((grantType) => server.grantTypes.includes(grantType)),
|
|
69
|
+
responseTypes: client.responseTypes.filter((responseType) => server.responseTypes.includes(responseType)),
|
|
70
|
+
tokenEndpointAuthMethod: client.tokenEndpointAuthMethod
|
|
71
|
+
};
|
|
72
|
+
validateClientCapabilities(server, effective);
|
|
73
|
+
return effective;
|
|
74
|
+
}
|
|
32
75
|
function validateAuthorizationResponseType(server, responseType, clientResponseTypes) {
|
|
33
|
-
if (!responseType) throw new
|
|
34
|
-
if (!server.responseTypes.includes(responseType)) throw new
|
|
35
|
-
if (!(clientResponseTypes ?? ["code"]).includes(responseType)) throw new
|
|
76
|
+
if (!responseType) throw new AuthorizationError("invalid_request", { description: "response_type is required" });
|
|
77
|
+
if (!server.responseTypes.includes(responseType)) throw new AuthorizationError("unsupported_response_type", { description: `The authorization server does not support response_type ${responseType}` });
|
|
78
|
+
if (!(clientResponseTypes ?? ["code"]).includes(responseType)) throw new AuthorizationError("unauthorized_client", { description: `The client is not registered for response_type ${responseType}` });
|
|
36
79
|
}
|
|
37
80
|
/** Parse a PKCE method, applying RFC 7636's default of `plain`. */
|
|
38
81
|
function normalizePkceCodeChallengeMethod(method) {
|
|
39
82
|
const effectiveMethod = method ?? "plain";
|
|
40
|
-
if (effectiveMethod !== "plain" && effectiveMethod !== "S256") throw new
|
|
83
|
+
if (effectiveMethod !== "plain" && effectiveMethod !== "S256") throw new AuthorizationError("invalid_request", { description: `Unsupported PKCE code_challenge_method: ${effectiveMethod}` });
|
|
41
84
|
return effectiveMethod;
|
|
42
85
|
}
|
|
43
86
|
/** Require a syntactically valid PKCE method that the server advertises. */
|
|
44
87
|
function validatePkceCodeChallengeMethod(server, method) {
|
|
45
88
|
const effectiveMethod = normalizePkceCodeChallengeMethod(method);
|
|
46
|
-
if (!server.codeChallengeMethods.includes(effectiveMethod)) throw new
|
|
89
|
+
if (!server.codeChallengeMethods.includes(effectiveMethod)) throw new AuthorizationError("invalid_request", { description: "The plain PKCE method is not allowed. Use S256 instead." });
|
|
47
90
|
return effectiveMethod;
|
|
48
91
|
}
|
|
49
92
|
/** Validate authorization-request PKCE against server and client capabilities. */
|
|
@@ -52,8 +95,8 @@ function validateAuthorizationPkce(server, request, client) {
|
|
|
52
95
|
validatePkceCodeChallengeMethod(server, request.codeChallengeMethod);
|
|
53
96
|
return;
|
|
54
97
|
}
|
|
55
|
-
if (request.codeChallengeMethod) throw new
|
|
56
|
-
if (request.responseType === "code" && client.tokenEndpointAuthMethod === "none") throw new
|
|
98
|
+
if (request.codeChallengeMethod) throw new AuthorizationError("invalid_request", { description: "PKCE code_challenge is required when code_challenge_method is provided." });
|
|
99
|
+
if (request.responseType === "code" && client.tokenEndpointAuthMethod === "none") throw new AuthorizationError("invalid_request", { description: "Public clients must use PKCE with the authorization code flow." });
|
|
57
100
|
}
|
|
58
101
|
function validateAuthorizationServerScopes(scopes) {
|
|
59
102
|
if (!scopes) return;
|
|
@@ -770,6 +813,15 @@ let GrantType = /* @__PURE__ */ function(GrantType$1) {
|
|
|
770
813
|
GrantType$1["JWT_BEARER"] = "urn:ietf:params:oauth:grant-type:jwt-bearer";
|
|
771
814
|
return GrantType$1;
|
|
772
815
|
}({});
|
|
816
|
+
function toPublicClientInfo(client) {
|
|
817
|
+
const { authMethodExplicit: _explicit, ...publicClient } = client;
|
|
818
|
+
return publicClient;
|
|
819
|
+
}
|
|
820
|
+
function isClientAuthMethodAllowed(client, presentedMethod, isClientMetadataDocument) {
|
|
821
|
+
if (presentedMethod === client.tokenEndpointAuthMethod) return true;
|
|
822
|
+
const isSecretMethod = (method) => method === "client_secret_basic" || method === "client_secret_post";
|
|
823
|
+
return !isClientMetadataDocument && client.authMethodExplicit === void 0 && isSecretMethod(client.tokenEndpointAuthMethod) && isSecretMethod(presentedMethod);
|
|
824
|
+
}
|
|
773
825
|
/**
|
|
774
826
|
* OAuth 2.0 Provider implementation for Cloudflare Workers
|
|
775
827
|
* Implements authorization code flow with support for refresh tokens
|
|
@@ -911,7 +963,7 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
911
963
|
/** Validate configured RFC 9728 protected resource metadata. */
|
|
912
964
|
validateResourceMetadataOptions(options) {
|
|
913
965
|
if (!options) return;
|
|
914
|
-
if (options.resource && !validateResourceUri(options.resource)) throw new TypeError("resourceMetadata.resource must be an absolute HTTP(S) URI without a fragment");
|
|
966
|
+
if (options.resource !== void 0 && !validateResourceUri(options.resource)) throw new TypeError("resourceMetadata.resource must be an absolute HTTP(S) URI without a fragment");
|
|
915
967
|
if (options.authorization_servers !== void 0) {
|
|
916
968
|
if (options.authorization_servers.length === 0) throw new TypeError("resourceMetadata.authorization_servers must contain at least one issuer");
|
|
917
969
|
for (const issuer of options.authorization_servers) {
|
|
@@ -1155,7 +1207,16 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
1155
1207
|
}
|
|
1156
1208
|
if (!clientInfo) return this.createInvalidClientResponse("Client not found", basicAuthenticationAttempted);
|
|
1157
1209
|
const presentedAuthMethod = basicAuthenticationAttempted ? "client_secret_basic" : formData.has("client_secret") ? "client_secret_post" : "none";
|
|
1158
|
-
|
|
1210
|
+
const registeredAuthMethod = clientInfo.tokenEndpointAuthMethod;
|
|
1211
|
+
if (!isClientAuthMethodAllowed(clientInfo, presentedAuthMethod, !!this.options.clientIdMetadataDocumentEnabled && this.isClientMetadataUrl(clientInfo.clientId))) return this.createInvalidClientResponse("Client authentication failed", basicAuthenticationAttempted, {
|
|
1212
|
+
category: "client-authentication",
|
|
1213
|
+
reason: "token_endpoint_auth_method_mismatch",
|
|
1214
|
+
detail: {
|
|
1215
|
+
clientId: clientInfo.clientId,
|
|
1216
|
+
registeredMethod: registeredAuthMethod,
|
|
1217
|
+
presentedMethod: presentedAuthMethod
|
|
1218
|
+
}
|
|
1219
|
+
});
|
|
1159
1220
|
if (presentedAuthMethod !== "none") {
|
|
1160
1221
|
if (!clientSecret) return this.createInvalidClientResponse("Client authentication failed: missing client_secret", basicAuthenticationAttempted);
|
|
1161
1222
|
if (!clientInfo.clientSecret) return this.createInvalidClientResponse("Client authentication failed: client has no registered secret", basicAuthenticationAttempted);
|
|
@@ -1647,7 +1708,7 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
1647
1708
|
let tokenScopes = this.downscope(requestedScopes, tokenSummary.scope);
|
|
1648
1709
|
const configuredResource = this.options.resourceMetadata?.resource;
|
|
1649
1710
|
if (configuredResource && !isExactResource(tokenSummary.audience, configuredResource)) throw new OAuthError("invalid_target", { description: "Subject token is not bound to the configured resource" });
|
|
1650
|
-
const newAudience = this.resolveTokenResource(requestedResource, grantData.resource);
|
|
1711
|
+
const newAudience = requestedResource === void 0 ? tokenSummary.audience : this.resolveTokenResource(requestedResource, grantData.resource);
|
|
1651
1712
|
const now = Math.floor(Date.now() / 1e3);
|
|
1652
1713
|
const subjectTokenRemainingLifetime = tokenSummary.expiresAt - now;
|
|
1653
1714
|
if (subjectTokenRemainingLifetime < KV_MIN_EXPIRATION_TTL_SECONDS) throw new OAuthError("invalid_grant", { description: "Subject token is too close to expiry to exchange" });
|
|
@@ -2054,10 +2115,12 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
2054
2115
|
statusCode: 400
|
|
2055
2116
|
});
|
|
2056
2117
|
}
|
|
2118
|
+
let authMethodWasExplicit;
|
|
2057
2119
|
let authMethod;
|
|
2058
2120
|
let grantTypes;
|
|
2059
2121
|
let responseTypes;
|
|
2060
2122
|
try {
|
|
2123
|
+
authMethodWasExplicit = clientMetadata.token_endpoint_auth_method !== void 0;
|
|
2061
2124
|
authMethod = OAuthProviderImpl.validateStringField(clientMetadata.token_endpoint_auth_method) || "client_secret_basic";
|
|
2062
2125
|
grantTypes = OAuthProviderImpl.validateStringArray(clientMetadata.grant_types, "grant_types") || [GrantType.AUTHORIZATION_CODE];
|
|
2063
2126
|
responseTypes = OAuthProviderImpl.validateStringArray(clientMetadata.response_types, "response_types") || ["code"];
|
|
@@ -2097,7 +2160,8 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
2097
2160
|
grantTypes,
|
|
2098
2161
|
responseTypes,
|
|
2099
2162
|
registrationDate: Math.floor(Date.now() / 1e3),
|
|
2100
|
-
tokenEndpointAuthMethod: authMethod
|
|
2163
|
+
tokenEndpointAuthMethod: authMethod,
|
|
2164
|
+
...authMethodWasExplicit ? { authMethodExplicit: true } : {}
|
|
2101
2165
|
};
|
|
2102
2166
|
if (!isPublicClient && hashedSecret) clientInfo.clientSecret = hashedSecret;
|
|
2103
2167
|
} catch (error) {
|
|
@@ -2329,19 +2393,26 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
2329
2393
|
}
|
|
2330
2394
|
/**
|
|
2331
2395
|
* Resolves an access-token audience from a token request and its authorization grant.
|
|
2332
|
-
*
|
|
2333
|
-
* configuration, RFC 8707 downscoping is allowed
|
|
2396
|
+
* A configured canonical resource is inherited when omitted but cannot be overridden.
|
|
2397
|
+
* Without configuration, RFC 8707 downscoping is allowed, omission inherits a
|
|
2398
|
+
* bound grant, and a legacy unbound grant retains the v0.8.2 behavior.
|
|
2334
2399
|
*/
|
|
2335
2400
|
resolveTokenResource(requestedResource, grantedResource) {
|
|
2401
|
+
const resourceWasProvided = requestedResource !== void 0;
|
|
2336
2402
|
const requestedAudience = parseResourceParameter(requestedResource);
|
|
2337
|
-
if (
|
|
2403
|
+
if (resourceWasProvided && !requestedAudience) throw new OAuthError("invalid_target", { description: "The resource parameter must be a valid absolute URI without a fragment" });
|
|
2404
|
+
const grantResourceWasStored = grantedResource !== void 0;
|
|
2338
2405
|
const grantedAudience = parseResourceParameter(grantedResource);
|
|
2339
|
-
if (
|
|
2406
|
+
if (grantResourceWasStored && !grantedAudience) throw new OAuthError("invalid_target", { description: "The authorization grant contains an invalid resource" });
|
|
2340
2407
|
const configuredResource = this.options.resourceMetadata?.resource;
|
|
2341
|
-
if (configuredResource
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2408
|
+
if (configuredResource) {
|
|
2409
|
+
if (resourceWasProvided && !isExactResource(requestedResource, configuredResource)) throw new OAuthError("invalid_target", { description: `The resource parameter must exactly match ${configuredResource}` });
|
|
2410
|
+
if (isExactResource(grantedResource, configuredResource)) return configuredResource;
|
|
2411
|
+
if (!grantResourceWasStored) return configuredResource;
|
|
2412
|
+
throw new OAuthError("invalid_target", { description: "The authorization grant is not bound to the configured resource" });
|
|
2413
|
+
}
|
|
2414
|
+
const originOnly = !!this.options.resourceMatchOriginOnly;
|
|
2415
|
+
if (resourceWasProvided && grantResourceWasStored) {
|
|
2345
2416
|
const requestedResources = Array.isArray(requestedResource) ? requestedResource : [requestedResource];
|
|
2346
2417
|
const grantedResources = Array.isArray(grantedResource) ? grantedResource : [grantedResource];
|
|
2347
2418
|
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" });
|
|
@@ -2546,13 +2617,13 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
2546
2617
|
if (!clientName?.trim()) throw new Error("client_name is required and must not be empty");
|
|
2547
2618
|
if (!redirectUris || redirectUris.length === 0) throw new Error("redirect_uris is required and must not be empty");
|
|
2548
2619
|
if (declaredAuthMethod && !OAuthProviderImpl.CIMD_ALLOWED_AUTH_METHODS.includes(declaredAuthMethod) || authMethodChoices && !authMethodChoices.some((method) => OAuthProviderImpl.CIMD_ALLOWED_AUTH_METHODS.includes(method))) throw new Error(`CIMD client does not support an accepted token endpoint authentication method. Supported methods: ${OAuthProviderImpl.CIMD_ALLOWED_AUTH_METHODS.join(", ")}`);
|
|
2549
|
-
const
|
|
2550
|
-
const
|
|
2620
|
+
const advertisedGrantTypes = OAuthProviderImpl.validateStringArray(rawMetadata.grant_types, "grant_types") || [GrantType.AUTHORIZATION_CODE];
|
|
2621
|
+
const advertisedResponseTypes = OAuthProviderImpl.validateStringArray(rawMetadata.response_types, "response_types") || ["code"];
|
|
2551
2622
|
const effectiveAuthMethod = tokenEndpointAuthMethod || "none";
|
|
2552
|
-
|
|
2623
|
+
const { grantTypes, responseTypes } = negotiateCimdClientCapabilities(this.serverCapabilities, {
|
|
2553
2624
|
tokenEndpointAuthMethod: effectiveAuthMethod,
|
|
2554
|
-
grantTypes,
|
|
2555
|
-
responseTypes
|
|
2625
|
+
grantTypes: advertisedGrantTypes,
|
|
2626
|
+
responseTypes: advertisedResponseTypes
|
|
2556
2627
|
});
|
|
2557
2628
|
return {
|
|
2558
2629
|
clientId,
|
|
@@ -2861,6 +2932,7 @@ function audienceMatches(resourceServerUrl, audienceValue) {
|
|
|
2861
2932
|
function parseResourceParameter(value) {
|
|
2862
2933
|
if (!value) return;
|
|
2863
2934
|
const uris = Array.isArray(value) ? value : [value];
|
|
2935
|
+
if (uris.length === 0) return;
|
|
2864
2936
|
for (const uri of uris) if (typeof uri !== "string" || !validateResourceUri(uri)) return;
|
|
2865
2937
|
return value;
|
|
2866
2938
|
}
|
|
@@ -3207,7 +3279,7 @@ var OAuthHelpersImpl = class {
|
|
|
3207
3279
|
* Parses an OAuth authorization request from the HTTP request
|
|
3208
3280
|
* @param request - The HTTP request containing OAuth parameters
|
|
3209
3281
|
* @returns The parsed authorization request parameters
|
|
3210
|
-
* @throws
|
|
3282
|
+
* @throws AuthorizationError for expected authorization-request validation failures
|
|
3211
3283
|
* @throws CimdFetchError when the client ID is a CIMD URL whose document cannot be resolved
|
|
3212
3284
|
*/
|
|
3213
3285
|
async parseAuthRequest(request) {
|
|
@@ -3222,22 +3294,36 @@ var OAuthHelpersImpl = class {
|
|
|
3222
3294
|
const issuer = this.provider.getAuthorizationServerIssuer(url);
|
|
3223
3295
|
const resourceParams = url.searchParams.getAll("resource");
|
|
3224
3296
|
const resourceParam = resourceParams.length > 0 ? resourceParams.length === 1 ? resourceParams[0] : resourceParams : void 0;
|
|
3225
|
-
|
|
3297
|
+
if (!clientId) throw new AuthorizationError("invalid_request", { description: "client_id is required" });
|
|
3298
|
+
const clientInfo = await this.lookupClient(clientId);
|
|
3299
|
+
if (!clientInfo) throw new AuthorizationError("invalid_request", { description: "Invalid client_id" });
|
|
3300
|
+
try {
|
|
3301
|
+
validateRedirectUriScheme(redirectUri);
|
|
3302
|
+
} catch {
|
|
3303
|
+
throw new AuthorizationError("invalid_request", { description: "Invalid redirect URI" });
|
|
3304
|
+
}
|
|
3305
|
+
if (!redirectUri || !isValidRedirectUri(redirectUri, clientInfo.redirectUris)) throw new AuthorizationError("invalid_request", { description: "Invalid redirect URI" });
|
|
3306
|
+
const withRedirect = (error) => {
|
|
3307
|
+
throw withAuthorizationRedirect(error, redirectUri, state || void 0, issuer);
|
|
3308
|
+
};
|
|
3309
|
+
const resourceWasProvided = resourceParam !== void 0;
|
|
3226
3310
|
let resource = parseResourceParameter(resourceParam);
|
|
3227
|
-
if (
|
|
3311
|
+
if (resourceWasProvided && !resource) withRedirect(new AuthorizationError("invalid_target", { description: "The resource parameter must be a valid absolute URI without a fragment" }));
|
|
3228
3312
|
const configuredResource = this.provider.options.resourceMetadata?.resource;
|
|
3229
|
-
if (configuredResource
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
if (!redirectUri || !isValidRedirectUri(redirectUri, clientInfo.redirectUris)) throw new Error(`Invalid redirect URI. The redirect URI provided does not match any registered URI for this client.`);
|
|
3313
|
+
if (configuredResource) {
|
|
3314
|
+
if (resourceWasProvided && !isExactResource(resource, configuredResource)) withRedirect(new AuthorizationError("invalid_target", { description: `The resource parameter must exactly match ${configuredResource}` }));
|
|
3315
|
+
resource = configuredResource;
|
|
3316
|
+
}
|
|
3317
|
+
try {
|
|
3235
3318
|
validateAuthorizationResponseType(this.provider.serverCapabilities, responseType, clientInfo.responseTypes);
|
|
3236
3319
|
validateAuthorizationPkce(this.provider.serverCapabilities, {
|
|
3237
3320
|
responseType,
|
|
3238
3321
|
codeChallenge,
|
|
3239
3322
|
codeChallengeMethod
|
|
3240
3323
|
}, clientInfo);
|
|
3324
|
+
} catch (error) {
|
|
3325
|
+
if (error instanceof AuthorizationError) withRedirect(error);
|
|
3326
|
+
throw error;
|
|
3241
3327
|
}
|
|
3242
3328
|
return {
|
|
3243
3329
|
responseType,
|
|
@@ -3263,7 +3349,8 @@ var OAuthHelpersImpl = class {
|
|
|
3263
3349
|
* validating the metadata document fails.
|
|
3264
3350
|
*/
|
|
3265
3351
|
async lookupClient(clientId) {
|
|
3266
|
-
|
|
3352
|
+
const client = await this.provider.getClient(this.env, clientId);
|
|
3353
|
+
return client ? toPublicClientInfo(client) : null;
|
|
3267
3354
|
}
|
|
3268
3355
|
/**
|
|
3269
3356
|
* Completes an authorization request by creating a grant and either:
|
|
@@ -3281,8 +3368,11 @@ var OAuthHelpersImpl = class {
|
|
|
3281
3368
|
if (!clientInfo || !isValidRedirectUri(redirectUri, clientInfo.redirectUris)) throw new Error("Invalid redirect URI. The redirect URI provided does not match any registered URI for this client.");
|
|
3282
3369
|
validateAuthorizationResponseType(this.provider.serverCapabilities, options.request.responseType, clientInfo.responseTypes);
|
|
3283
3370
|
const configuredResource = this.provider.options.resourceMetadata?.resource;
|
|
3284
|
-
|
|
3285
|
-
const
|
|
3371
|
+
const resourceWasProvided = options.request.resource !== void 0;
|
|
3372
|
+
const parsedResource = parseResourceParameter(options.request.resource);
|
|
3373
|
+
if (resourceWasProvided && !parsedResource) throw new AuthorizationError("invalid_target", { description: "The resource parameter must be a valid absolute URI without a fragment" });
|
|
3374
|
+
if (configuredResource && resourceWasProvided && !isExactResource(parsedResource, configuredResource)) throw new AuthorizationError("invalid_target", { description: `The resource parameter must exactly match ${configuredResource}` });
|
|
3375
|
+
const effectiveResource = configuredResource ?? parsedResource;
|
|
3286
3376
|
validateAuthorizationPkce(this.provider.serverCapabilities, options.request, clientInfo);
|
|
3287
3377
|
let grantsToRevoke = [];
|
|
3288
3378
|
if (options.revokeExistingGrants !== false) {
|
|
@@ -3388,6 +3478,7 @@ var OAuthHelpersImpl = class {
|
|
|
3388
3478
|
*/
|
|
3389
3479
|
async createClient(clientInfo) {
|
|
3390
3480
|
const clientId = generateRandomString(16);
|
|
3481
|
+
const authMethodWasExplicit = clientInfo.tokenEndpointAuthMethod !== void 0;
|
|
3391
3482
|
const tokenEndpointAuthMethod = clientInfo.tokenEndpointAuthMethod || "client_secret_basic";
|
|
3392
3483
|
const isPublicClient = tokenEndpointAuthMethod === "none";
|
|
3393
3484
|
const newClient = {
|
|
@@ -3408,7 +3499,8 @@ var OAuthHelpersImpl = class {
|
|
|
3408
3499
|
],
|
|
3409
3500
|
responseTypes: clientInfo.responseTypes || ["code"],
|
|
3410
3501
|
registrationDate: Math.floor(Date.now() / 1e3),
|
|
3411
|
-
tokenEndpointAuthMethod
|
|
3502
|
+
tokenEndpointAuthMethod,
|
|
3503
|
+
...authMethodWasExplicit ? { authMethodExplicit: true } : {}
|
|
3412
3504
|
};
|
|
3413
3505
|
for (const uri of newClient.redirectUris) validateRedirectUriScheme(uri);
|
|
3414
3506
|
let clientSecret;
|
|
@@ -3417,7 +3509,7 @@ var OAuthHelpersImpl = class {
|
|
|
3417
3509
|
newClient.clientSecret = await hashSecret(clientSecret);
|
|
3418
3510
|
}
|
|
3419
3511
|
await this.env.OAUTH_KV.put(`client:${clientId}`, JSON.stringify(newClient));
|
|
3420
|
-
const clientResponse =
|
|
3512
|
+
const clientResponse = toPublicClientInfo(newClient);
|
|
3421
3513
|
if (!isPublicClient && clientSecret) clientResponse.clientSecret = clientSecret;
|
|
3422
3514
|
return clientResponse;
|
|
3423
3515
|
}
|
|
@@ -3435,7 +3527,7 @@ var OAuthHelpersImpl = class {
|
|
|
3435
3527
|
const promises = response.keys.map(async (key) => {
|
|
3436
3528
|
const clientId = key.name.substring(7);
|
|
3437
3529
|
const client = await this.provider.getClient(this.env, clientId);
|
|
3438
|
-
if (client) clients.push(client);
|
|
3530
|
+
if (client) clients.push(toPublicClientInfo(client));
|
|
3439
3531
|
});
|
|
3440
3532
|
await Promise.all(promises);
|
|
3441
3533
|
return {
|
|
@@ -3452,7 +3544,8 @@ var OAuthHelpersImpl = class {
|
|
|
3452
3544
|
async updateClient(clientId, updates) {
|
|
3453
3545
|
const client = await this.provider.getClient(this.env, clientId);
|
|
3454
3546
|
if (!client) return null;
|
|
3455
|
-
|
|
3547
|
+
const authMethodWasExplicit = updates.tokenEndpointAuthMethod !== void 0;
|
|
3548
|
+
const authMethod = updates.tokenEndpointAuthMethod || client.tokenEndpointAuthMethod || "client_secret_basic";
|
|
3456
3549
|
const isPublicClient = authMethod === "none";
|
|
3457
3550
|
let secretToStore = client.clientSecret;
|
|
3458
3551
|
let originalSecret = void 0;
|
|
@@ -3465,14 +3558,15 @@ var OAuthHelpersImpl = class {
|
|
|
3465
3558
|
...client,
|
|
3466
3559
|
...updates,
|
|
3467
3560
|
clientId: client.clientId,
|
|
3468
|
-
tokenEndpointAuthMethod: authMethod
|
|
3561
|
+
tokenEndpointAuthMethod: authMethod,
|
|
3562
|
+
authMethodExplicit: authMethodWasExplicit ? true : client.authMethodExplicit
|
|
3469
3563
|
};
|
|
3470
3564
|
if (!isPublicClient && secretToStore) updatedClient.clientSecret = secretToStore;
|
|
3471
3565
|
else delete updatedClient.clientSecret;
|
|
3472
3566
|
const clientKvOptions = {};
|
|
3473
3567
|
if (this.provider.options.clientRegistrationTTL !== void 0) clientKvOptions.expirationTtl = this.provider.options.clientRegistrationTTL;
|
|
3474
3568
|
await this.env.OAUTH_KV.put(`client:${clientId}`, JSON.stringify(updatedClient), clientKvOptions);
|
|
3475
|
-
const response =
|
|
3569
|
+
const response = toPublicClientInfo(updatedClient);
|
|
3476
3570
|
if (!isPublicClient && originalSecret) response.clientSecret = originalSecret;
|
|
3477
3571
|
return response;
|
|
3478
3572
|
}
|
|
@@ -3670,4 +3764,4 @@ var OAuthHelpersImpl = class {
|
|
|
3670
3764
|
var oauth_provider_default = OAuthProvider;
|
|
3671
3765
|
|
|
3672
3766
|
//#endregion
|
|
3673
|
-
export { CimdFetchError, ExternalTokenError, GrantType, OAuthError, OAuthProvider, base64UrlToBytes, oauth_provider_default as default, getJwtCryptoAlgorithms, getOAuthApi, isValidOAuthScopeToken, parseJwtJsonPart, resourceMatches, validateResourceUri };
|
|
3767
|
+
export { AuthorizationError, CimdFetchError, ExternalTokenError, GrantType, OAuthError, OAuthProvider, base64UrlToBytes, oauth_provider_default as default, getJwtCryptoAlgorithms, getOAuthApi, isValidOAuthScopeToken, parseJwtJsonPart, resourceMatches, validateResourceUri };
|