@cloudflare/workers-oauth-provider 0.10.0 → 0.10.1

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 CHANGED
@@ -321,6 +321,10 @@ MCP 2026-07-28 deprecates DCR for new implementations in favor of CIMD. The endp
321
321
 
322
322
  Registration accepts only authentication methods, grants, and response types implemented by the configured provider, and rejects inconsistent grant/response combinations before storage. Omitted metadata uses the RFC 7591 defaults: `client_secret_basic`, `grant_types: ["authorization_code"]`, and `response_types: ["code"]`.
323
323
 
324
+ An explicitly supplied `token_endpoint_auth_method` is enforced exactly. When it is omitted, no explicit-method marker is stored and the client may use either `client_secret_basic` or `client_secret_post`, provided the same stored secret validates. Client records written by earlier releases have no marker and receive the same compatibility. This never crosses between `none` and a secret method and does not apply to CIMD clients.
325
+
326
+ Calling `OAuthHelpers.updateClient()` with `tokenEndpointAuthMethod` adds the marker; unrelated updates leave it unchanged.
327
+
324
328
  Related options:
325
329
 
326
330
  - `clientRegistrationTTL` controls the lifetime of dynamically registered clients. The default is 90 days.
@@ -345,16 +349,11 @@ The provider owns `tokenEndpoint`. It exchanges authorization codes for tokens,
345
349
 
346
350
  ## Resources and token audiences
347
351
 
348
- MCP clients must send the canonical MCP server URI as `resource` in authorization and token requests. The provider parses RFC 8707 resource indicators, stores the authorized resource on the grant, uses it as the access-token audience, and rejects resource expansion or audience mismatch.
349
-
350
- Resource policy follows `resourceMetadata.resource`:
351
-
352
- - When configured, authorization requests, token requests, and externally resolved tokens must use that one exact resource. `resourceMatchOriginOnly` cannot weaken this policy.
353
- - When omitted, valid resources are accepted. Token requests may inherit the authorization resource. If the authorization request also omits it, the provider uses the request origin as the default and issues an origin-bound token.
352
+ MCP clients are required to send the canonical MCP server URI as `resource` in authorization and token requests. The provider tolerates omission for compatibility: when `resourceMetadata.resource` is configured, it is used as the canonical default and inherited by later token requests; otherwise a token request inherits any resource already stored on the grant. An explicit resource that does not match a bound grant is rejected with `invalid_target`.
354
353
 
355
- Path-aware audiences use path-boundary prefix matching. A token for `https://example.com/mcp` can be used at `/mcp/tools`, but not at `/mcp-other`. Split deployments and deployments requiring path isolation should configure the canonical resource explicitly.
354
+ Legacy grants may have no stored resource. With no configured canonical resource, omitting `resource` preserves that unbound state. If a client supplies a resource during code exchange or refresh, it applies to that issued token but is not persisted as a new grant binding. Path-aware audiences use path-boundary prefix matching, so a token for `https://example.com/mcp` can be used at `/mcp/tools`, but not at `/mcp-other`.
356
355
 
357
- `resourceMatchOriginOnly` remains a migration option for grants created before path-aware resources were introduced. Do not enable it for a new deployment.
356
+ `resourceMatchOriginOnly` is deprecated; its existing behavior is unchanged. Prefer `resourceMetadata.resource` for new deployments.
358
357
 
359
358
  ## Scopes and step-up authorization
360
359
 
@@ -431,7 +430,7 @@ Deleting a client through `OAuthHelpers.deleteClient()` also revokes its grants
431
430
  | `allowTokenExchangeGrant` | Enable RFC 8693 | `false` |
432
431
  | `tokenExchangeCallback` | Update props, scopes, or lifetimes during token exchange | None |
433
432
  | `resolveExternalToken` | Validate external bearer credentials (advanced) | None |
434
- | `resourceMatchOriginOnly` | Migration mode for old origin-only resource grants | `false` |
433
+ | `resourceMatchOriginOnly` | Deprecated origin-only resource comparison | `false` |
435
434
  | `enterpriseManagedAuthorization` | Enable experimental ID-JAG grant support | Disabled |
436
435
  | `onError` | Observe or replace OAuth error responses | Logs a warning |
437
436
 
@@ -299,7 +299,7 @@ interface EmaOptions<Env = Cloudflare.Env> {
299
299
  }
300
300
  //#endregion
301
301
  //#region src/oauth-capabilities.d.ts
302
- type AuthorizationErrorCode = 'invalid_request' | 'unauthorized_client' | 'access_denied' | 'unsupported_response_type' | 'invalid_scope' | 'server_error' | 'temporarily_unavailable';
302
+ type AuthorizationErrorCode = 'invalid_request' | 'invalid_target' | 'unauthorized_client' | 'access_denied' | 'unsupported_response_type' | 'invalid_scope' | 'server_error' | 'temporarily_unavailable';
303
303
  interface AuthorizationErrorOptions {
304
304
  /** Wire-safe OAuth authorization error description. */
305
305
  description: string;
@@ -685,14 +685,16 @@ interface OAuthProviderOptions<Env = Cloudflare.Env> {
685
685
  */
686
686
  clientIdMetadataDocumentEnabled?: boolean;
687
687
  /**
688
- * When true, resource validation during token exchange compares origins only
689
- * (scheme + host + port) instead of exact URI matching. This allows grants issued
690
- * with an origin-only resource (e.g. `https://server.com`) to be used with
691
- * path-aware resource requests (e.g. `https://server.com/mcp`), enabling seamless
692
- * migration from pre-0.4.0 versions that stored origin-only resource URIs.
693
- * Explicit `resourceMetadata.resource` configuration always uses exact matching.
688
+ * When true, requested-vs-granted resource validation compares origins only
689
+ * (scheme + host + port) instead of exact URIs. This allows an origin-only
690
+ * grant such as `https://server.com` to accept `https://server.com/mcp`, but
691
+ * also ignores path and query differences. Configured canonical resources
692
+ * always use exact matching.
694
693
  *
695
694
  * Defaults to false (strict exact matching per RFC 8707).
695
+ *
696
+ * @deprecated This comparison is unsafe for shared-origin multi-path or
697
+ * multi-tenant deployments. Prefer configuring `resourceMetadata.resource`.
696
698
  */
697
699
  resourceMatchOriginOnly?: boolean;
698
700
  /**
@@ -706,10 +708,11 @@ interface OAuthProviderOptions<Env = Cloudflare.Env> {
706
708
  /**
707
709
  * The protected resource identifier URL (RFC 9728 `resource` field).
708
710
  *
709
- * Configuring this value pins authorization requests, token requests, and
710
- * access-token audiences to this exact resource. If omitted, the provider
711
- * accepts valid RFC 8707 resource indicators and uses the authorization
712
- * request origin as the default when the client does not send one.
711
+ * Configuring this value pins grants and access-token audiences to this
712
+ * exact resource. An omitted authorization resource defaults to this value,
713
+ * and an omitted token-request resource inherits it from the grant. Without
714
+ * configuration, explicit RFC 8707 resource indicators are accepted and
715
+ * omission remains unbound for backwards compatibility.
713
716
  */
714
717
  resource?: string;
715
718
  /**
@@ -53,6 +53,25 @@ function validateClientCapabilities(server, client) {
53
53
  if (client.grantTypes.includes("authorization_code") !== client.responseTypes.includes("code")) throw new Error("grant_types authorization_code and response_types code must be registered together");
54
54
  if (client.grantTypes.includes("implicit") !== client.responseTypes.includes("token")) throw new Error("grant_types implicit and response_types token must be registered together");
55
55
  }
56
+ /**
57
+ * Selects the capabilities from a Client ID Metadata Document that this
58
+ * authorization server supports. CIMD documents may advertise extension
59
+ * capabilities alongside the flow used with this server, so unsupported grant
60
+ * and response types are omitted from the effective client metadata instead of
61
+ * invalidating an otherwise usable client.
62
+ *
63
+ * The effective subset is still checked for grant/response consistency, and
64
+ * token endpoint authentication must have a mutually supported method.
65
+ */
66
+ function negotiateCimdClientCapabilities(server, client) {
67
+ const effective = {
68
+ grantTypes: client.grantTypes.filter((grantType) => server.grantTypes.includes(grantType)),
69
+ responseTypes: client.responseTypes.filter((responseType) => server.responseTypes.includes(responseType)),
70
+ tokenEndpointAuthMethod: client.tokenEndpointAuthMethod
71
+ };
72
+ validateClientCapabilities(server, effective);
73
+ return effective;
74
+ }
56
75
  function validateAuthorizationResponseType(server, responseType, clientResponseTypes) {
57
76
  if (!responseType) throw new AuthorizationError("invalid_request", { description: "response_type is required" });
58
77
  if (!server.responseTypes.includes(responseType)) throw new AuthorizationError("unsupported_response_type", { description: `The authorization server does not support response_type ${responseType}` });
@@ -794,6 +813,15 @@ let GrantType = /* @__PURE__ */ function(GrantType$1) {
794
813
  GrantType$1["JWT_BEARER"] = "urn:ietf:params:oauth:grant-type:jwt-bearer";
795
814
  return GrantType$1;
796
815
  }({});
816
+ function toPublicClientInfo(client) {
817
+ const { authMethodExplicit: _explicit, ...publicClient } = client;
818
+ return publicClient;
819
+ }
820
+ function isClientAuthMethodAllowed(client, presentedMethod, isClientMetadataDocument) {
821
+ if (presentedMethod === client.tokenEndpointAuthMethod) return true;
822
+ const isSecretMethod = (method) => method === "client_secret_basic" || method === "client_secret_post";
823
+ return !isClientMetadataDocument && client.authMethodExplicit === void 0 && isSecretMethod(client.tokenEndpointAuthMethod) && isSecretMethod(presentedMethod);
824
+ }
797
825
  /**
798
826
  * OAuth 2.0 Provider implementation for Cloudflare Workers
799
827
  * Implements authorization code flow with support for refresh tokens
@@ -935,7 +963,7 @@ var OAuthProviderImpl = class OAuthProviderImpl {
935
963
  /** Validate configured RFC 9728 protected resource metadata. */
936
964
  validateResourceMetadataOptions(options) {
937
965
  if (!options) return;
938
- if (options.resource && !validateResourceUri(options.resource)) throw new TypeError("resourceMetadata.resource must be an absolute HTTP(S) URI without a fragment");
966
+ if (options.resource !== void 0 && !validateResourceUri(options.resource)) throw new TypeError("resourceMetadata.resource must be an absolute HTTP(S) URI without a fragment");
939
967
  if (options.authorization_servers !== void 0) {
940
968
  if (options.authorization_servers.length === 0) throw new TypeError("resourceMetadata.authorization_servers must contain at least one issuer");
941
969
  for (const issuer of options.authorization_servers) {
@@ -1179,7 +1207,16 @@ var OAuthProviderImpl = class OAuthProviderImpl {
1179
1207
  }
1180
1208
  if (!clientInfo) return this.createInvalidClientResponse("Client not found", basicAuthenticationAttempted);
1181
1209
  const presentedAuthMethod = basicAuthenticationAttempted ? "client_secret_basic" : formData.has("client_secret") ? "client_secret_post" : "none";
1182
- if (presentedAuthMethod !== clientInfo.tokenEndpointAuthMethod) return this.createInvalidClientResponse("Client authentication failed", basicAuthenticationAttempted);
1210
+ const registeredAuthMethod = clientInfo.tokenEndpointAuthMethod;
1211
+ if (!isClientAuthMethodAllowed(clientInfo, presentedAuthMethod, !!this.options.clientIdMetadataDocumentEnabled && this.isClientMetadataUrl(clientInfo.clientId))) return this.createInvalidClientResponse("Client authentication failed", basicAuthenticationAttempted, {
1212
+ category: "client-authentication",
1213
+ reason: "token_endpoint_auth_method_mismatch",
1214
+ detail: {
1215
+ clientId: clientInfo.clientId,
1216
+ registeredMethod: registeredAuthMethod,
1217
+ presentedMethod: presentedAuthMethod
1218
+ }
1219
+ });
1183
1220
  if (presentedAuthMethod !== "none") {
1184
1221
  if (!clientSecret) return this.createInvalidClientResponse("Client authentication failed: missing client_secret", basicAuthenticationAttempted);
1185
1222
  if (!clientInfo.clientSecret) return this.createInvalidClientResponse("Client authentication failed: client has no registered secret", basicAuthenticationAttempted);
@@ -1671,7 +1708,7 @@ var OAuthProviderImpl = class OAuthProviderImpl {
1671
1708
  let tokenScopes = this.downscope(requestedScopes, tokenSummary.scope);
1672
1709
  const configuredResource = this.options.resourceMetadata?.resource;
1673
1710
  if (configuredResource && !isExactResource(tokenSummary.audience, configuredResource)) throw new OAuthError("invalid_target", { description: "Subject token is not bound to the configured resource" });
1674
- const newAudience = this.resolveTokenResource(requestedResource, grantData.resource);
1711
+ const newAudience = requestedResource === void 0 ? tokenSummary.audience : this.resolveTokenResource(requestedResource, grantData.resource);
1675
1712
  const now = Math.floor(Date.now() / 1e3);
1676
1713
  const subjectTokenRemainingLifetime = tokenSummary.expiresAt - now;
1677
1714
  if (subjectTokenRemainingLifetime < KV_MIN_EXPIRATION_TTL_SECONDS) throw new OAuthError("invalid_grant", { description: "Subject token is too close to expiry to exchange" });
@@ -2078,10 +2115,12 @@ var OAuthProviderImpl = class OAuthProviderImpl {
2078
2115
  statusCode: 400
2079
2116
  });
2080
2117
  }
2118
+ let authMethodWasExplicit;
2081
2119
  let authMethod;
2082
2120
  let grantTypes;
2083
2121
  let responseTypes;
2084
2122
  try {
2123
+ authMethodWasExplicit = clientMetadata.token_endpoint_auth_method !== void 0;
2085
2124
  authMethod = OAuthProviderImpl.validateStringField(clientMetadata.token_endpoint_auth_method) || "client_secret_basic";
2086
2125
  grantTypes = OAuthProviderImpl.validateStringArray(clientMetadata.grant_types, "grant_types") || [GrantType.AUTHORIZATION_CODE];
2087
2126
  responseTypes = OAuthProviderImpl.validateStringArray(clientMetadata.response_types, "response_types") || ["code"];
@@ -2121,7 +2160,8 @@ var OAuthProviderImpl = class OAuthProviderImpl {
2121
2160
  grantTypes,
2122
2161
  responseTypes,
2123
2162
  registrationDate: Math.floor(Date.now() / 1e3),
2124
- tokenEndpointAuthMethod: authMethod
2163
+ tokenEndpointAuthMethod: authMethod,
2164
+ ...authMethodWasExplicit ? { authMethodExplicit: true } : {}
2125
2165
  };
2126
2166
  if (!isPublicClient && hashedSecret) clientInfo.clientSecret = hashedSecret;
2127
2167
  } catch (error) {
@@ -2353,19 +2393,26 @@ var OAuthProviderImpl = class OAuthProviderImpl {
2353
2393
  }
2354
2394
  /**
2355
2395
  * Resolves an access-token audience from a token request and its authorization grant.
2356
- * Explicit resource configuration requires one exact value in both places. Without
2357
- * configuration, RFC 8707 downscoping is allowed and omission inherits the grant.
2396
+ * A configured canonical resource is inherited when omitted but cannot be overridden.
2397
+ * Without configuration, RFC 8707 downscoping is allowed, omission inherits a
2398
+ * bound grant, and a legacy unbound grant retains the v0.8.2 behavior.
2358
2399
  */
2359
2400
  resolveTokenResource(requestedResource, grantedResource) {
2401
+ const resourceWasProvided = requestedResource !== void 0;
2360
2402
  const requestedAudience = parseResourceParameter(requestedResource);
2361
- if (requestedResource && !requestedAudience) throw new OAuthError("invalid_target", { description: "The resource parameter must be a valid absolute URI without a fragment" });
2403
+ if (resourceWasProvided && !requestedAudience) throw new OAuthError("invalid_target", { description: "The resource parameter must be a valid absolute URI without a fragment" });
2404
+ const grantResourceWasStored = grantedResource !== void 0;
2362
2405
  const grantedAudience = parseResourceParameter(grantedResource);
2363
- if (grantedResource && !grantedAudience) throw new OAuthError("invalid_target", { description: "The authorization grant contains an invalid resource" });
2406
+ if (grantResourceWasStored && !grantedAudience) throw new OAuthError("invalid_target", { description: "The authorization grant contains an invalid resource" });
2364
2407
  const configuredResource = this.options.resourceMetadata?.resource;
2365
- if (configuredResource && (!isExactResource(grantedResource, configuredResource) || !isExactResource(requestedResource, configuredResource))) throw new OAuthError("invalid_target", { description: `The resource parameter must exactly match ${configuredResource}` });
2366
- if (!configuredResource && requestedResource && !grantedResource) throw new OAuthError("invalid_target", { description: "Requested resource was not included in the authorization request" });
2367
- const originOnly = configuredResource ? false : !!this.options.resourceMatchOriginOnly;
2368
- if (requestedResource && grantedResource) {
2408
+ if (configuredResource) {
2409
+ if (resourceWasProvided && !isExactResource(requestedResource, configuredResource)) throw new OAuthError("invalid_target", { description: `The resource parameter must exactly match ${configuredResource}` });
2410
+ if (isExactResource(grantedResource, configuredResource)) return configuredResource;
2411
+ if (!grantResourceWasStored) return configuredResource;
2412
+ throw new OAuthError("invalid_target", { description: "The authorization grant is not bound to the configured resource" });
2413
+ }
2414
+ const originOnly = !!this.options.resourceMatchOriginOnly;
2415
+ if (resourceWasProvided && grantResourceWasStored) {
2369
2416
  const requestedResources = Array.isArray(requestedResource) ? requestedResource : [requestedResource];
2370
2417
  const grantedResources = Array.isArray(grantedResource) ? grantedResource : [grantedResource];
2371
2418
  for (const requested of requestedResources) if (!grantedResources.some((granted) => resourceMatches(requested, granted, originOnly))) throw new OAuthError("invalid_target", { description: "Requested resource was not included in the authorization request" });
@@ -2570,13 +2617,13 @@ var OAuthProviderImpl = class OAuthProviderImpl {
2570
2617
  if (!clientName?.trim()) throw new Error("client_name is required and must not be empty");
2571
2618
  if (!redirectUris || redirectUris.length === 0) throw new Error("redirect_uris is required and must not be empty");
2572
2619
  if (declaredAuthMethod && !OAuthProviderImpl.CIMD_ALLOWED_AUTH_METHODS.includes(declaredAuthMethod) || authMethodChoices && !authMethodChoices.some((method) => OAuthProviderImpl.CIMD_ALLOWED_AUTH_METHODS.includes(method))) throw new Error(`CIMD client does not support an accepted token endpoint authentication method. Supported methods: ${OAuthProviderImpl.CIMD_ALLOWED_AUTH_METHODS.join(", ")}`);
2573
- const grantTypes = OAuthProviderImpl.validateStringArray(rawMetadata.grant_types, "grant_types") || [GrantType.AUTHORIZATION_CODE];
2574
- const responseTypes = OAuthProviderImpl.validateStringArray(rawMetadata.response_types, "response_types") || ["code"];
2620
+ const advertisedGrantTypes = OAuthProviderImpl.validateStringArray(rawMetadata.grant_types, "grant_types") || [GrantType.AUTHORIZATION_CODE];
2621
+ const advertisedResponseTypes = OAuthProviderImpl.validateStringArray(rawMetadata.response_types, "response_types") || ["code"];
2575
2622
  const effectiveAuthMethod = tokenEndpointAuthMethod || "none";
2576
- validateClientCapabilities(this.serverCapabilities, {
2623
+ const { grantTypes, responseTypes } = negotiateCimdClientCapabilities(this.serverCapabilities, {
2577
2624
  tokenEndpointAuthMethod: effectiveAuthMethod,
2578
- grantTypes,
2579
- responseTypes
2625
+ grantTypes: advertisedGrantTypes,
2626
+ responseTypes: advertisedResponseTypes
2580
2627
  });
2581
2628
  return {
2582
2629
  clientId,
@@ -2885,6 +2932,7 @@ function audienceMatches(resourceServerUrl, audienceValue) {
2885
2932
  function parseResourceParameter(value) {
2886
2933
  if (!value) return;
2887
2934
  const uris = Array.isArray(value) ? value : [value];
2935
+ if (uris.length === 0) return;
2888
2936
  for (const uri of uris) if (typeof uri !== "string" || !validateResourceUri(uri)) return;
2889
2937
  return value;
2890
2938
  }
@@ -3258,11 +3306,14 @@ var OAuthHelpersImpl = class {
3258
3306
  const withRedirect = (error) => {
3259
3307
  throw withAuthorizationRedirect(error, redirectUri, state || void 0, issuer);
3260
3308
  };
3309
+ const resourceWasProvided = resourceParam !== void 0;
3261
3310
  let resource = parseResourceParameter(resourceParam);
3262
- if (resourceParam && !resource) withRedirect(new AuthorizationError("invalid_request", { description: "The resource parameter must be a valid absolute URI without a fragment" }));
3311
+ if (resourceWasProvided && !resource) withRedirect(new AuthorizationError("invalid_target", { description: "The resource parameter must be a valid absolute URI without a fragment" }));
3263
3312
  const configuredResource = this.provider.options.resourceMetadata?.resource;
3264
- if (configuredResource && !isExactResource(resource, configuredResource)) withRedirect(new AuthorizationError("invalid_request", { description: `The resource parameter must exactly match ${configuredResource}` }));
3265
- resource ??= url.origin;
3313
+ if (configuredResource) {
3314
+ if (resourceWasProvided && !isExactResource(resource, configuredResource)) withRedirect(new AuthorizationError("invalid_target", { description: `The resource parameter must exactly match ${configuredResource}` }));
3315
+ resource = configuredResource;
3316
+ }
3266
3317
  try {
3267
3318
  validateAuthorizationResponseType(this.provider.serverCapabilities, responseType, clientInfo.responseTypes);
3268
3319
  validateAuthorizationPkce(this.provider.serverCapabilities, {
@@ -3298,7 +3349,8 @@ var OAuthHelpersImpl = class {
3298
3349
  * validating the metadata document fails.
3299
3350
  */
3300
3351
  async lookupClient(clientId) {
3301
- return await this.provider.getClient(this.env, clientId);
3352
+ const client = await this.provider.getClient(this.env, clientId);
3353
+ return client ? toPublicClientInfo(client) : null;
3302
3354
  }
3303
3355
  /**
3304
3356
  * Completes an authorization request by creating a grant and either:
@@ -3316,8 +3368,11 @@ var OAuthHelpersImpl = class {
3316
3368
  if (!clientInfo || !isValidRedirectUri(redirectUri, clientInfo.redirectUris)) throw new Error("Invalid redirect URI. The redirect URI provided does not match any registered URI for this client.");
3317
3369
  validateAuthorizationResponseType(this.provider.serverCapabilities, options.request.responseType, clientInfo.responseTypes);
3318
3370
  const configuredResource = this.provider.options.resourceMetadata?.resource;
3319
- if (configuredResource && !isExactResource(options.request.resource, configuredResource)) throw new Error(`The resource parameter must exactly match ${configuredResource}`);
3320
- const effectiveResource = options.request.resource ?? options.request.issuer;
3371
+ const resourceWasProvided = options.request.resource !== void 0;
3372
+ const parsedResource = parseResourceParameter(options.request.resource);
3373
+ if (resourceWasProvided && !parsedResource) throw new AuthorizationError("invalid_target", { description: "The resource parameter must be a valid absolute URI without a fragment" });
3374
+ if (configuredResource && resourceWasProvided && !isExactResource(parsedResource, configuredResource)) throw new AuthorizationError("invalid_target", { description: `The resource parameter must exactly match ${configuredResource}` });
3375
+ const effectiveResource = configuredResource ?? parsedResource;
3321
3376
  validateAuthorizationPkce(this.provider.serverCapabilities, options.request, clientInfo);
3322
3377
  let grantsToRevoke = [];
3323
3378
  if (options.revokeExistingGrants !== false) {
@@ -3423,6 +3478,7 @@ var OAuthHelpersImpl = class {
3423
3478
  */
3424
3479
  async createClient(clientInfo) {
3425
3480
  const clientId = generateRandomString(16);
3481
+ const authMethodWasExplicit = clientInfo.tokenEndpointAuthMethod !== void 0;
3426
3482
  const tokenEndpointAuthMethod = clientInfo.tokenEndpointAuthMethod || "client_secret_basic";
3427
3483
  const isPublicClient = tokenEndpointAuthMethod === "none";
3428
3484
  const newClient = {
@@ -3443,7 +3499,8 @@ var OAuthHelpersImpl = class {
3443
3499
  ],
3444
3500
  responseTypes: clientInfo.responseTypes || ["code"],
3445
3501
  registrationDate: Math.floor(Date.now() / 1e3),
3446
- tokenEndpointAuthMethod
3502
+ tokenEndpointAuthMethod,
3503
+ ...authMethodWasExplicit ? { authMethodExplicit: true } : {}
3447
3504
  };
3448
3505
  for (const uri of newClient.redirectUris) validateRedirectUriScheme(uri);
3449
3506
  let clientSecret;
@@ -3452,7 +3509,7 @@ var OAuthHelpersImpl = class {
3452
3509
  newClient.clientSecret = await hashSecret(clientSecret);
3453
3510
  }
3454
3511
  await this.env.OAUTH_KV.put(`client:${clientId}`, JSON.stringify(newClient));
3455
- const clientResponse = { ...newClient };
3512
+ const clientResponse = toPublicClientInfo(newClient);
3456
3513
  if (!isPublicClient && clientSecret) clientResponse.clientSecret = clientSecret;
3457
3514
  return clientResponse;
3458
3515
  }
@@ -3470,7 +3527,7 @@ var OAuthHelpersImpl = class {
3470
3527
  const promises = response.keys.map(async (key) => {
3471
3528
  const clientId = key.name.substring(7);
3472
3529
  const client = await this.provider.getClient(this.env, clientId);
3473
- if (client) clients.push(client);
3530
+ if (client) clients.push(toPublicClientInfo(client));
3474
3531
  });
3475
3532
  await Promise.all(promises);
3476
3533
  return {
@@ -3487,7 +3544,8 @@ var OAuthHelpersImpl = class {
3487
3544
  async updateClient(clientId, updates) {
3488
3545
  const client = await this.provider.getClient(this.env, clientId);
3489
3546
  if (!client) return null;
3490
- let authMethod = updates.tokenEndpointAuthMethod || client.tokenEndpointAuthMethod || "client_secret_basic";
3547
+ const authMethodWasExplicit = updates.tokenEndpointAuthMethod !== void 0;
3548
+ const authMethod = updates.tokenEndpointAuthMethod || client.tokenEndpointAuthMethod || "client_secret_basic";
3491
3549
  const isPublicClient = authMethod === "none";
3492
3550
  let secretToStore = client.clientSecret;
3493
3551
  let originalSecret = void 0;
@@ -3500,14 +3558,15 @@ var OAuthHelpersImpl = class {
3500
3558
  ...client,
3501
3559
  ...updates,
3502
3560
  clientId: client.clientId,
3503
- tokenEndpointAuthMethod: authMethod
3561
+ tokenEndpointAuthMethod: authMethod,
3562
+ authMethodExplicit: authMethodWasExplicit ? true : client.authMethodExplicit
3504
3563
  };
3505
3564
  if (!isPublicClient && secretToStore) updatedClient.clientSecret = secretToStore;
3506
3565
  else delete updatedClient.clientSecret;
3507
3566
  const clientKvOptions = {};
3508
3567
  if (this.provider.options.clientRegistrationTTL !== void 0) clientKvOptions.expirationTtl = this.provider.options.clientRegistrationTTL;
3509
3568
  await this.env.OAUTH_KV.put(`client:${clientId}`, JSON.stringify(updatedClient), clientKvOptions);
3510
- const response = { ...updatedClient };
3569
+ const response = toPublicClientInfo(updatedClient);
3511
3570
  if (!isPublicClient && originalSecret) response.clientSecret = originalSecret;
3512
3571
  return response;
3513
3572
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudflare/workers-oauth-provider",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "OAuth provider for Cloudflare Workers",
5
5
  "main": "dist/oauth-provider.js",
6
6
  "types": "dist/oauth-provider.d.ts",