@cloudflare/workers-oauth-provider 0.3.0 → 0.3.2

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.
@@ -1,6 +1,7 @@
1
1
  import { WorkerEntrypoint } from "cloudflare:workers";
2
2
 
3
3
  //#region src/oauth-provider.ts
4
+ const PROTECTED_RESOURCE_WELL_KNOWN_PREFIX = "/.well-known/oauth-protected-resource";
4
5
  if (!(typeof Cloudflare !== "undefined" && Cloudflare.compatibilityFlags?.global_fetch_strictly_public === true)) console.warn("CIMD (Client ID Metadata Document) is disabled: add '\"compatibility_flags\": [\"global_fetch_strictly_public\"]' to your wrangler.jsonc to enable. See: https://developers.cloudflare.com/workers/configuration/compatibility-flags/#global-fetch-strictly-public");
5
6
  /**
6
7
  * Enum representing the type of handler (ExportedHandler or WorkerEntrypoint)
@@ -141,7 +142,7 @@ var OAuthProviderImpl = class OAuthProviderImpl {
141
142
  async fetch(request, env, ctx) {
142
143
  const url = new URL(request.url);
143
144
  if (request.method === "OPTIONS") {
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
+ if (this.isApiRequest(url) || url.pathname === "/.well-known/oauth-authorization-server" || this.isProtectedResourceMetadataRequest(url) || this.isTokenEndpoint(url) || this.options.clientRegistrationEndpoint && this.isClientRegistrationEndpoint(url)) return this.addCorsHeaders(new Response(null, {
145
146
  status: 204,
146
147
  headers: { "Content-Length": "0" }
147
148
  }), request);
@@ -150,7 +151,7 @@ var OAuthProviderImpl = class OAuthProviderImpl {
150
151
  const response = await this.handleMetadataDiscovery(url);
151
152
  return this.addCorsHeaders(response, request);
152
153
  }
153
- if (url.pathname === "/.well-known/oauth-protected-resource") {
154
+ if (this.isProtectedResourceMetadataRequest(url)) {
154
155
  const response = this.handleProtectedResourceMetadata(url);
155
156
  return this.addCorsHeaders(response, request);
156
157
  }
@@ -245,6 +246,27 @@ var OAuthProviderImpl = class OAuthProviderImpl {
245
246
  return this.matchEndpoint(url, this.options.clientRegistrationEndpoint);
246
247
  }
247
248
  /**
249
+ * Checks if a URL is a request for OAuth Protected Resource Metadata (RFC 9728).
250
+ * Matches both the root well-known path and path-suffixed variants per RFC 9728 §3.1.
251
+ */
252
+ isProtectedResourceMetadataRequest(url) {
253
+ return url.pathname === PROTECTED_RESOURCE_WELL_KNOWN_PREFIX || url.pathname.startsWith(PROTECTED_RESOURCE_WELL_KNOWN_PREFIX + "/");
254
+ }
255
+ /**
256
+ * Derives the resource identifier from a protected resource metadata well-known URL.
257
+ * Per RFC 9728 §3.1, the well-known URI is inserted after the authority and before the path,
258
+ * so the resource identifier is reconstructed by removing the well-known prefix.
259
+ *
260
+ * Examples:
261
+ * /.well-known/oauth-protected-resource → origin (e.g. https://example.com)
262
+ * /.well-known/oauth-protected-resource/mcp → origin + /mcp (e.g. https://example.com/mcp)
263
+ */
264
+ deriveResourceIdentifier(requestUrl) {
265
+ const suffix = requestUrl.pathname.slice(37);
266
+ if (!suffix || suffix === "/") return requestUrl.origin;
267
+ return `${requestUrl.origin}${suffix}`;
268
+ }
269
+ /**
248
270
  * Parses and validates a token endpoint request (used for both token exchange and revocation)
249
271
  * @param request - The HTTP request to parse
250
272
  * @returns Promise with parsed body and client info, or error response
@@ -389,7 +411,7 @@ var OAuthProviderImpl = class OAuthProviderImpl {
389
411
  const tokenEndpointUrl = this.getFullEndpointUrl(this.options.tokenEndpoint, requestUrl);
390
412
  const authServerOrigin = new URL(tokenEndpointUrl).origin;
391
413
  const metadata = {
392
- resource: rm?.resource ?? requestUrl.origin,
414
+ resource: rm?.resource ?? this.deriveResourceIdentifier(requestUrl),
393
415
  authorization_servers: rm?.authorization_servers ?? [authServerOrigin],
394
416
  scopes_supported: rm?.scopes_supported ?? this.options.scopesSupported,
395
417
  bearer_methods_supported: rm?.bearer_methods_supported ?? ["header"]
@@ -956,7 +978,7 @@ var OAuthProviderImpl = class OAuthProviderImpl {
956
978
  */
957
979
  async handleApiRequest(request, env, ctx) {
958
980
  const url = new URL(request.url);
959
- const resourceMetadataUrl = `${url.origin}/.well-known/oauth-protected-resource`;
981
+ const resourceMetadataUrl = `${url.origin}/.well-known/oauth-protected-resource${url.pathname}`;
960
982
  const authHeader = request.headers.get("Authorization");
961
983
  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") });
962
984
  const accessToken = authHeader.substring(7);
@@ -1390,14 +1412,16 @@ function validateRedirectUriScheme(redirectUri) {
1390
1412
  for (const dangerousScheme of dangerousSchemes) if (scheme === dangerousScheme) throw new Error("Invalid redirect URI");
1391
1413
  }
1392
1414
  /**
1393
- * Checks if a URI is a loopback redirect URI (127.0.0.0/8 or ::1)
1394
- * Per RFC 8252 Section 7.3, these get special port handling
1415
+ * Checks if a URI is a loopback redirect URI (127.0.0.0/8, ::1, or localhost).
1416
+ * Per RFC 8252 Section 7.3, loopback IPs get special port handling. This library
1417
+ * applies the same port flexibility to localhost for native apps (e.g., Claude Code).
1395
1418
  */
1396
1419
  function isLoopbackUri(uri) {
1397
1420
  try {
1398
1421
  const host = new URL(uri).hostname;
1399
1422
  if (host.match(/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) return true;
1400
1423
  if (host === "::1" || host === "[::1]") return true;
1424
+ if (host.toLowerCase() === "localhost") return true;
1401
1425
  return false;
1402
1426
  } catch {
1403
1427
  return false;
@@ -1405,7 +1429,7 @@ function isLoopbackUri(uri) {
1405
1429
  }
1406
1430
  /**
1407
1431
  * Validates a redirect URI against registered URIs with RFC 8252 loopback support.
1408
- * For loopback URIs (127.x.x.x, ::1), any port is allowed as long as scheme, host, path, and query match.
1432
+ * For loopback URIs (127.x.x.x, ::1, localhost), any port is allowed as long as scheme, host, path, and query match.
1409
1433
  * For non-loopback URIs, exact match is required.
1410
1434
  */
1411
1435
  function isValidRedirectUri(requestUri, registeredUris) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudflare/workers-oauth-provider",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "OAuth provider for Cloudflare Workers",
5
5
  "main": "dist/oauth-provider.js",
6
6
  "types": "dist/oauth-provider.d.ts",