@cloudflare/workers-oauth-provider 0.7.0 → 0.7.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.
- package/README.md +13 -0
- package/dist/oauth-provider.d.ts +28 -0
- package/dist/oauth-provider.js +82 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -390,6 +390,19 @@ Setup:
|
|
|
390
390
|
|
|
391
391
|
The AS enforces `resolved.issuer === iss` (confused-deputy guard) and validates ID-JAG `typ`, signature, audience, client binding, resource, `exp` / `iat` / `nbf`, max lifetime, and `jti` replay. Refresh tokens are not issued for this grant — the ID-JAG itself is the renewable assertion.
|
|
392
392
|
|
|
393
|
+
### Public clients
|
|
394
|
+
|
|
395
|
+
By default the EMA grant requires client authentication, so public clients (`token_endpoint_auth_method: 'none'`) are rejected. Set `allowPublicClients: true` to also accept them:
|
|
396
|
+
|
|
397
|
+
```ts
|
|
398
|
+
enterpriseManagedAuthorization: {
|
|
399
|
+
allowPublicClients: true,
|
|
400
|
+
// ... trustedIssuers, mapClaims ...
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
This is useful for clients registered via a [Client ID Metadata Document (CIMD)](https://modelcontextprotocol.io/), which are always public and therefore cannot present a client secret. With this enabled, trust rests on the IdP-issued, signature-verified, short-lived, single-use ID-JAG assertion (audience-, resource-, and client-bound) rather than on a separately presented client secret. Leave it unset (default `false`) to keep the spec-default behavior of requiring client authentication.
|
|
405
|
+
|
|
393
406
|
Experimental — the MCP extension is still a draft.
|
|
394
407
|
|
|
395
408
|
## Custom Error Responses
|
package/dist/oauth-provider.d.ts
CHANGED
|
@@ -269,6 +269,22 @@ interface EmaOptions<Env = Cloudflare.Env> {
|
|
|
269
269
|
clockSkewSeconds?: number;
|
|
270
270
|
/** Maximum accepted assertion lifetime in seconds. Defaults to 300 seconds. */
|
|
271
271
|
maxAssertionLifetimeSeconds?: number;
|
|
272
|
+
/**
|
|
273
|
+
* Allow public clients (`token_endpoint_auth_method: 'none'`) to use the
|
|
274
|
+
* enterprise-managed authorization (ID-JAG) grant.
|
|
275
|
+
*
|
|
276
|
+
* Defaults to `false`. By default the EMA grant requires client
|
|
277
|
+
* authentication, matching the MCP enterprise-managed-authorization draft.
|
|
278
|
+
*
|
|
279
|
+
* Set to `true` to also accept public clients on this grant — for example
|
|
280
|
+
* clients registered via a Client ID Metadata Document (CIMD), which are
|
|
281
|
+
* always public (`none`) and therefore cannot present a client secret. The
|
|
282
|
+
* security trade-off is documented in the README: the trust then rests on
|
|
283
|
+
* the IdP-issued, signature-verified, short-lived, single-use ID-JAG
|
|
284
|
+
* assertion (audience-, resource-, and client-bound) rather than on a
|
|
285
|
+
* separately presented client secret.
|
|
286
|
+
*/
|
|
287
|
+
allowPublicClients?: boolean;
|
|
272
288
|
}
|
|
273
289
|
//#endregion
|
|
274
290
|
//#region src/oauth-provider.d.ts
|
|
@@ -809,6 +825,18 @@ interface ClientInfo {
|
|
|
809
825
|
* URL to the client's JSON Web Key Set for validating signatures
|
|
810
826
|
*/
|
|
811
827
|
jwksUri?: string;
|
|
828
|
+
/**
|
|
829
|
+
* RFC 7591 §2.2 internationalized variants of the human-readable client
|
|
830
|
+
* metadata fields, keyed by the raw member name including its BCP 47 language
|
|
831
|
+
* tag (e.g. `"client_name#ja"`, `"tos_uri#fr"`).
|
|
832
|
+
*
|
|
833
|
+
* Only the human-readable fields the RFC names are captured here:
|
|
834
|
+
* `client_name`, `client_uri`, `logo_uri`, `tos_uri`, and `policy_uri`.
|
|
835
|
+
* The canonical (un-tagged) values continue to live in their own typed
|
|
836
|
+
* fields above; this map holds only the locale-specific variants so that
|
|
837
|
+
* consumers can perform their own locale selection.
|
|
838
|
+
*/
|
|
839
|
+
i18n?: Record<string, string>;
|
|
812
840
|
/**
|
|
813
841
|
* List of email addresses for contacting the client developers
|
|
814
842
|
*/
|
package/dist/oauth-provider.js
CHANGED
|
@@ -1601,7 +1601,7 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
1601
1601
|
async handleJwtBearerGrant(body, clientInfo, env, requestUrl, request) {
|
|
1602
1602
|
const enterpriseOptions = this.options.enterpriseManagedAuthorization;
|
|
1603
1603
|
if (!enterpriseOptions) return this.createErrorResponse("unsupported_grant_type", { description: "Grant type not supported" });
|
|
1604
|
-
if (clientInfo.tokenEndpointAuthMethod === "none") return this.createErrorResponse("invalid_client", {
|
|
1604
|
+
if (clientInfo.tokenEndpointAuthMethod === "none" && !enterpriseOptions.allowPublicClients) return this.createErrorResponse("invalid_client", {
|
|
1605
1605
|
description: "Enterprise-managed authorization requires client authentication",
|
|
1606
1606
|
statusCode: 401
|
|
1607
1607
|
});
|
|
@@ -1921,12 +1921,13 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
1921
1921
|
clientInfo = {
|
|
1922
1922
|
clientId,
|
|
1923
1923
|
redirectUris,
|
|
1924
|
-
clientName: OAuthProviderImpl.validateStringField(clientMetadata.client_name),
|
|
1925
|
-
logoUri: OAuthProviderImpl.
|
|
1926
|
-
clientUri: OAuthProviderImpl.
|
|
1927
|
-
policyUri: OAuthProviderImpl.
|
|
1928
|
-
tosUri: OAuthProviderImpl.
|
|
1929
|
-
jwksUri: OAuthProviderImpl.
|
|
1924
|
+
clientName: OAuthProviderImpl.validateStringField(clientMetadata.client_name, "client_name"),
|
|
1925
|
+
logoUri: OAuthProviderImpl.validateOptionalUriField(clientMetadata.logo_uri, "logo_uri"),
|
|
1926
|
+
clientUri: OAuthProviderImpl.validateOptionalUriField(clientMetadata.client_uri, "client_uri"),
|
|
1927
|
+
policyUri: OAuthProviderImpl.validateOptionalUriField(clientMetadata.policy_uri, "policy_uri"),
|
|
1928
|
+
tosUri: OAuthProviderImpl.validateOptionalUriField(clientMetadata.tos_uri, "tos_uri"),
|
|
1929
|
+
jwksUri: OAuthProviderImpl.validateOptionalUriField(clientMetadata.jwks_uri, "jwks_uri"),
|
|
1930
|
+
i18n: OAuthProviderImpl.extractI18nFields(clientMetadata),
|
|
1930
1931
|
contacts: OAuthProviderImpl.validateStringArray(clientMetadata.contacts),
|
|
1931
1932
|
grantTypes: OAuthProviderImpl.validateStringArray(clientMetadata.grant_types) || [
|
|
1932
1933
|
GrantType.AUTHORIZATION_CODE,
|
|
@@ -1960,6 +1961,9 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
1960
1961
|
registration_client_uri: `${this.options.clientRegistrationEndpoint}/${clientId}`,
|
|
1961
1962
|
client_id_issued_at: clientInfo.registrationDate
|
|
1962
1963
|
};
|
|
1964
|
+
if (clientInfo.i18n) {
|
|
1965
|
+
for (const [key, value] of Object.entries(clientInfo.i18n)) if (!(key in response)) response[key] = value;
|
|
1966
|
+
}
|
|
1963
1967
|
if (clientSecret) {
|
|
1964
1968
|
response.client_secret = clientSecret;
|
|
1965
1969
|
response.client_secret_expires_at = this.options.clientRegistrationTTL && clientInfo.registrationDate ? clientInfo.registrationDate + this.options.clientRegistrationTTL : 0;
|
|
@@ -2203,6 +2207,70 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
2203
2207
|
return field;
|
|
2204
2208
|
}
|
|
2205
2209
|
/**
|
|
2210
|
+
* Validates that a field is an optional URI string using a safe scheme.
|
|
2211
|
+
*
|
|
2212
|
+
* Client metadata URI fields (e.g. logo_uri, client_uri, policy_uri, tos_uri,
|
|
2213
|
+
* jwks_uri) are frequently rendered into HTML attributes such as `<a href>` or
|
|
2214
|
+
* `<img src>` on consent screens. Permitting non-http(s) schemes such as
|
|
2215
|
+
* `javascript:` or `data:` would allow script execution in that context, so we
|
|
2216
|
+
* require an absolute http: or https: URL here, matching how redirect URIs are
|
|
2217
|
+
* already restricted.
|
|
2218
|
+
*
|
|
2219
|
+
* @param field - The field to validate
|
|
2220
|
+
* @param fieldName - Name of the field for error messages
|
|
2221
|
+
* @returns The validated URI string or undefined
|
|
2222
|
+
* @throws Error if the field is not a string or is not an absolute http(s) URL
|
|
2223
|
+
*/
|
|
2224
|
+
static validateOptionalUriField(field, fieldName) {
|
|
2225
|
+
const value = OAuthProviderImpl.validateStringField(field, fieldName);
|
|
2226
|
+
if (value === void 0) return void 0;
|
|
2227
|
+
let parsed;
|
|
2228
|
+
try {
|
|
2229
|
+
parsed = new URL(value);
|
|
2230
|
+
} catch {
|
|
2231
|
+
throw new Error(`Invalid ${fieldName}: must be an absolute http: or https: URL`);
|
|
2232
|
+
}
|
|
2233
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error(`Invalid ${fieldName}: must be an absolute http: or https: URL`);
|
|
2234
|
+
return value;
|
|
2235
|
+
}
|
|
2236
|
+
static {
|
|
2237
|
+
this.I18N_FIELDS = {
|
|
2238
|
+
client_name: "string",
|
|
2239
|
+
client_uri: "uri",
|
|
2240
|
+
logo_uri: "uri",
|
|
2241
|
+
tos_uri: "uri",
|
|
2242
|
+
policy_uri: "uri"
|
|
2243
|
+
};
|
|
2244
|
+
}
|
|
2245
|
+
/**
|
|
2246
|
+
* Extracts RFC 7591 §2.2 internationalized metadata variants from a raw
|
|
2247
|
+
* registration payload.
|
|
2248
|
+
*
|
|
2249
|
+
* Localized variants are expressed by appending a `#<BCP 47 language tag>`
|
|
2250
|
+
* suffix to a metadata member name (e.g. `client_name#ja`, `tos_uri#fr`).
|
|
2251
|
+
* Only the human-readable fields the RFC names are considered; each value is
|
|
2252
|
+
* validated with the same rules as its canonical field (URI fields must be
|
|
2253
|
+
* absolute http(s) URLs). The raw `field#tag` keys are preserved verbatim so
|
|
2254
|
+
* that consumers can do their own locale matching.
|
|
2255
|
+
*
|
|
2256
|
+
* @param raw - The parsed client metadata object
|
|
2257
|
+
* @returns A map of `field#tag` to validated value, or undefined if none present
|
|
2258
|
+
* @throws Error if a localized value fails its field's validation
|
|
2259
|
+
*/
|
|
2260
|
+
static extractI18nFields(raw) {
|
|
2261
|
+
const result = {};
|
|
2262
|
+
for (const key of Object.keys(raw)) {
|
|
2263
|
+
const hashIndex = key.indexOf("#");
|
|
2264
|
+
if (hashIndex <= 0 || hashIndex === key.length - 1) continue;
|
|
2265
|
+
const baseField = key.slice(0, hashIndex);
|
|
2266
|
+
const kind = OAuthProviderImpl.I18N_FIELDS[baseField];
|
|
2267
|
+
if (!kind) continue;
|
|
2268
|
+
const validated = kind === "uri" ? OAuthProviderImpl.validateOptionalUriField(raw[key], key) : OAuthProviderImpl.validateStringField(raw[key], key);
|
|
2269
|
+
if (validated !== void 0) result[key] = validated;
|
|
2270
|
+
}
|
|
2271
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
2272
|
+
}
|
|
2273
|
+
/**
|
|
2206
2274
|
* Validates that a field is a string array or undefined
|
|
2207
2275
|
* @param arr - The array to validate
|
|
2208
2276
|
* @param fieldName - Name of the field for error messages
|
|
@@ -2250,11 +2318,12 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
2250
2318
|
clientId,
|
|
2251
2319
|
redirectUris,
|
|
2252
2320
|
clientName: OAuthProviderImpl.validateStringField(rawMetadata.client_name, "client_name"),
|
|
2253
|
-
clientUri: OAuthProviderImpl.
|
|
2254
|
-
logoUri: OAuthProviderImpl.
|
|
2255
|
-
policyUri: OAuthProviderImpl.
|
|
2256
|
-
tosUri: OAuthProviderImpl.
|
|
2257
|
-
jwksUri: OAuthProviderImpl.
|
|
2321
|
+
clientUri: OAuthProviderImpl.validateOptionalUriField(rawMetadata.client_uri, "client_uri"),
|
|
2322
|
+
logoUri: OAuthProviderImpl.validateOptionalUriField(rawMetadata.logo_uri, "logo_uri"),
|
|
2323
|
+
policyUri: OAuthProviderImpl.validateOptionalUriField(rawMetadata.policy_uri, "policy_uri"),
|
|
2324
|
+
tosUri: OAuthProviderImpl.validateOptionalUriField(rawMetadata.tos_uri, "tos_uri"),
|
|
2325
|
+
jwksUri: OAuthProviderImpl.validateOptionalUriField(rawMetadata.jwks_uri, "jwks_uri"),
|
|
2326
|
+
i18n: OAuthProviderImpl.extractI18nFields(rawMetadata),
|
|
2258
2327
|
contacts: OAuthProviderImpl.validateStringArray(rawMetadata.contacts, "contacts"),
|
|
2259
2328
|
grantTypes: OAuthProviderImpl.validateStringArray(rawMetadata.grant_types, "grant_types") || ["authorization_code"],
|
|
2260
2329
|
responseTypes: OAuthProviderImpl.validateStringArray(rawMetadata.response_types, "response_types") || ["code"],
|
|
@@ -2942,6 +3011,7 @@ var OAuthHelpersImpl = class {
|
|
|
2942
3011
|
policyUri: clientInfo.policyUri,
|
|
2943
3012
|
tosUri: clientInfo.tosUri,
|
|
2944
3013
|
jwksUri: clientInfo.jwksUri,
|
|
3014
|
+
i18n: clientInfo.i18n,
|
|
2945
3015
|
contacts: clientInfo.contacts,
|
|
2946
3016
|
grantTypes: clientInfo.grantTypes || [
|
|
2947
3017
|
GrantType.AUTHORIZATION_CODE,
|