@cloudflare/workers-oauth-provider 0.3.2 → 0.4.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.
@@ -253,6 +253,16 @@ interface OAuthProviderOptions<Env = Cloudflare.Env> {
253
253
  * Defaults to false.
254
254
  */
255
255
  clientIdMetadataDocumentEnabled?: boolean;
256
+ /**
257
+ * When true, resource validation during token exchange compares origins only
258
+ * (scheme + host + port) instead of exact URI matching. This allows grants issued
259
+ * with an origin-only resource (e.g. `https://server.com`) to be used with
260
+ * path-aware resource requests (e.g. `https://server.com/mcp`), enabling seamless
261
+ * migration from pre-0.4.0 versions that stored origin-only resource URIs.
262
+ *
263
+ * Defaults to false (strict exact matching per RFC 8707).
264
+ */
265
+ resourceMatchOriginOnly?: boolean;
256
266
  /**
257
267
  * Optional metadata for RFC 9728 OAuth 2.0 Protected Resource Metadata.
258
268
  * Controls the response served at /.well-known/oauth-protected-resource.
@@ -537,10 +537,11 @@ var OAuthProviderImpl = class OAuthProviderImpl {
537
537
  grantData.expiresAt = expiresAt;
538
538
  }
539
539
  await this.saveGrantWithTTL(env, grantKey, grantData, now);
540
+ const originOnly = !!this.options.resourceMatchOriginOnly;
540
541
  if (body.resource && grantData.resource) {
541
542
  const requestedResources = Array.isArray(body.resource) ? body.resource : [body.resource];
542
543
  const grantedResources = Array.isArray(grantData.resource) ? grantData.resource : [grantData.resource];
543
- for (const requested of requestedResources) if (!grantedResources.includes(requested)) return this.createErrorResponse("invalid_target", "Requested resource was not included in the authorization request");
544
+ 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");
544
545
  }
545
546
  const audience = parseResourceParameter(body.resource || grantData.resource);
546
547
  if ((body.resource || grantData.resource) && !audience) return this.createErrorResponse("invalid_target", "The resource parameter must be a valid absolute URI without a fragment");
@@ -657,10 +658,11 @@ var OAuthProviderImpl = class OAuthProviderImpl {
657
658
  grantData.refreshTokenId = newRefreshTokenId;
658
659
  grantData.refreshTokenWrappedKey = newRefreshTokenWrappedKey;
659
660
  await this.saveGrantWithTTL(env, grantKey, grantData, now);
661
+ const originOnly = !!this.options.resourceMatchOriginOnly;
660
662
  if (body.resource && grantData.resource) {
661
663
  const requestedResources = Array.isArray(body.resource) ? body.resource : [body.resource];
662
664
  const grantedResources = Array.isArray(grantData.resource) ? grantData.resource : [grantData.resource];
663
- for (const requested of requestedResources) if (!grantedResources.includes(requested)) return this.createErrorResponse("invalid_target", "Requested resource was not included in the authorization request");
665
+ 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");
664
666
  }
665
667
  const audience = parseResourceParameter(body.resource || grantData.resource);
666
668
  if ((body.resource || grantData.resource) && !audience) return this.createErrorResponse("invalid_target", "The resource parameter must be a valid absolute URI without a fragment");
@@ -712,12 +714,13 @@ var OAuthProviderImpl = class OAuthProviderImpl {
712
714
  const grantData = await env.OAUTH_KV.get(grantKey, { type: "json" });
713
715
  if (!grantData) throw new OAuthError("invalid_grant", "Grant not found");
714
716
  let tokenScopes = this.downscope(requestedScopes, grantData.scope);
717
+ const originOnly = !!this.options.resourceMatchOriginOnly;
715
718
  let newAudience = tokenSummary.audience;
716
719
  if (requestedResource) {
717
720
  if (grantData.resource) {
718
721
  const requestedResources = Array.isArray(requestedResource) ? requestedResource : [requestedResource];
719
722
  const grantedResources = Array.isArray(grantData.resource) ? grantData.resource : [grantData.resource];
720
- for (const requested of requestedResources) if (!grantedResources.includes(requested)) throw new OAuthError("invalid_target", "Requested resource was not included in the authorization request");
723
+ 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");
721
724
  }
722
725
  const parsedResource = parseResourceParameter(requestedResource);
723
726
  if (!parsedResource) throw new OAuthError("invalid_target", "The resource parameter must be a valid absolute URI without a fragment");
@@ -1353,6 +1356,19 @@ function parseResourceParameter(value) {
1353
1356
  return value;
1354
1357
  }
1355
1358
  /**
1359
+ * Checks if a requested resource matches a granted resource.
1360
+ * When originOnly is true, compares only the origin (scheme + host + port),
1361
+ * allowing path-aware resources to match origin-only grants.
1362
+ */
1363
+ function resourceMatches(requested, granted, originOnly) {
1364
+ if (!originOnly) return requested === granted;
1365
+ try {
1366
+ return new URL(requested).origin === new URL(granted).origin;
1367
+ } catch {
1368
+ return requested === granted;
1369
+ }
1370
+ }
1371
+ /**
1356
1372
  * Hashes a secret value using SHA-256
1357
1373
  * @param secret - The secret value to hash
1358
1374
  * @returns A hex string representation of the hash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudflare/workers-oauth-provider",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "OAuth provider for Cloudflare Workers",
5
5
  "main": "dist/oauth-provider.js",
6
6
  "types": "dist/oauth-provider.d.ts",