@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.
package/dist/oauth-provider.js
CHANGED
|
@@ -209,7 +209,7 @@ function isValidOAuthScopeToken(scopeToken) {
|
|
|
209
209
|
|
|
210
210
|
//#endregion
|
|
211
211
|
//#region package.json
|
|
212
|
-
var version = "1.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.
|
|
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.
|
|
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.
|
|
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.
|
|
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,
|
|
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
|
|