@cloudflare/workers-oauth-provider 1.2.2 → 1.2.3

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.
@@ -209,7 +209,7 @@ function isValidOAuthScopeToken(scopeToken) {
209
209
 
210
210
  //#endregion
211
211
  //#region package.json
212
- var version = "1.2.2";
212
+ var version = "1.2.3";
213
213
 
214
214
  //#endregion
215
215
  //#region src/oauth-http.ts
@@ -1039,6 +1039,32 @@ function requestCarriesResourceQuery(requestUrl, resourceUrl) {
1039
1039
  for (const [name, value] of resourceUrl.searchParams) if (!requestUrl.searchParams.getAll(name).includes(value)) return false;
1040
1040
  return true;
1041
1041
  }
1042
+ /**
1043
+ * The configured resource a client-supplied resource indicator names when it is
1044
+ * not an exact match: one with the same origin and path whose query parameters
1045
+ * the indicator carries, as a request URL the resource covers would. With
1046
+ * several such resources, the one with the most query parameters wins and a tie
1047
+ * names none. A value without a query never matches here.
1048
+ */
1049
+ function findCoveringResource(requested, configured) {
1050
+ let url;
1051
+ try {
1052
+ url = new URL(requested);
1053
+ } catch {
1054
+ return;
1055
+ }
1056
+ if (url.search === "") return void 0;
1057
+ const covering = configured.map((resource) => ({
1058
+ resource,
1059
+ url: new URL(resource)
1060
+ })).filter((candidate) => candidate.url.origin === url.origin && (candidate.url.pathname || "/") === (url.pathname || "/") && requestCarriesResourceQuery(url, candidate.url)).map(({ resource, url: url$1 }) => ({
1061
+ resource,
1062
+ specificity: [...url$1.searchParams].length
1063
+ })).sort((a, b) => b.specificity - a.specificity);
1064
+ if (covering.length === 0) return void 0;
1065
+ if (covering.length > 1 && covering[0].specificity === covering[1].specificity) return void 0;
1066
+ return covering[0].resource;
1067
+ }
1042
1068
 
1043
1069
  //#endregion
1044
1070
  //#region src/oauth-client-metadata.ts
@@ -4705,6 +4731,27 @@ var OAuthProviderImpl = class {
4705
4731
  if (typeof singular !== "string" || !validateResourceUri(singular)) return void 0;
4706
4732
  return this.resourceServers.map((server) => server.resourceMetadata.resource).find((configured) => isExactResource(singular, configured));
4707
4733
  }
4734
+ /**
4735
+ * Resolve a client-supplied RFC 8707 resource indicator to a configured resource.
4736
+ *
4737
+ * An exact match wins. Otherwise a request may name the URL it uses at the resource
4738
+ * when that URL differs from a configured resource only by query parameters the
4739
+ * configured resource does not constrain, such as `https://mcp.example.com/mcp?mode=x`
4740
+ * for `https://mcp.example.com/mcp`. The resource server already treats that URL as
4741
+ * covered by the resource (its 401 points at the resource's metadata, and the
4742
+ * resource's tokens are accepted there), and RFC 8707 §2.2 lets the authorization
4743
+ * server map a requested value to a more general resource. The audience stays the
4744
+ * configured resource, so the token is no broader. See {@link findCoveringResource}.
4745
+ * Only client requests use this; stored audiences and configuration stay exact.
4746
+ */
4747
+ findRequestedResource(value) {
4748
+ const exact = this.findConfiguredResource(value);
4749
+ if (exact) return exact;
4750
+ const distinct = Array.isArray(value) ? [...new Set(value)] : [value];
4751
+ const singular = distinct.length === 1 ? distinct[0] : void 0;
4752
+ if (typeof singular !== "string" || !validateResourceUri(singular)) return void 0;
4753
+ return findCoveringResource(singular, this.resourceServers.map((server) => server.resourceMetadata.resource));
4754
+ }
4708
4755
  /** Select the audience for a new interactive authorization. */
4709
4756
  resolveAuthorizationRequestResource(requestedResource) {
4710
4757
  if (requestedResource === void 0) {
@@ -4712,7 +4759,7 @@ var OAuthProviderImpl = class {
4712
4759
  if (defaultResource) return defaultResource;
4713
4760
  throw new AuthorizationError("invalid_target", { description: "The resource parameter is required when the authorization server has multiple resources" });
4714
4761
  }
4715
- const configured = this.findConfiguredResource(requestedResource);
4762
+ const configured = this.findRequestedResource(requestedResource);
4716
4763
  if (!configured) throw new AuthorizationError("invalid_target", { description: "The resource parameter must name exactly one configured protected resource" });
4717
4764
  return configured;
4718
4765
  }
@@ -4740,7 +4787,7 @@ var OAuthProviderImpl = class {
4740
4787
  /** Validate explicit resource syntax and configured-resource policy. */
4741
4788
  validateTokenRequestResourceIndicator(requestedResource) {
4742
4789
  if (requestedResource === void 0) return;
4743
- if (!this.findConfiguredResource(requestedResource)) throw new OAuthError("invalid_target", {
4790
+ if (!this.findRequestedResource(requestedResource)) throw new OAuthError("invalid_target", {
4744
4791
  description: "The resource parameter must name exactly one configured protected resource",
4745
4792
  internal: {
4746
4793
  category: "resource-indicator",
@@ -4759,7 +4806,7 @@ var OAuthProviderImpl = class {
4759
4806
  }
4760
4807
  });
4761
4808
  const resourceWasProvided = requestedResource !== void 0;
4762
- const requestedAudience = resourceWasProvided ? this.findConfiguredResource(requestedResource) : void 0;
4809
+ const requestedAudience = resourceWasProvided ? this.findRequestedResource(requestedResource) : void 0;
4763
4810
  if (resourceWasProvided && (!requestedAudience || requestedAudience !== subjectAudience)) throw new OAuthError("invalid_target", {
4764
4811
  description: "The requested resource must exactly match the subject token audience",
4765
4812
  internal: {
@@ -4778,7 +4825,7 @@ var OAuthProviderImpl = class {
4778
4825
  const resourceWasProvided = requestedResource !== void 0;
4779
4826
  const grantResourceWasStored = grantedResource !== void 0;
4780
4827
  const canonicalGrantResource = this.findStoredConfiguredResource(grantedResource);
4781
- const canonicalRequestedResource = resourceWasProvided ? this.findConfiguredResource(requestedResource) : void 0;
4828
+ const canonicalRequestedResource = resourceWasProvided ? this.findRequestedResource(requestedResource) : void 0;
4782
4829
  if (grantResourceWasStored && !canonicalGrantResource) throw new OAuthError("invalid_grant", {
4783
4830
  description: "The authorization grant is not bound to a configured resource",
4784
4831
  internal: {
@@ -147,7 +147,7 @@ Conforming MCP clients are required to send `resource` in authorization and toke
147
147
  - An authorization-code or refresh-token request may omit `resource`; the server inherits the resource already stored on the grant. If present, it must match that grant and cannot retarget it.
148
148
  - Malformed, unknown, or multi-valued resource input returns `invalid_target` before code consumption, callbacks, refresh rotation, or storage writes.
149
149
 
150
- ASCII case differences in the URI scheme and host are accepted, but port, path, query, trailing slash, and array cardinality remain strict. The authorization server always stores and returns the configured lowercase scheme-and-host spelling. The token response includes the selected resource, and the access-token audience contains that resource alone.
150
+ ASCII case differences in the URI scheme and host are accepted, but port, path, trailing slash, and array cardinality remain strict. A client may also name the URL it uses at the resource when that URL adds query parameters the resource does not constrain, such as `https://mcp.example.com/mcp?mode=direct` for `https://mcp.example.com/mcp`: the protected route already accepts the resource's tokens there and points its `401` at the resource's metadata, so the authorization server maps the value to the resource (RFC 8707 §2.2). The query parameters a query-bearing resource names must be present, and when several resources cover a value the one with the most query parameters wins; a tie is rejected. Stored grant and token audiences, and `validateToken`'s resource argument, stay exact. The authorization server always stores and returns the configured lowercase scheme-and-host spelling. The token response includes the selected resource, and the access-token audience contains that resource alone.
151
151
 
152
152
  Token exchange cannot change the resource. Both the subject-token audience and any explicit requested resource must resolve to the same registered canonical value. A token is exchanged by the client its grant was issued to unless `tokenExchangeCallback` returns `allowCrossClientExchange: true`. Internally and externally validated tokens are accepted at a protected route only when their audience matches that route's resource.
153
153
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudflare/workers-oauth-provider",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "description": "OAuth provider for Cloudflare Workers",
5
5
  "main": "dist/oauth-provider.js",
6
6
  "types": "dist/oauth-provider.d.ts",