@cloudflare/workers-oauth-provider 0.2.3 → 0.2.4
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 +18 -0
- package/dist/oauth-provider.d.ts +53 -10
- package/dist/oauth-provider.js +80 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -81,6 +81,12 @@ export default new OAuthProvider({
|
|
|
81
81
|
// Defaults to false.
|
|
82
82
|
allowImplicitFlow: false,
|
|
83
83
|
|
|
84
|
+
// Optional: Controls whether the plain PKCE code_challenge_method is allowed.
|
|
85
|
+
// OAuth 2.1 recommends using S256 exclusively as plain offers no cryptographic protection.
|
|
86
|
+
// When false, only S256 is accepted and advertised in the metadata endpoint.
|
|
87
|
+
// Defaults to true for backward compatibility.
|
|
88
|
+
allowPlainPKCE: true,
|
|
89
|
+
|
|
84
90
|
// Optional: Controls whether public clients (clients without a secret, like SPAs)
|
|
85
91
|
// can register via the dynamic client registration endpoint.
|
|
86
92
|
// When true, only confidential clients can register.
|
|
@@ -304,6 +310,18 @@ new OAuthProvider({
|
|
|
304
310
|
|
|
305
311
|
By default, the `onError` callback is set to ``({ status, code, description }) => console.warn(`OAuth error response: ${status} ${code} - ${description}`)``.
|
|
306
312
|
|
|
313
|
+
## Standards Compliance
|
|
314
|
+
|
|
315
|
+
This library implements the following OAuth and MCP specifications:
|
|
316
|
+
|
|
317
|
+
- [OAuth 2.1 (draft-ietf-oauth-v2-1-13)](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13) — Core authorization framework with PKCE
|
|
318
|
+
- [OAuth 2.0 Authorization Server Metadata (RFC 8414)](https://datatracker.ietf.org/doc/html/rfc8414) — `/.well-known/oauth-authorization-server` discovery endpoint
|
|
319
|
+
- [OAuth 2.0 Protected Resource Metadata (RFC 9728)](https://datatracker.ietf.org/doc/html/rfc9728) — `/.well-known/oauth-protected-resource` discovery endpoint
|
|
320
|
+
- [OAuth 2.0 Dynamic Client Registration (RFC 7591)](https://datatracker.ietf.org/doc/html/rfc7591) — Dynamic client registration endpoint
|
|
321
|
+
- [OAuth Client ID Metadata Documents (draft-ietf-oauth-client-id-metadata-document)](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-client-id-metadata-document) — HTTPS URLs as client IDs
|
|
322
|
+
|
|
323
|
+
These are the specifications required by the [MCP authorization specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization).
|
|
324
|
+
|
|
307
325
|
## Implementation Notes
|
|
308
326
|
|
|
309
327
|
### End-to-end encryption
|
package/dist/oauth-provider.d.ts
CHANGED
|
@@ -13,8 +13,10 @@ declare enum GrantType {
|
|
|
13
13
|
/**
|
|
14
14
|
* Aliases for either type of Handler that makes .fetch required
|
|
15
15
|
*/
|
|
16
|
-
type ExportedHandlerWithFetch = ExportedHandler & Pick<Required<ExportedHandler
|
|
17
|
-
type WorkerEntrypointWithFetch = WorkerEntrypoint &
|
|
16
|
+
type ExportedHandlerWithFetch<Env = Cloudflare.Env> = ExportedHandler<Env> & Pick<Required<ExportedHandler<Env>>, 'fetch'>;
|
|
17
|
+
type WorkerEntrypointWithFetch<Env = Cloudflare.Env> = WorkerEntrypoint<Env> & {
|
|
18
|
+
fetch: NonNullable<WorkerEntrypoint['fetch']>;
|
|
19
|
+
};
|
|
18
20
|
/**
|
|
19
21
|
* Configuration options for the OAuth Provider
|
|
20
22
|
*/
|
|
@@ -119,7 +121,7 @@ interface ResolveExternalTokenResult {
|
|
|
119
121
|
*/
|
|
120
122
|
audience?: string | string[];
|
|
121
123
|
}
|
|
122
|
-
interface OAuthProviderOptions {
|
|
124
|
+
interface OAuthProviderOptions<Env = Cloudflare.Env> {
|
|
123
125
|
/**
|
|
124
126
|
* URL(s) for API routes. Requests with URLs starting with any of these prefixes
|
|
125
127
|
* will be treated as API requests and require a valid access token.
|
|
@@ -137,7 +139,7 @@ interface OAuthProviderOptions {
|
|
|
137
139
|
* Used with `apiRoute` for the single-handler configuration. This is incompatible with
|
|
138
140
|
* the `apiHandlers` property. You must use either `apiRoute` + `apiHandler` OR `apiHandlers`, not both.
|
|
139
141
|
*/
|
|
140
|
-
apiHandler?: ExportedHandlerWithFetch | (new (ctx: ExecutionContext, env:
|
|
142
|
+
apiHandler?: ExportedHandlerWithFetch<Env> | (new (ctx: ExecutionContext, env: Env) => WorkerEntrypointWithFetch<Env>);
|
|
141
143
|
/**
|
|
142
144
|
* Map of API routes to their corresponding handlers for the multi-handler configuration.
|
|
143
145
|
* The keys are the API routes (strings only, not arrays), and the values are the handlers.
|
|
@@ -148,12 +150,12 @@ interface OAuthProviderOptions {
|
|
|
148
150
|
* `apiRoute` + `apiHandler` (single-handler configuration) OR `apiHandlers` (multi-handler
|
|
149
151
|
* configuration), not both.
|
|
150
152
|
*/
|
|
151
|
-
apiHandlers?: Record<string, ExportedHandlerWithFetch | (new (ctx: ExecutionContext, env:
|
|
153
|
+
apiHandlers?: Record<string, ExportedHandlerWithFetch<Env> | (new (ctx: ExecutionContext, env: Env) => WorkerEntrypointWithFetch<Env>)>;
|
|
152
154
|
/**
|
|
153
155
|
* Handler for all non-API requests or API requests without a valid token.
|
|
154
156
|
* Can be either an ExportedHandler object with a fetch method or a class extending WorkerEntrypoint.
|
|
155
157
|
*/
|
|
156
|
-
defaultHandler: ExportedHandler | (new (ctx: ExecutionContext, env:
|
|
158
|
+
defaultHandler: ExportedHandler<Env> | (new (ctx: ExecutionContext, env: Env) => WorkerEntrypointWithFetch<Env>);
|
|
157
159
|
/**
|
|
158
160
|
* URL of the OAuth authorization endpoint where users can grant permissions.
|
|
159
161
|
* This URL is used in OAuth metadata and is not handled by the provider itself.
|
|
@@ -191,6 +193,13 @@ interface OAuthProviderOptions {
|
|
|
191
193
|
* Defaults to false.
|
|
192
194
|
*/
|
|
193
195
|
allowImplicitFlow?: boolean;
|
|
196
|
+
/**
|
|
197
|
+
* Controls whether the plain PKCE method is allowed.
|
|
198
|
+
* OAuth 2.1 recommends using S256 exclusively as plain offers no cryptographic protection.
|
|
199
|
+
* When set to false, only the S256 code_challenge_method will be accepted.
|
|
200
|
+
* Defaults to true for backward compatibility.
|
|
201
|
+
*/
|
|
202
|
+
allowPlainPKCE?: boolean;
|
|
194
203
|
/**
|
|
195
204
|
* Controls whether OAuth 2.0 Token Exchange (RFC 8693) is allowed.
|
|
196
205
|
* When false, the token exchange grant type will not be advertised in metadata
|
|
@@ -237,6 +246,40 @@ interface OAuthProviderOptions {
|
|
|
237
246
|
status: number;
|
|
238
247
|
headers: Record<string, string>;
|
|
239
248
|
}) => Response | void;
|
|
249
|
+
/**
|
|
250
|
+
* Optional metadata for RFC 9728 OAuth 2.0 Protected Resource Metadata.
|
|
251
|
+
* Controls the response served at /.well-known/oauth-protected-resource.
|
|
252
|
+
*
|
|
253
|
+
* If not provided, the endpoint will be automatically generated using the request origin
|
|
254
|
+
* as the resource identifier, and the token endpoint's origin as the authorization server.
|
|
255
|
+
*/
|
|
256
|
+
resourceMetadata?: {
|
|
257
|
+
/**
|
|
258
|
+
* The protected resource identifier URL (RFC 9728 `resource` field).
|
|
259
|
+
* If not set, defaults to the request URL's origin.
|
|
260
|
+
*/
|
|
261
|
+
resource?: string;
|
|
262
|
+
/**
|
|
263
|
+
* List of authorization server issuer URLs that can issue tokens for this resource.
|
|
264
|
+
* If not set, defaults to the token endpoint's origin (consistent with the issuer
|
|
265
|
+
* in authorization server metadata).
|
|
266
|
+
*/
|
|
267
|
+
authorization_servers?: string[];
|
|
268
|
+
/**
|
|
269
|
+
* Scopes supported by this protected resource.
|
|
270
|
+
* If not set, falls back to the top-level scopesSupported option.
|
|
271
|
+
*/
|
|
272
|
+
scopes_supported?: string[];
|
|
273
|
+
/**
|
|
274
|
+
* Methods by which bearer tokens can be presented to this resource.
|
|
275
|
+
* Defaults to ["header"].
|
|
276
|
+
*/
|
|
277
|
+
bearer_methods_supported?: string[];
|
|
278
|
+
/**
|
|
279
|
+
* Human-readable name for this resource.
|
|
280
|
+
*/
|
|
281
|
+
resource_name?: string;
|
|
282
|
+
};
|
|
240
283
|
}
|
|
241
284
|
/**
|
|
242
285
|
* Helper methods for OAuth operations provided to handler functions
|
|
@@ -714,13 +757,13 @@ interface GrantSummary {
|
|
|
714
757
|
* Implements authorization code flow with support for refresh tokens
|
|
715
758
|
* and dynamic client registration.
|
|
716
759
|
*/
|
|
717
|
-
declare class OAuthProvider {
|
|
760
|
+
declare class OAuthProvider<Env = Cloudflare.Env> {
|
|
718
761
|
#private;
|
|
719
762
|
/**
|
|
720
763
|
* Creates a new OAuth provider instance
|
|
721
764
|
* @param options - Configuration options for the provider
|
|
722
765
|
*/
|
|
723
|
-
constructor(options: OAuthProviderOptions);
|
|
766
|
+
constructor(options: OAuthProviderOptions<Env>);
|
|
724
767
|
/**
|
|
725
768
|
* Main fetch handler for the Worker
|
|
726
769
|
* Routes requests to the appropriate handler based on the URL
|
|
@@ -729,7 +772,7 @@ declare class OAuthProvider {
|
|
|
729
772
|
* @param ctx - Cloudflare Worker execution context
|
|
730
773
|
* @returns A Promise resolving to an HTTP Response
|
|
731
774
|
*/
|
|
732
|
-
fetch(request: Request, env:
|
|
775
|
+
fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response>;
|
|
733
776
|
}
|
|
734
777
|
/**
|
|
735
778
|
* Gets OAuthHelpers for the given environment
|
|
@@ -737,6 +780,6 @@ declare class OAuthProvider {
|
|
|
737
780
|
* @param env - Cloudflare Worker environment variables
|
|
738
781
|
* @returns An instance of OAuthHelpers
|
|
739
782
|
*/
|
|
740
|
-
declare function getOAuthApi(options: OAuthProviderOptions
|
|
783
|
+
declare function getOAuthApi<Env = Cloudflare.Env>(options: OAuthProviderOptions<Env>, env: Env): OAuthHelpers;
|
|
741
784
|
//#endregion
|
|
742
785
|
export { AuthRequest, ClientInfo, CompleteAuthorizationOptions, ExchangeTokenOptions, Grant, GrantSummary, GrantType, ListOptions, ListResult, OAuthHelpers, OAuthProvider, OAuthProvider as default, OAuthProviderOptions, ResolveExternalTokenInput, ResolveExternalTokenResult, Token, TokenBase, TokenExchangeCallbackOptions, TokenExchangeCallbackResult, TokenSummary, getOAuthApi };
|
package/dist/oauth-provider.js
CHANGED
|
@@ -141,7 +141,7 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
141
141
|
async fetch(request, env, ctx) {
|
|
142
142
|
const url = new URL(request.url);
|
|
143
143
|
if (request.method === "OPTIONS") {
|
|
144
|
-
if (this.isApiRequest(url) || url.pathname === "/.well-known/oauth-authorization-server" || this.isTokenEndpoint(url) || this.options.clientRegistrationEndpoint && this.isClientRegistrationEndpoint(url)) return this.addCorsHeaders(new Response(null, {
|
|
144
|
+
if (this.isApiRequest(url) || url.pathname === "/.well-known/oauth-authorization-server" || url.pathname === "/.well-known/oauth-protected-resource" || this.isTokenEndpoint(url) || this.options.clientRegistrationEndpoint && this.isClientRegistrationEndpoint(url)) return this.addCorsHeaders(new Response(null, {
|
|
145
145
|
status: 204,
|
|
146
146
|
headers: { "Content-Length": "0" }
|
|
147
147
|
}), request);
|
|
@@ -150,6 +150,10 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
150
150
|
const response = await this.handleMetadataDiscovery(url);
|
|
151
151
|
return this.addCorsHeaders(response, request);
|
|
152
152
|
}
|
|
153
|
+
if (url.pathname === "/.well-known/oauth-protected-resource") {
|
|
154
|
+
const response = this.handleProtectedResourceMetadata(url);
|
|
155
|
+
return this.addCorsHeaders(response, request);
|
|
156
|
+
}
|
|
153
157
|
if (this.isTokenEndpoint(url)) {
|
|
154
158
|
const parsed = await this.parseTokenEndpointRequest(request, env);
|
|
155
159
|
if (parsed instanceof Response) return this.addCorsHeaders(parsed, request);
|
|
@@ -287,8 +291,10 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
287
291
|
* @returns True if the URL matches the API route
|
|
288
292
|
*/
|
|
289
293
|
matchApiRoute(url, route) {
|
|
290
|
-
if (this.isPath(route))
|
|
291
|
-
|
|
294
|
+
if (this.isPath(route)) {
|
|
295
|
+
if (route === "/") return url.pathname === "/";
|
|
296
|
+
return url.pathname.startsWith(route);
|
|
297
|
+
} else {
|
|
292
298
|
const apiUrl = new URL(route);
|
|
293
299
|
return url.hostname === apiUrl.hostname && url.pathname.startsWith(apiUrl.pathname);
|
|
294
300
|
}
|
|
@@ -367,12 +373,31 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
367
373
|
"none"
|
|
368
374
|
],
|
|
369
375
|
revocation_endpoint: tokenEndpoint,
|
|
370
|
-
code_challenge_methods_supported: ["plain", "S256"],
|
|
376
|
+
code_challenge_methods_supported: this.options.allowPlainPKCE !== false ? ["plain", "S256"] : ["S256"],
|
|
371
377
|
client_id_metadata_document_supported: this.hasGlobalFetchStrictlyPublic()
|
|
372
378
|
};
|
|
373
379
|
return new Response(JSON.stringify(metadata), { headers: { "Content-Type": "application/json" } });
|
|
374
380
|
}
|
|
375
381
|
/**
|
|
382
|
+
* Handles the OAuth Protected Resource Metadata endpoint
|
|
383
|
+
* Implements RFC 9728 for OAuth Protected Resource Metadata
|
|
384
|
+
* @param requestUrl - The URL of the incoming request
|
|
385
|
+
* @returns Response with protected resource metadata
|
|
386
|
+
*/
|
|
387
|
+
handleProtectedResourceMetadata(requestUrl) {
|
|
388
|
+
const rm = this.options.resourceMetadata;
|
|
389
|
+
const tokenEndpointUrl = this.getFullEndpointUrl(this.options.tokenEndpoint, requestUrl);
|
|
390
|
+
const authServerOrigin = new URL(tokenEndpointUrl).origin;
|
|
391
|
+
const metadata = {
|
|
392
|
+
resource: rm?.resource ?? requestUrl.origin,
|
|
393
|
+
authorization_servers: rm?.authorization_servers ?? [authServerOrigin],
|
|
394
|
+
scopes_supported: rm?.scopes_supported ?? this.options.scopesSupported,
|
|
395
|
+
bearer_methods_supported: rm?.bearer_methods_supported ?? ["header"]
|
|
396
|
+
};
|
|
397
|
+
if (rm?.resource_name) metadata.resource_name = rm.resource_name;
|
|
398
|
+
return new Response(JSON.stringify(metadata), { headers: { "Content-Type": "application/json" } });
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
376
401
|
* Handles client authentication and token issuance via the token endpoint
|
|
377
402
|
* Supports authorization_code and refresh_token grant types
|
|
378
403
|
* @param body - The parsed request body
|
|
@@ -411,7 +436,7 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
411
436
|
if (grantData.clientId !== clientInfo.clientId) return this.createErrorResponse("invalid_grant", "Client ID mismatch");
|
|
412
437
|
const isPkceEnabled = !!grantData.codeChallenge;
|
|
413
438
|
if (!redirectUri && !isPkceEnabled) return this.createErrorResponse("invalid_request", "redirect_uri is required when not using PKCE");
|
|
414
|
-
if (redirectUri && !clientInfo.redirectUris
|
|
439
|
+
if (redirectUri && !isValidRedirectUri(redirectUri, clientInfo.redirectUris)) return this.createErrorResponse("invalid_grant", "Invalid redirect URI");
|
|
415
440
|
if (!isPkceEnabled && codeVerifier) return this.createErrorResponse("invalid_request", "code_verifier provided for a flow that did not use PKCE");
|
|
416
441
|
if (isPkceEnabled) {
|
|
417
442
|
if (!codeVerifier) return this.createErrorResponse("invalid_request", "code_verifier is required for PKCE");
|
|
@@ -921,8 +946,10 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
921
946
|
* @returns Response from the API handler or error
|
|
922
947
|
*/
|
|
923
948
|
async handleApiRequest(request, env, ctx) {
|
|
949
|
+
const url = new URL(request.url);
|
|
950
|
+
const resourceMetadataUrl = `${url.origin}/.well-known/oauth-protected-resource`;
|
|
924
951
|
const authHeader = request.headers.get("Authorization");
|
|
925
|
-
if (!authHeader || !authHeader.startsWith("Bearer ")) return this.createErrorResponse("invalid_token", "Missing or invalid access token", 401, { "WWW-Authenticate":
|
|
952
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) return this.createErrorResponse("invalid_token", "Missing or invalid access token", 401, { "WWW-Authenticate": this.buildWwwAuthenticateHeader(resourceMetadataUrl, "invalid_token", "Missing or invalid access token") });
|
|
926
953
|
const accessToken = authHeader.substring(7);
|
|
927
954
|
const parts = accessToken.split(":");
|
|
928
955
|
const isPossiblyInternalFormat = parts.length === 3;
|
|
@@ -934,14 +961,14 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
934
961
|
const id = await generateTokenId(accessToken);
|
|
935
962
|
tokenData = await env.OAUTH_KV.get(`token:${userId}:${grantId}:${id}`, { type: "json" });
|
|
936
963
|
}
|
|
937
|
-
if (!tokenData && !this.options.resolveExternalToken) return this.createErrorResponse("invalid_token", "Invalid access token", 401, { "WWW-Authenticate":
|
|
964
|
+
if (!tokenData && !this.options.resolveExternalToken) return this.createErrorResponse("invalid_token", "Invalid access token", 401, { "WWW-Authenticate": this.buildWwwAuthenticateHeader(resourceMetadataUrl, "invalid_token") });
|
|
938
965
|
if (tokenData) {
|
|
939
966
|
const now = Math.floor(Date.now() / 1e3);
|
|
940
|
-
if (tokenData.expiresAt < now) return this.createErrorResponse("invalid_token", "Access token expired", 401, { "WWW-Authenticate":
|
|
967
|
+
if (tokenData.expiresAt < now) return this.createErrorResponse("invalid_token", "Access token expired", 401, { "WWW-Authenticate": this.buildWwwAuthenticateHeader(resourceMetadataUrl, "invalid_token") });
|
|
941
968
|
if (tokenData.audience) {
|
|
942
969
|
const requestUrl = new URL(request.url);
|
|
943
970
|
const resourceServer = `${requestUrl.protocol}//${requestUrl.host}${requestUrl.pathname}`;
|
|
944
|
-
if (!(Array.isArray(tokenData.audience) ? tokenData.audience : [tokenData.audience]).some((aud) => audienceMatches(resourceServer, aud))) return this.createErrorResponse("invalid_token", "Token audience does not match resource server", 401, { "WWW-Authenticate":
|
|
971
|
+
if (!(Array.isArray(tokenData.audience) ? tokenData.audience : [tokenData.audience]).some((aud) => audienceMatches(resourceServer, aud))) return this.createErrorResponse("invalid_token", "Token audience does not match resource server", 401, { "WWW-Authenticate": this.buildWwwAuthenticateHeader(resourceMetadataUrl, "invalid_token", "Invalid audience") });
|
|
945
972
|
}
|
|
946
973
|
ctx.props = await decryptProps(await unwrapKeyWithToken(accessToken, tokenData.wrappedEncryptionKey), tokenData.grant.encryptedProps);
|
|
947
974
|
} else if (this.options.resolveExternalToken) {
|
|
@@ -950,16 +977,15 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
950
977
|
request,
|
|
951
978
|
env
|
|
952
979
|
});
|
|
953
|
-
if (!ext) return this.createErrorResponse("invalid_token", "Invalid access token", 401, { "WWW-Authenticate":
|
|
980
|
+
if (!ext) return this.createErrorResponse("invalid_token", "Invalid access token", 401, { "WWW-Authenticate": this.buildWwwAuthenticateHeader(resourceMetadataUrl, "invalid_token") });
|
|
954
981
|
if (ext.audience) {
|
|
955
982
|
const requestUrl = new URL(request.url);
|
|
956
983
|
const resourceServer = `${requestUrl.protocol}//${requestUrl.host}${requestUrl.pathname}`;
|
|
957
|
-
if (!(Array.isArray(ext.audience) ? ext.audience : [ext.audience]).some((aud) => audienceMatches(resourceServer, aud))) return this.createErrorResponse("invalid_token", "Token audience does not match resource server", 401, { "WWW-Authenticate":
|
|
984
|
+
if (!(Array.isArray(ext.audience) ? ext.audience : [ext.audience]).some((aud) => audienceMatches(resourceServer, aud))) return this.createErrorResponse("invalid_token", "Token audience does not match resource server", 401, { "WWW-Authenticate": this.buildWwwAuthenticateHeader(resourceMetadataUrl, "invalid_token", "Invalid audience") });
|
|
958
985
|
}
|
|
959
986
|
ctx.props = ext.props;
|
|
960
987
|
}
|
|
961
988
|
if (!env.OAUTH_PROVIDER) env.OAUTH_PROVIDER = this.createOAuthHelpers(env);
|
|
962
|
-
const url = new URL(request.url);
|
|
963
989
|
const apiHandler = this.findApiHandlerForUrl(url);
|
|
964
990
|
if (!apiHandler) return this.createErrorResponse("invalid_request", "No handler found for API route", 404);
|
|
965
991
|
if (apiHandler.type === HandlerType.EXPORTED_HANDLER) return apiHandler.handler.fetch(request, env, ctx);
|
|
@@ -1183,6 +1209,14 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
1183
1209
|
return JSON.parse(text);
|
|
1184
1210
|
}
|
|
1185
1211
|
/**
|
|
1212
|
+
* Builds a WWW-Authenticate header value with resource_metadata per RFC 9728 §5.1
|
|
1213
|
+
*/
|
|
1214
|
+
buildWwwAuthenticateHeader(resourceMetadataUrl, error, errorDescription) {
|
|
1215
|
+
let header = `Bearer realm="OAuth", resource_metadata="${resourceMetadataUrl}", error="${error}"`;
|
|
1216
|
+
if (errorDescription) header += `, error_description="${errorDescription}"`;
|
|
1217
|
+
return header;
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1186
1220
|
* Helper function to create OAuth error responses
|
|
1187
1221
|
* @param code - OAuth error code (e.g., 'invalid_request', 'invalid_token')
|
|
1188
1222
|
* @param description - Human-readable error description
|
|
@@ -1338,6 +1372,37 @@ function validateRedirectUriScheme(redirectUri) {
|
|
|
1338
1372
|
for (const dangerousScheme of dangerousSchemes) if (scheme === dangerousScheme) throw new Error("Invalid redirect URI");
|
|
1339
1373
|
}
|
|
1340
1374
|
/**
|
|
1375
|
+
* Checks if a URI is a loopback redirect URI (127.0.0.0/8 or ::1)
|
|
1376
|
+
* Per RFC 8252 Section 7.3, these get special port handling
|
|
1377
|
+
*/
|
|
1378
|
+
function isLoopbackUri(uri) {
|
|
1379
|
+
try {
|
|
1380
|
+
const host = new URL(uri).hostname;
|
|
1381
|
+
if (host.match(/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) return true;
|
|
1382
|
+
if (host === "::1" || host === "[::1]") return true;
|
|
1383
|
+
return false;
|
|
1384
|
+
} catch {
|
|
1385
|
+
return false;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
/**
|
|
1389
|
+
* Validates a redirect URI against registered URIs with RFC 8252 loopback support.
|
|
1390
|
+
* For loopback URIs (127.x.x.x, ::1), any port is allowed as long as scheme, host, path, and query match.
|
|
1391
|
+
* For non-loopback URIs, exact match is required.
|
|
1392
|
+
*/
|
|
1393
|
+
function isValidRedirectUri(requestUri, registeredUris) {
|
|
1394
|
+
return registeredUris.some((registered) => {
|
|
1395
|
+
if (isLoopbackUri(requestUri) && isLoopbackUri(registered)) try {
|
|
1396
|
+
const reqUrl = new URL(requestUri);
|
|
1397
|
+
const regUrl = new URL(registered);
|
|
1398
|
+
return reqUrl.protocol === regUrl.protocol && reqUrl.hostname === regUrl.hostname && reqUrl.pathname === regUrl.pathname && reqUrl.search === regUrl.search;
|
|
1399
|
+
} catch {
|
|
1400
|
+
return false;
|
|
1401
|
+
}
|
|
1402
|
+
return requestUri === registered;
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
/**
|
|
1341
1406
|
* Encodes a string as base64url (URL-safe base64)
|
|
1342
1407
|
* @param str - The string to encode
|
|
1343
1408
|
* @returns The base64url encoded string
|
|
@@ -1506,11 +1571,12 @@ var OAuthHelpersImpl = class {
|
|
|
1506
1571
|
const resource = parseResourceParameter(resourceParam);
|
|
1507
1572
|
if (resourceParam && !resource) throw new Error("The resource parameter must be a valid absolute URI without a fragment");
|
|
1508
1573
|
if (responseType === "token" && !this.provider.options.allowImplicitFlow) throw new Error("The implicit grant flow is not enabled for this provider");
|
|
1574
|
+
if (codeChallengeMethod === "plain" && this.provider.options.allowPlainPKCE === false) throw new Error("The plain PKCE method is not allowed. Use S256 instead.");
|
|
1509
1575
|
if (clientId) {
|
|
1510
1576
|
const clientInfo = await this.lookupClient(clientId);
|
|
1511
1577
|
if (!clientInfo) throw new Error(`Invalid client. The clientId provided does not match to this client.`);
|
|
1512
1578
|
if (clientInfo && redirectUri) {
|
|
1513
|
-
if (!clientInfo.redirectUris
|
|
1579
|
+
if (!isValidRedirectUri(redirectUri, clientInfo.redirectUris)) throw new Error(`Invalid redirect URI. The redirect URI provided does not match any registered URI for this client.`);
|
|
1514
1580
|
}
|
|
1515
1581
|
}
|
|
1516
1582
|
return {
|
|
@@ -1543,7 +1609,7 @@ var OAuthHelpersImpl = class {
|
|
|
1543
1609
|
const { clientId, redirectUri } = options.request;
|
|
1544
1610
|
if (!clientId || !redirectUri) throw new Error("Client ID and Redirect URI are required in the authorization request.");
|
|
1545
1611
|
const clientInfo = await this.lookupClient(clientId);
|
|
1546
|
-
if (!clientInfo || !clientInfo.redirectUris
|
|
1612
|
+
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.");
|
|
1547
1613
|
const grantId = generateRandomString(16);
|
|
1548
1614
|
const { encryptedData, key: encryptionKey } = await encryptProps(options.props);
|
|
1549
1615
|
const now = Math.floor(Date.now() / 1e3);
|