@cloudflare/workers-oauth-provider 1.2.0 → 1.2.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.
@@ -1098,9 +1098,11 @@ interface OAuthProviderOptions<Env = Cloudflare.Env> {
1098
1098
  request?: Request;
1099
1099
  }) => Response | void;
1100
1100
  /**
1101
- * Accept RFC 8252 private-use URI scheme redirect URIs (for example `com.example.app:/oauth`)
1102
- * for native apps. By default redirect URIs must use `https`, or `http` on a loopback host, as
1103
- * MCP and OAuth 2.1 require; remote `http` is never accepted. Leave this off for MCP servers.
1101
+ * Let authorization requests use RFC 8252 private-use URI scheme redirect URIs (for example
1102
+ * `com.example.app:/oauth`), for native apps. By default a request's redirect URI must use
1103
+ * `https`, or `http` on a loopback host, as MCP and OAuth 2.1 require; remote `http` is never
1104
+ * accepted. Without this option a client can still register private-use URIs next to a compliant
1105
+ * one (Cursor does), but no request can use them.
1104
1106
  */
1105
1107
  allowPrivateUseRedirectUris?: boolean;
1106
1108
  /**
@@ -516,14 +516,12 @@ function validateRedirectUriSafety(redirectUri) {
516
516
  if (dangerousSchemes.includes(scheme)) throw new Error("Invalid redirect URI");
517
517
  }
518
518
  /**
519
- * Validates a redirect URI against the MCP / OAuth 2.1 redirect policy: `https`, or `http` on a
520
- * loopback host (`localhost`, `127.0.0.0/8`, `::1`), with no userinfo and no fragment (not even an
521
- * empty `#`). RFC 8252 private-use schemes are accepted only when the server enables
522
- * `allowPrivateUseRedirectUris`; remote `http` never is. The {@link validateRedirectUriSafety}
523
- * checks always apply.
524
- * @throws Error describing why the redirect URI is not acceptable
519
+ * The checks every redirect URI a client registers passes, whether or not this server would send a
520
+ * code to it: the {@link validateRedirectUriSafety} checks, a parseable URI, and no fragment (not
521
+ * even an empty `#`) or userinfo.
522
+ * @throws Error when the redirect URI can't be registered at all
525
523
  */
526
- function validateRedirectUri(redirectUri, server) {
524
+ function validateListedRedirectUri(redirectUri) {
527
525
  validateRedirectUriSafety(redirectUri);
528
526
  const normalized = redirectUri.trim();
529
527
  let url;
@@ -533,6 +531,18 @@ function validateRedirectUri(redirectUri, server) {
533
531
  throw new Error("Invalid redirect URI");
534
532
  }
535
533
  if (normalized.includes("#") || url.username || url.password) throw new Error("Invalid redirect URI");
534
+ return url;
535
+ }
536
+ /**
537
+ * Validates a redirect URI against the MCP / OAuth 2.1 redirect policy: `https`, or `http` on a
538
+ * loopback host (`localhost`, `127.0.0.0/8`, `::1`), with no userinfo and no fragment (not even an
539
+ * empty `#`). RFC 8252 private-use schemes are accepted only when the server enables
540
+ * `allowPrivateUseRedirectUris`; remote `http` never is. The {@link validateRedirectUriSafety}
541
+ * checks always apply.
542
+ * @throws Error describing why the redirect URI is not acceptable
543
+ */
544
+ function validateRedirectUri(redirectUri, server) {
545
+ const url = validateListedRedirectUri(redirectUri);
536
546
  if (url.protocol === "https:") return;
537
547
  if (url.protocol === "http:") {
538
548
  if (isLoopbackHostname(url.hostname)) return;
@@ -540,6 +550,27 @@ function validateRedirectUri(redirectUri, server) {
540
550
  }
541
551
  if (!server.allowPrivateUseRedirectUris) throw new Error("Redirect URI must use https, or http on a loopback host");
542
552
  }
553
+ /**
554
+ * Validates the redirect URIs a client registers (dynamic registration, `createClient()`,
555
+ * `updateClient()`). A client may list callbacks for surfaces this server won't send codes to:
556
+ * Cursor lists `cursor://anysphere.cursor-mcp/oauth/callback` next to its https and loopback
557
+ * callbacks. So each URI is held to {@link validateListedRedirectUri}, and at least one must satisfy
558
+ * {@link validateRedirectUri} so the client can sign in. The URI a request uses is held to the full
559
+ * policy by `parseAuthRequest()` and `completeAuthorization()`.
560
+ * @throws Error when a URI can't be registered, or none of them could ever receive a code
561
+ */
562
+ function validateRegisteredRedirectUris(redirectUris, server) {
563
+ if (!redirectUris || redirectUris.length === 0) throw new Error("redirect_uris is required and must not be empty");
564
+ for (const redirectUri of redirectUris) validateListedRedirectUri(redirectUri);
565
+ let firstRefusal;
566
+ for (const redirectUri of redirectUris) try {
567
+ validateRedirectUri(redirectUri, server);
568
+ return redirectUris;
569
+ } catch (error) {
570
+ firstRefusal ??= error;
571
+ }
572
+ throw firstRefusal instanceof Error ? firstRefusal : /* @__PURE__ */ new Error("Invalid redirect URI");
573
+ }
543
574
  function requireValidRedirectUris(redirectUris, validate) {
544
575
  if (!redirectUris || redirectUris.length === 0) throw new Error("redirect_uris is required and must not be empty");
545
576
  for (const redirectUri of redirectUris) validate(redirectUri);
@@ -560,7 +591,7 @@ function resolveDynamicClientRegistrationMetadata(raw, server) {
560
591
  });
561
592
  return {
562
593
  ...pickDisplayMetadata(metadata),
563
- redirectUris: requireValidRedirectUris(metadata.redirectUris, (uri) => validateRedirectUri(uri, server)),
594
+ redirectUris: validateRegisteredRedirectUris(metadata.redirectUris, server),
564
595
  ...capabilities,
565
596
  authMethodExplicit: metadata.tokenEndpointAuthMethod !== void 0 || metadata.tokenEndpointAuthMethodsSupported !== void 0
566
597
  };
@@ -5309,7 +5340,7 @@ var OAuthHelpersImpl = class {
5309
5340
  tokenEndpointAuthMethod,
5310
5341
  ...authMethodWasExplicit ? { authMethodExplicit: true } : {}
5311
5342
  };
5312
- for (const uri of newClient.redirectUris) validateRedirectUri(uri, this.provider.serverCapabilities);
5343
+ validateRegisteredRedirectUris(newClient.redirectUris, this.provider.serverCapabilities);
5313
5344
  validateClientCapabilities(this.provider.serverCapabilities, {
5314
5345
  grantTypes: newClient.grantTypes,
5315
5346
  responseTypes: newClient.responseTypes,
@@ -5359,7 +5390,7 @@ var OAuthHelpersImpl = class {
5359
5390
  if (!client) return null;
5360
5391
  if (updates.redirectUris !== void 0) {
5361
5392
  if (!Array.isArray(updates.redirectUris) || updates.redirectUris.length === 0) throw new Error("redirectUris must not be empty");
5362
- for (const uri of updates.redirectUris) validateRedirectUri(uri, this.provider.serverCapabilities);
5393
+ validateRegisteredRedirectUris(updates.redirectUris, this.provider.serverCapabilities);
5363
5394
  }
5364
5395
  const authMethodWasExplicit = updates.tokenEndpointAuthMethod !== void 0;
5365
5396
  const authMethod = updates.tokenEndpointAuthMethod || client.tokenEndpointAuthMethod || "client_secret_basic";
@@ -71,7 +71,7 @@ CIMD validation follows [draft-ietf-oauth-client-id-metadata-document-00](https:
71
71
 
72
72
  - An HTTPS Client Identifier URL with a path component and no userinfo, fragment, or dot path segments.
73
73
  - A document `client_id` exactly matching its URL.
74
- - Non-empty `client_name` and `redirect_uris` fields, as MCP requires. Redirect URIs must use `https`, or `http` on a loopback host (`localhost`, `127.0.0.0/8`, `::1`), with no userinfo or fragment. The same rule applies to dynamic registration, `createClient()`, `updateClient()`, and every authorization request, so older clients are held to it too. A CIMD document may also list redirect URIs the rule refuses, such as a desktop app's private-use scheme, since every server the client uses reads the same document: those are refused only when a request uses one, and the document's https callback keeps working. Dangerous schemes such as `javascript:` still reject the whole document. Native apps using RFC 8252 private-use schemes (`com.example.app:/cb`) need `allowPrivateUseRedirectUris: true`; remote `http` is never accepted.
74
+ - Non-empty `client_name` and `redirect_uris` fields, as MCP requires. Redirect URIs must use `https`, or `http` on a loopback host (`localhost`, `127.0.0.0/8`, `::1`), with no userinfo or fragment. Every authorization request is held to it, so older clients are too. A client may register, or list in its CIMD document, other redirect URIs next to a compliant one, because it signs in from several places: Cursor registers `cursor://anysphere.cursor-mcp/oauth/callback` beside its https and loopback callbacks. Those are stored, and refused whenever a request uses one. Dynamic registration, `createClient()` and `updateClient()` need at least one compliant redirect URI, and refuse any with a dangerous scheme such as `javascript:`, a fragment or userinfo; a dangerous scheme also rejects a whole CIMD document. Native apps using RFC 8252 private-use schemes (`com.example.app:/cb`) need `allowPrivateUseRedirectUris: true`; remote `http` is never accepted.
75
75
  - Exact authorization-request redirect URI validation, with RFC 8252 loopback port handling.
76
76
  - A 5 KB response size limit and a 10 second timeout covering both headers and body.
77
77
  - Valid UTF-8 JSON object syntax and safe URI schemes for client metadata fields.
@@ -103,7 +103,7 @@ If you have your own clients on either flow, move them to the authorization code
103
103
 
104
104
  ### Redirect URIs must be HTTPS or loopback HTTP (1.2)
105
105
 
106
- Redirect URIs must use `https`, or `http` on `localhost`, `127.0.0.0/8` or `::1`, with no userinfo or fragment. That's what MCP and OAuth 2.1 require. The rule applies at dynamic registration, in `createClient()` and `updateClient()`, and on every authorization request. A CIMD document may list other redirect URIs too, since it's shared by every server the client uses; a request that uses one of them is refused, and the rest of the document keeps working.
106
+ Redirect URIs must use `https`, or `http` on `localhost`, `127.0.0.0/8` or `::1`, with no userinfo or fragment. That's what MCP and OAuth 2.1 require. Every authorization request is held to it. A registration (dynamic registration, `createClient()`, `updateClient()`) needs at least one redirect URI that follows it, and may list others next to it, as Cursor lists `cursor://…` beside its https and loopback callbacks; so may a CIMD document. A request that uses one of those is refused. Registrations still refuse dangerous schemes such as `javascript:`, fragments and userinfo.
107
107
 
108
108
  Because it applies at authorization too, clients registered before 1.2 are held to it. A client with a remote `http` redirect URI gets a locally rendered `invalid_request` ("Invalid redirect URI") and is never redirected. It has to register a compliant URI.
109
109
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudflare/workers-oauth-provider",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "OAuth provider for Cloudflare Workers",
5
5
  "main": "dist/oauth-provider.js",
6
6
  "types": "dist/oauth-provider.d.ts",