@cloudflare/workers-oauth-provider 1.0.0 → 1.2.0

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.
@@ -1,5 +1,126 @@
1
1
  import { WorkerEntrypoint } from "cloudflare:workers";
2
2
 
3
+ //#region src/oauth-error.d.ts
4
+
5
+ /**
6
+ * The error your code throws to the library: from a `tokenExchangeCallback`, or from an
7
+ * `OAuthResourceServer`'s `validateToken`, to answer with a standard OAuth error response instead
8
+ * of a generic failure. Its own module so both hosts can use it without importing the package entry.
9
+ */
10
+ /**
11
+ * The internal reason behind an error response, forwarded to the `onError` hook and never
12
+ * placed on the wire. `category` is a stable kebab-case subsystem (`client-authentication`,
13
+ * `authorization-code-grant`, `refresh-token-grant`, `token-exchange-grant`,
14
+ * `token-endpoint-request`, `token-revocation`, `token-issuance`, `resource-indicator`,
15
+ * `client-registration`, `client-id-metadata-document`, `protected-resource`,
16
+ * `enterprise-managed-authorization`, `token-exchange-callback`); `reason` is a stable
17
+ * snake_case slug naming the exact check that failed. Treat both like enum members in semver.
18
+ * `detail` may carry structured context such as a caught error; it is never a secret.
19
+ */
20
+ interface OAuthErrorInternal {
21
+ /** Stable kebab-case subsystem that produced the error. */
22
+ category: string;
23
+ /** Stable snake_case slug naming the failed check, often more specific than the wire description. */
24
+ reason: string;
25
+ /** Optional structured context, e.g. the caught error or the offending parameter. */
26
+ detail?: unknown;
27
+ }
28
+ /**
29
+ * Options accepted by the {@link OAuthError} constructor.
30
+ */
31
+ interface OAuthErrorOptions {
32
+ /**
33
+ * Human-readable text returned in the `error_description` field.
34
+ */
35
+ description: string;
36
+ /**
37
+ * HTTP status code for the error response. Defaults to `400`.
38
+ */
39
+ statusCode?: number;
40
+ /**
41
+ * Additional response headers.
42
+ *
43
+ * For transient failures (e.g. upstream rate limits), set
44
+ * `Retry-After` here so well-behaved clients back off instead of
45
+ * retry-storming. Per RFC 7231 §7.1.3 the value may be either a
46
+ * number of seconds or an HTTP-date.
47
+ */
48
+ headers?: Record<string, string>;
49
+ /**
50
+ * Internal reason forwarded to the `onError` hook and never sent on the wire. The library
51
+ * sets it on every error it originates; a `tokenExchangeCallback` may set its own. An
52
+ * `OAuthError` thrown without one reaches `onError` as
53
+ * `{ category: 'token-exchange-callback', reason: 'callback_error', detail: error }`.
54
+ */
55
+ internal?: OAuthErrorInternal;
56
+ /**
57
+ * For `insufficient_scope` from an `OAuthResourceServer`'s `validateToken`: every scope the
58
+ * operation needs, named in the `403` challenge. Defaults to the resource's `requiredScopes`.
59
+ */
60
+ requiredScopes?: string[];
61
+ }
62
+ /**
63
+ * The OAuth error your code throws to the library, to answer with a standard OAuth error instead
64
+ * of a generic failure. It is honoured in two places:
65
+ *
66
+ * - **`tokenExchangeCallback`** (token endpoint): the response is `{ error, error_description }`
67
+ * with `statusCode` (default `400`) and `headers`. `invalid_grant` also revokes the grant.
68
+ * Anything else thrown is a `500`.
69
+ * - **`OAuthResourceServer`'s `validateToken`**: `invalid_token` is a `401` and
70
+ * `insufficient_scope` a `403`, each with a Bearer challenge (the latter naming
71
+ * `requiredScopes`); any other code keeps `statusCode` and `headers`, such as `429` with
72
+ * `Retry-After`. Anything else thrown is a `503`.
73
+ *
74
+ * Unexpected failures stay visible that way: the library doesn't catch everything and return 400.
75
+ * (`OAuthProvider`'s `resolveExternalToken` uses `ExternalTokenError` instead.)
76
+ *
77
+ * @example
78
+ * ```ts
79
+ * import { OAuthError } from '@cloudflare/workers-oauth-provider';
80
+ *
81
+ * tokenExchangeCallback: async (options) => {
82
+ * if (options.grantType === 'refresh_token') {
83
+ * // refreshUpstream() may throw OAuthError from any depth
84
+ * return { newProps: await refreshUpstream(options.props) };
85
+ * }
86
+ * }
87
+ *
88
+ * async function refreshUpstream(props) {
89
+ * const res = await fetch(...);
90
+ * if (res.status === 401) {
91
+ * // invalid_grant can never recover: the provider also revokes this grant and its tokens.
92
+ * throw new OAuthError('invalid_grant', { description: 'upstream refresh token is invalid' });
93
+ * }
94
+ * if (res.status === 429) {
95
+ * // Mirror upstream's Retry-After if present, otherwise pick a default.
96
+ * throw new OAuthError('temporarily_unavailable', {
97
+ * description: 'upstream rate limited',
98
+ * statusCode: 429,
99
+ * headers: { 'Retry-After': res.headers.get('retry-after') ?? '60' },
100
+ * });
101
+ * }
102
+ * return await res.json();
103
+ * }
104
+ * ```
105
+ */
106
+ declare class OAuthError extends Error {
107
+ /** OAuth 2.0 error code. */
108
+ readonly code: string;
109
+ /** Options controlling the OAuth error response. */
110
+ readonly options: OAuthErrorOptions & {
111
+ statusCode: number;
112
+ };
113
+ /** Human-readable description sent in the `error_description` field. */
114
+ readonly description: string;
115
+ /** HTTP status code for the error response. */
116
+ readonly statusCode: number;
117
+ /** Additional response headers. */
118
+ readonly headers?: Record<string, string>;
119
+ /** Scopes an `insufficient_scope` challenge names. */
120
+ readonly requiredScopes?: string[];
121
+ constructor(code: string, options: OAuthErrorOptions);
122
+ }
123
+ //#endregion
3
124
  //#region src/ema/result.d.ts
4
125
 
5
126
  /**
@@ -289,7 +410,7 @@ interface EmaOptions<Env = Cloudflare.Env> {
289
410
  * Set to `true` to also accept public clients on this grant — for example
290
411
  * clients registered via a Client ID Metadata Document (CIMD), which are
291
412
  * always public (`none`) and therefore cannot present a client secret. The
292
- * security trade-off is documented in the README: the trust then rests on
413
+ * security trade-off is documented in docs/advanced-configuration.md: the trust then rests on
293
414
  * the IdP-issued, signature-verified, short-lived, single-use ID-JAG
294
415
  * assertion (audience- and client-bound), together with the provider's
295
416
  * configured resource pinning, rather than on a separately presented client
@@ -310,6 +431,23 @@ interface AuthorizationErrorOptions {
310
431
  /** Authorization server issuer for RFC 9207 error responses. */
311
432
  issuer?: string;
312
433
  }
434
+ /**
435
+ * The OAuth error redirect back to a client (RFC 6749 §4.1.2.1): `error`, an optional
436
+ * `error_description`, the client's `state`, and `iss` (RFC 9207). Pass only a request the
437
+ * library validated, from `parseAuthRequest()`, `finishUpstream()` or `approveConsent()`,
438
+ * never one rebuilt from user input.
439
+ *
440
+ * ```ts
441
+ * if (new URL(req.url).searchParams.get('error')) {
442
+ * return Response.redirect(authorizationErrorRedirect(original, 'access_denied'), 302);
443
+ * }
444
+ * ```
445
+ */
446
+ declare function authorizationErrorRedirect(request: {
447
+ redirectUri: string;
448
+ state?: string;
449
+ issuer?: string;
450
+ }, code: AuthorizationErrorCode, description?: string): string;
313
451
  /**
314
452
  * Expected authorization-request validation failure. Absence of `redirectUri`
315
453
  * means a caller MUST render locally and MUST NOT redirect.
@@ -320,9 +458,13 @@ declare class AuthorizationError extends Error {
320
458
  readonly redirectUri?: string;
321
459
  readonly state?: string;
322
460
  readonly issuer?: string;
461
+ /**
462
+ * The ready-made error redirect back to the client, set only when a redirect is safe
463
+ * (`redirectUri` was validated). Without it, render the error locally.
464
+ */
465
+ readonly redirectTo?: string;
323
466
  constructor(code: AuthorizationErrorCode, options: AuthorizationErrorOptions);
324
467
  }
325
- declare function isValidOAuthScopeToken(scopeToken: string): boolean;
326
468
  //#endregion
327
469
  //#region src/oauth-resource-server.d.ts
328
470
  /** RFC 9728 metadata published by a standalone OAuth resource server. */
@@ -331,7 +473,10 @@ interface OAuthResourceMetadata {
331
473
  resource: string;
332
474
  /** Authorization server issuers that can issue tokens for this resource. */
333
475
  authorization_servers: string[];
334
- /** Minimal scopes used to access the protected resource. */
476
+ /**
477
+ * Minimal scopes used to access the protected resource.
478
+ * @deprecated Use `requiredScopes`, which this becomes on the wire. Setting both is refused.
479
+ */
335
480
  scopes_supported?: string[];
336
481
  /** Bearer-token presentation methods. This implementation supports only `header`. */
337
482
  bearer_methods_supported?: string[];
@@ -398,6 +543,18 @@ type OAuthResourceHandler<Env, Props> = {
398
543
  interface OAuthResourceServerOptions<Env = Cloudflare.Env, Props = unknown> {
399
544
  /** RFC 9728 metadata, including this server's one canonical resource. */
400
545
  resourceMetadata: OAuthResourceMetadata;
546
+ /**
547
+ * The scopes required for accessing this resource (MCP: the minimal set for basic use). Published
548
+ * as its protected resource metadata's `scopes_supported` and named in the `401` challenge, so
549
+ * clients request them first; not the authorization server's catalogue, `scopesSupported`.
550
+ * Operations that need more ask for it with `insufficientScope()` (step-up). `offline_access` is
551
+ * removed automatically. Leave unset when your consent page picks the scopes.
552
+ *
553
+ * Advertised, not enforced: the handler decides whether a token is sufficient (`ctx.auth.scope`),
554
+ * because only it knows the deployment's scope hierarchy (a broader scope implying a narrower
555
+ * one), which MCP requires servers to honour.
556
+ */
557
+ requiredScopes?: string[];
401
558
  /** Application handler for the canonical resource URL and its path descendants. */
402
559
  handler: OAuthResourceHandler<Env, Props>;
403
560
  /**
@@ -409,7 +566,15 @@ interface OAuthResourceServerOptions<Env = Cloudflare.Env, Props = unknown> {
409
566
  * `(env) => env.AUTH_SERVER.validateToken`
410
567
  * - The authorization server in this Worker:
411
568
  * `(env) => (resource, token) => authorizationServer.validateToken(resource, token, env)`
412
- * - Anything else, at your own risk: a function that validates the token for `resource`.
569
+ * - Anything else, at your own risk: a function that validates the token for `resource`,
570
+ * such as one that recognises an upstream API's credentials and validates them itself
571
+ * before falling back to the authorization server.
572
+ *
573
+ * Return `null` for a token that isn't valid here (a `401` challenge). Throw an `OAuthError`
574
+ * for a specific answer: `temporarily_unavailable` with `statusCode: 429` and a `Retry-After`
575
+ * header, `insufficient_scope` with `requiredScopes`, or `invalid_token` with a description.
576
+ * Anything else thrown is a validator failure (`503`). An `OAuthError` thrown in another
577
+ * Worker arrives over RPC as a plain `Error`, so throw it in this Worker's validator.
413
578
  */
414
579
  validateToken(env: Env, request: Request): OAuthResourceTokenValidator<Props>;
415
580
  }
@@ -439,18 +604,80 @@ declare class OAuthResourceServer<Env = Cloudflare.Env, Props = unknown> {
439
604
  fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response>;
440
605
  }
441
606
  //#endregion
442
- //#region src/oauth-resource.d.ts
443
- /** Validate an RFC 3986-safe HTTP(S) resource identifier for RFC 8707. */
444
- declare function validateResourceUri(uri: string): boolean;
607
+ //#region src/oauth-consent.d.ts
608
+ /** Remember an approval so the consent page can be skipped for requests it already covers. */
609
+ interface RememberConsentOptions {
610
+ /** HMAC key for the approvals cookie: at least 32 characters, from a Worker secret. */
611
+ secret: string;
612
+ /** How long an approval is remembered. Defaults to 30 days. */
613
+ maxAgeSeconds?: number;
614
+ /**
615
+ * The signed-in user, when you know them before consent (your own sign-in). The approval is then
616
+ * bound to them as well, so another account on the same browser is asked again. Without it an
617
+ * approval belongs to the browser, which suits a proxy server that only learns the user from the
618
+ * third party after consent.
619
+ */
620
+ subject?: string;
621
+ }
445
622
  /**
446
- * Whether a requested resource identifies a granted or configured resource.
447
- * Only ASCII case in the URI scheme and host is ignored, and an empty path is
448
- * equivalent to `/` (RFC 3986 §6.2.3), so a client that round-trips
449
- * `https://example.com` through a URL parser as `https://example.com/` still
450
- * names it. Port, path, query, a trailing slash after a path segment, user
451
- * information, and every other byte remain significant.
623
+ * What a consent page must show, per the MCP authorization spec and security best practices.
624
+ * Every string here may come from the client (dynamic registration or a CIMD document), so
625
+ * escape all of it before rendering.
452
626
  */
453
- declare function resourceMatches(requested: string, granted: string): boolean;
627
+ interface ConsentDescription {
628
+ clientId: string;
629
+ /** The client's name, or its client ID when it gave none. Self-asserted unless `clientDomain` is set. */
630
+ clientName: string;
631
+ /**
632
+ * For a Client ID Metadata Document client: the hostname of its HTTPS client ID, a domain the
633
+ * client controls. Show it prominently. Absent for registered clients, whose names are unverified.
634
+ */
635
+ clientDomain?: string;
636
+ clientUri?: string;
637
+ logoUri?: string;
638
+ /** Where the tokens will be sent. */
639
+ redirectUri: string;
640
+ /**
641
+ * The redirect URI's hostname, which the page MUST display. A native app's private-use URI
642
+ * (`com.example.app:/callback`) has no host, so it's the whole URI instead.
643
+ */
644
+ redirectHost: string;
645
+ /**
646
+ * The redirect goes to a local app (`localhost`, `127.0.0.0/8`, `::1`). The page SHOULD warn:
647
+ * any local process could be listening, whatever name the client claims.
648
+ */
649
+ redirectIsLoopback: boolean;
650
+ /** The scopes being requested. */
651
+ scope: string[];
652
+ }
653
+ /** A consent page to render: post `handle` back to {@link approveConsent}; send `headers` with the page. */
654
+ interface ConsentTransaction {
655
+ handle: string;
656
+ headers: Headers;
657
+ }
658
+ /** The approved authorization request, with the cookies to set on the next response. */
659
+ interface ApprovedConsent {
660
+ request: AuthRequest;
661
+ headers: Headers;
662
+ }
663
+ /** Where to send the browser after the user declined, with the cookies to set on that redirect. */
664
+ interface DeniedConsent {
665
+ request: AuthRequest;
666
+ /** The client's redirect URI with `error=access_denied`, its `state`, and `iss` (RFC 9207). */
667
+ redirectTo: string;
668
+ headers: Headers;
669
+ }
670
+ /** The `state` to send to the third-party provider, with the binding cookie to set on the redirect. */
671
+ interface UpstreamTransaction {
672
+ state: string;
673
+ headers: Headers;
674
+ }
675
+ /** The authorization request and data saved by `beginUpstream()`, recovered at the callback. */
676
+ interface ResumedUpstream<Data = unknown> {
677
+ request: AuthRequest;
678
+ data: Data;
679
+ headers: Headers;
680
+ }
454
681
  //#endregion
455
682
  //#region src/oauth-provider.d.ts
456
683
  /**
@@ -508,19 +735,19 @@ interface TokenExchangeCallbackResult {
508
735
  */
509
736
  accessTokenTTL?: number;
510
737
  /**
511
- * Override the default refresh token TTL (time-to-live) for this specific grant.
512
- * Value should be in seconds.
513
- * Note: This is only honored during authorization code exchange. Returning it during
514
- * refresh token exchange is rejected with `invalid_request`; to extend a grant on
515
- * refresh, return `refreshTokenIdleTTL`.
738
+ * Override the default refresh token TTL (time-to-live) for this specific grant, in seconds:
739
+ * `0` issues no refresh token, anything else must be an integer of at least 60. `undefined`
740
+ * keeps the provider's `refreshTokenTTL`; it never means "no expiry".
741
+ * Applies at authorization code exchange only and is ignored for other grant types, so one
742
+ * callback can return it for every grant; to extend a grant on refresh, return
743
+ * `refreshTokenIdleTTL`.
516
744
  */
517
745
  refreshTokenTTL?: number;
518
746
  /**
519
747
  * Idle lifetime for this grant's refresh token, in seconds from this refresh. The
520
- * grant then expires that long after this refresh unless it is refreshed again. Only
521
- * honored during refresh token exchange; returning it for another grant type is
522
- * rejected with `invalid_request`. Overrides the provider's `refreshTokenIdleTTL`
523
- * for this refresh.
748
+ * grant then expires that long after this refresh unless it is refreshed again. Applies to
749
+ * refresh token exchange only and is ignored for other grant types. Overrides the provider's
750
+ * `refreshTokenIdleTTL` for this refresh.
524
751
  *
525
752
  * Useful when the Worker is itself an OAuth client and has just rotated an upstream
526
753
  * refresh token: return the upstream lifetime and the grant lives exactly as long as
@@ -544,7 +771,7 @@ interface TokenExchangeCallbackResult {
544
771
  /**
545
772
  * Options for token exchange callback functions
546
773
  */
547
- interface TokenExchangeCallbackOptions {
774
+ interface TokenExchangeCallbackOptions<Env = Cloudflare.Env> {
548
775
  /**
549
776
  * The type of grant being processed.
550
777
  */
@@ -587,6 +814,11 @@ interface TokenExchangeCallbackOptions {
587
814
  * Application-specific properties currently associated with this grant
588
815
  */
589
816
  props: any;
817
+ /**
818
+ * The Worker's environment for this request, so the callback can reach secrets and bindings
819
+ * (an upstream provider's client secret, say) without the provider being rebuilt per request.
820
+ */
821
+ env: Env;
590
822
  }
591
823
  /**
592
824
  * Options for the client registration callback (RFC 7591).
@@ -684,7 +916,10 @@ interface OAuthProtectedResourceMetadata {
684
916
  * issuer.
685
917
  */
686
918
  authorization_servers?: string[];
687
- /** Minimal scopes required for basic protected-resource functionality. */
919
+ /**
920
+ * Minimal scopes required for basic protected-resource functionality.
921
+ * @deprecated Use `requiredScopes`, which this becomes on the wire. Setting both is refused.
922
+ */
688
923
  scopes_supported?: string[];
689
924
  /** Methods by which bearer tokens can be presented. Defaults to `["header"]`. */
690
925
  bearer_methods_supported?: string[];
@@ -756,6 +991,7 @@ interface OAuthProviderOptions<Env = Cloudflare.Env> {
756
991
  * Defaults to 30 days (2,592,000 seconds).
757
992
  * Set to 0 to disable refresh tokens entirely.
758
993
  * Set to `undefined` explicitly for refresh tokens that never expire.
994
+ * Anything else must be an integer of at least 60 seconds (Cloudflare KV's minimum expiration window).
759
995
  * For example: 3600 = 1 hour, 2592000 = 30 days
760
996
  */
761
997
  refreshTokenTTL?: number;
@@ -776,27 +1012,16 @@ interface OAuthProviderOptions<Env = Cloudflare.Env> {
776
1012
  * Defaults to 90 days (7,776,000 seconds).
777
1013
  * Clients created via the DCR endpoint will automatically expire after this duration.
778
1014
  * Clients created via `OAuthHelpers.createClient()` are not affected by this setting.
779
- * Set to `undefined` explicitly for clients that never expire.
1015
+ * Set to `undefined` explicitly for clients that never expire; otherwise an integer of at
1016
+ * least 60 seconds (Cloudflare KV's minimum expiration window).
780
1017
  */
781
1018
  clientRegistrationTTL?: number;
782
1019
  /**
783
1020
  * Scopes supported by the authorization server.
784
- * These are advertised only in authorization server metadata; configure
785
- * `resourceMetadata.scopes_supported` separately for protected resource requirements.
1021
+ * These are advertised only in authorization server metadata; configure a resource's
1022
+ * required scopes separately, with `requiredScopes`.
786
1023
  */
787
1024
  scopesSupported?: string[];
788
- /**
789
- * Controls whether the OAuth implicit flow is allowed.
790
- * This flow is discouraged in OAuth 2.1 due to security concerns.
791
- * Defaults to false.
792
- */
793
- allowImplicitFlow?: boolean;
794
- /**
795
- * Controls whether the legacy plain PKCE method is allowed.
796
- * Defaults to false so PKCE challenges use S256 exclusively.
797
- * Set to true only for compatibility with clients that cannot use S256.
798
- */
799
- allowPlainPKCE?: boolean;
800
1025
  /**
801
1026
  * Controls whether OAuth 2.0 Token Exchange (RFC 8693) is allowed.
802
1027
  * When false, the token exchange grant type will not be advertised in metadata
@@ -834,7 +1059,7 @@ interface OAuthProviderOptions<Env = Cloudflare.Env> {
834
1059
  * The callback can return new props values that will be stored with the token or grant.
835
1060
  * If the callback returns nothing or undefined for a props field, the original props will be used.
836
1061
  */
837
- tokenExchangeCallback?: (options: TokenExchangeCallbackOptions) => Promise<TokenExchangeCallbackResult | void> | TokenExchangeCallbackResult | void;
1062
+ tokenExchangeCallback?: (options: TokenExchangeCallbackOptions<Env>) => Promise<TokenExchangeCallbackResult | void> | TokenExchangeCallbackResult | void;
838
1063
  /**
839
1064
  * Optional callback called when a provided bearer credential was not found
840
1065
  * in the internal KV. It can validate an external OAuth access token, opaque
@@ -872,6 +1097,12 @@ interface OAuthProviderOptions<Env = Cloudflare.Env> {
872
1097
  internal: OAuthErrorInternal;
873
1098
  request?: Request;
874
1099
  }) => Response | void;
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.
1104
+ */
1105
+ allowPrivateUseRedirectUris?: boolean;
875
1106
  /**
876
1107
  * Explicitly enable Client ID Metadata Document (CIMD) support.
877
1108
  * When true, URL-formatted client_ids will be fetched as metadata documents.
@@ -879,11 +1110,29 @@ interface OAuthProviderOptions<Env = Cloudflare.Env> {
879
1110
  * Defaults to false.
880
1111
  */
881
1112
  clientIdMetadataDocumentEnabled?: boolean;
1113
+ /**
1114
+ * Prefix for the cookies the consent and upstream helpers set (`<prefix>consent`,
1115
+ * `<prefix>upstream`, `<prefix>approvals`). Must start with `__Host-`, which keeps them
1116
+ * `Secure`, host-only and `Path=/`. Defaults to `__Host-oauth-`.
1117
+ */
1118
+ cookiePrefix?: string;
882
1119
  /**
883
1120
  * Metadata for RFC 9728 OAuth 2.0 Protected Resource Metadata.
884
1121
  * Controls the response served at /.well-known/oauth-protected-resource.
885
1122
  */
886
1123
  resourceMetadata: OAuthProtectedResourceMetadata;
1124
+ /**
1125
+ * The scopes required for accessing this resource (MCP: the minimal set for basic use). Published
1126
+ * as its protected resource metadata's `scopes_supported` and named in the `401` challenge, so
1127
+ * clients request them first; not the authorization server's catalogue, `scopesSupported`.
1128
+ * Operations that need more ask for it with `insufficientScope()` (step-up). `offline_access` is
1129
+ * removed automatically. Leave unset when your consent page picks the scopes.
1130
+ *
1131
+ * Advertised, not enforced: the handler decides whether a token is sufficient (`ctx.auth.scope`),
1132
+ * because only it knows the deployment's scope hierarchy (a broader scope implying a narrower
1133
+ * one), which MCP requires servers to honour.
1134
+ */
1135
+ requiredScopes?: string[];
887
1136
  }
888
1137
  /**
889
1138
  * Functional authorization-server surface for the role-based API.
@@ -891,9 +1140,20 @@ interface OAuthProviderOptions<Env = Cloudflare.Env> {
891
1140
  * `getOAuthApi()` to parse and complete it; `fetch()` serves protocol-owned AS
892
1141
  * endpoints such as metadata, token, revocation, and optional registration.
893
1142
  */
894
- type OAuthAuthorizationServerOptions<Env = Cloudflare.Env> = Omit<OAuthProviderOptions<Env>, 'apiRoute' | 'apiHandler' | 'apiHandlers' | 'defaultHandler' | 'resourceMetadata' | 'resolveExternalToken'> & {
1143
+ type OAuthAuthorizationServerOptions<Env = Cloudflare.Env> = Omit<OAuthProviderOptions<Env>, 'apiRoute' | 'apiHandler' | 'apiHandlers' | 'defaultHandler' | 'authorizeEndpoint' | 'tokenEndpoint' | 'resourceMetadata' | 'requiredScopes' | 'resolveExternalToken'> & {
895
1144
  /** Canonical RFC 8414 issuer. */
896
1145
  issuer: string;
1146
+ /**
1147
+ * Your authorization endpoint, advertised in metadata. Route it before calling `fetch()`;
1148
+ * `parseAuthRequest()` rejects requests that arrive anywhere else, so a mismatch fails on the
1149
+ * first request. Defaults to `${issuer}/authorize`. A path is on the issuer's origin.
1150
+ */
1151
+ authorizeEndpoint?: string;
1152
+ /**
1153
+ * The token endpoint, which also handles revocation, served by `fetch()` and advertised in
1154
+ * metadata. Defaults to `${issuer}/oauth/token`. A path is on the issuer's origin.
1155
+ */
1156
+ tokenEndpoint?: string;
897
1157
  /**
898
1158
  * Canonical identifiers of every protected resource this authorization server issues
899
1159
  * tokens for, whether hosted in this Worker through `OAuthResourceServer` or by another
@@ -956,6 +1216,60 @@ interface OAuthHelpers {
956
1216
  completeAuthorization(options: CompleteAuthorizationOptions): Promise<{
957
1217
  redirectTo: string;
958
1218
  }>;
1219
+ /**
1220
+ * The facts a consent page must show for a request: the client's name (and, for a Client ID
1221
+ * Metadata Document client, its verified domain), the redirect URI's hostname, whether it goes
1222
+ * to a local app, and the scopes. Every string may come from the client: escape it.
1223
+ * @throws CimdFetchError when the client's metadata document cannot be resolved
1224
+ */
1225
+ describeConsent(authRequest: AuthRequest): Promise<ConsentDescription>;
1226
+ /**
1227
+ * Whether an approval remembered by `approveConsent(…, { remember })` covers this request: the
1228
+ * same client, redirect URI and resource, asking for a subset of the approved scopes. Pass the
1229
+ * same `secret`, and the same `subject` if the approval was bound to a user. Don't call it to ask
1230
+ * on every authorization.
1231
+ */
1232
+ isConsentRemembered(request: Request, authRequest: AuthRequest, remember: Pick<RememberConsentOptions, 'secret' | 'subject'>): Promise<boolean>;
1233
+ /**
1234
+ * Start a consent page for a request. Render your page with `handle` in the form and send
1235
+ * `headers` with it: they bind the handle to this browser and forbid framing the page.
1236
+ */
1237
+ beginConsent(authRequest: AuthRequest): Promise<ConsentTransaction>;
1238
+ /**
1239
+ * Accept the consent form. `handle` is the value your form posted; the browser's binding cookie
1240
+ * must match it, and it works once. `scope` is what the user approved: fewer or more than the
1241
+ * client requested, each one in `scopesSupported`. `remember` stores the approval in a signed
1242
+ * cookie for `isConsentRemembered()`. Send the returned `headers` on the next response.
1243
+ * @throws AuthorizationError when the handle is missing, unbound, expired, or already used
1244
+ */
1245
+ approveConsent(request: Request, handle: string, options?: {
1246
+ scope?: string[];
1247
+ remember?: RememberConsentOptions;
1248
+ }): Promise<ApprovedConsent>;
1249
+ /**
1250
+ * Decline the consent form: consumes the handle like `approveConsent()` and returns the redirect
1251
+ * back to the client with `error=access_denied`, its `state` and `iss`. Send `headers` with the
1252
+ * redirect (they include `Location`).
1253
+ * @throws AuthorizationError when the handle is missing, unbound, expired, or already used
1254
+ */
1255
+ denyConsent(request: Request, handle: string, options?: {
1256
+ description?: string;
1257
+ }): Promise<DeniedConsent>;
1258
+ /**
1259
+ * Save an approved request before redirecting to a third-party provider, and get the `state`
1260
+ * to send it. Call only after consent. `data` is returned at the callback, e.g. a PKCE verifier.
1261
+ * Pass `headers` to add the binding cookie to headers you are already sending.
1262
+ */
1263
+ beginUpstream(authRequest: AuthRequest, options?: {
1264
+ data?: unknown;
1265
+ headers?: Headers;
1266
+ }): Promise<UpstreamTransaction>;
1267
+ /**
1268
+ * At the third-party provider's callback, recover the approved request and your `data` from the
1269
+ * `state` parameter. The browser's binding cookie must match, and it works once.
1270
+ * @throws AuthorizationError when `state` is missing, unbound, expired, or already used
1271
+ */
1272
+ finishUpstream<Data = unknown>(request: Request): Promise<ResumedUpstream<Data>>;
959
1273
  /**
960
1274
  * Creates a new OAuth client
961
1275
  * @param clientInfo - Partial client information to create the client with
@@ -969,16 +1283,28 @@ interface OAuthHelpers {
969
1283
  */
970
1284
  listClients(options?: ListOptions): Promise<ListResult<ClientInfo>>;
971
1285
  /**
972
- * Updates an existing OAuth client
1286
+ * Updates an existing OAuth client. Redirect URIs, grant types and response types are held to
1287
+ * the same rules as registration. Client ID Metadata Document clients can't be updated here:
1288
+ * their document owns their metadata.
973
1289
  * @param clientId - The ID of the client to update
974
1290
  * @param updates - Partial client information with fields to update
975
1291
  * @returns A Promise resolving to the updated client info, or null if not found
1292
+ * @throws Error when an update breaks the redirect URI policy or names an unsupported grant or response type
1293
+ * @throws TypeError for a Client ID Metadata Document client while CIMD is enabled
976
1294
  */
977
1295
  updateClient(clientId: string, updates: Partial<ClientInfo>): Promise<ClientInfo | null>;
978
1296
  /**
979
- * Deletes an OAuth client
1297
+ * Deletes an OAuth client, then revokes its grants and tokens across all users. The client
1298
+ * can no longer authenticate, authorize or refresh as soon as the call starts. Access tokens
1299
+ * it already holds stay valid until revocation reaches them or they expire (`accessTokenTTL`):
1300
+ * validating an access token reads only the token record, never the client. If revocation is
1301
+ * cut short (for example by a subrequest limit on a very large namespace), the call throws;
1302
+ * call it again to continue, and `purgeExpiredData()` also removes the leftovers, which are
1303
+ * orphaned grants by then.
1304
+ * For a Client ID Metadata Document client there is no stored record: this revokes its grants,
1305
+ * and the client can authorize again as long as its document resolves.
980
1306
  * @param clientId - The ID of the client to delete
981
- * @returns A Promise resolving when the deletion is confirmed.
1307
+ * @returns A Promise resolving when the client and all its grants are gone.
982
1308
  */
983
1309
  deleteClient(clientId: string): Promise<void>;
984
1310
  /**
@@ -1182,7 +1508,9 @@ interface CompleteAuthorizationOptions {
1182
1508
  */
1183
1509
  request: AuthRequest;
1184
1510
  /**
1185
- * Identifier for the user granting the authorization
1511
+ * Identifier for the user granting the authorization. Must be non-empty and must not contain
1512
+ * `:`, which separates the parts of issued tokens and storage keys; encode namespaced or
1513
+ * composite IDs first (for example `encodeURIComponent('tenant:user')`).
1186
1514
  */
1187
1515
  userId: string;
1188
1516
  /**
@@ -1205,15 +1533,6 @@ interface CompleteAuthorizationOptions {
1205
1533
  * Set to false to allow multiple concurrent grants per user+client.
1206
1534
  */
1207
1535
  revokeExistingGrants?: boolean;
1208
- /**
1209
- * How many grants written by a version before 1.0 are read at once while
1210
- * revoking existing grants. Grants written by 1.0 or later carry their client,
1211
- * resource and redirect URI as KV key metadata and are matched from `list()`
1212
- * without being read, so this only bounds the fan-out for older grants. Only
1213
- * used when revokeExistingGrants is not false. Must be a positive integer;
1214
- * values above 1000 are clamped. Defaults to 50.
1215
- */
1216
- revokeExistingGrantsBatchSize?: number;
1217
1536
  }
1218
1537
  /**
1219
1538
  * Authorization grant record
@@ -1370,7 +1689,8 @@ interface Token extends TokenBase {
1370
1689
  */
1371
1690
  grant: {
1372
1691
  /**
1373
- * Client that received this grant
1692
+ * Client this token was issued to: the grant's client, or, for a token from an allowed
1693
+ * cross-client token exchange, the client that requested it (RFC 8693).
1374
1694
  */
1375
1695
  clientId: string;
1376
1696
  /**
@@ -1393,7 +1713,8 @@ interface TokenSummary<T = any> extends TokenBase {
1393
1713
  */
1394
1714
  grant: {
1395
1715
  /**
1396
- * Client that received this grant
1716
+ * Client this token was issued to: the grant's client, or, for a token from an allowed
1717
+ * cross-client token exchange, the client that requested it (RFC 8693).
1397
1718
  */
1398
1719
  clientId: string;
1399
1720
  /**
@@ -1465,6 +1786,12 @@ interface PurgeOptions {
1465
1786
  * Defaults to true.
1466
1787
  */
1467
1788
  purgeOrphanedTokens?: boolean;
1789
+ /**
1790
+ * Where to resume: the `cursor` from the previous invocation's result. Omit it to start
1791
+ * a new sweep. Persist the returned cursor between invocations (for example in KV), or
1792
+ * every invocation re-checks the same first `batchSize` records.
1793
+ */
1794
+ cursor?: string;
1468
1795
  }
1469
1796
  /**
1470
1797
  * Result of a purgeExpiredData garbage collection invocation
@@ -1478,8 +1805,10 @@ interface PurgeResult {
1478
1805
  tokensChecked: number;
1479
1806
  /** Number of token records purged (orphaned) */
1480
1807
  tokensPurged: number;
1481
- /** True if the full key space was scanned in this invocation (both grants and tokens) */
1808
+ /** True once the sweep has covered both key spaces; there is no `cursor` then. */
1482
1809
  done: boolean;
1810
+ /** Pass as `PurgeOptions.cursor` to the next invocation to continue this sweep. Absent when `done`. */
1811
+ cursor?: string;
1483
1812
  }
1484
1813
  /**
1485
1814
  * Public representation of a grant, with sensitive data removed
@@ -1603,107 +1932,6 @@ declare function getOAuthApi<Env = Cloudflare.Env>(options: OAuthProviderOptions
1603
1932
  * Error class for OAuth operations
1604
1933
  * Carries OAuth error code and description for proper error responses
1605
1934
  */
1606
- /**
1607
- * The internal reason behind an error response, forwarded to the `onError` hook and never
1608
- * placed on the wire. `category` is a stable kebab-case subsystem (`client-authentication`,
1609
- * `authorization-code-grant`, `refresh-token-grant`, `token-exchange-grant`,
1610
- * `token-endpoint-request`, `token-revocation`, `token-issuance`, `resource-indicator`,
1611
- * `client-registration`, `client-id-metadata-document`, `protected-resource`,
1612
- * `enterprise-managed-authorization`, `token-exchange-callback`); `reason` is a stable
1613
- * snake_case slug naming the exact check that failed. Treat both like enum members in semver.
1614
- * `detail` may carry structured context such as a caught error; it is never a secret.
1615
- */
1616
- interface OAuthErrorInternal {
1617
- /** Stable kebab-case subsystem that produced the error. */
1618
- category: string;
1619
- /** Stable snake_case slug naming the failed check, often more specific than the wire description. */
1620
- reason: string;
1621
- /** Optional structured context, e.g. the caught error or the offending parameter. */
1622
- detail?: unknown;
1623
- }
1624
- /**
1625
- * Options accepted by the {@link OAuthError} constructor.
1626
- */
1627
- interface OAuthErrorOptions {
1628
- /**
1629
- * Human-readable text returned in the `error_description` field.
1630
- */
1631
- description: string;
1632
- /**
1633
- * HTTP status code for the error response. Defaults to `400`.
1634
- */
1635
- statusCode?: number;
1636
- /**
1637
- * Additional response headers.
1638
- *
1639
- * For transient failures (e.g. upstream rate limits), set
1640
- * `Retry-After` here so well-behaved clients back off instead of
1641
- * retry-storming. Per RFC 7231 §7.1.3 the value may be either a
1642
- * number of seconds or an HTTP-date.
1643
- */
1644
- headers?: Record<string, string>;
1645
- /**
1646
- * Internal reason forwarded to the `onError` hook and never sent on the wire. The library
1647
- * sets it on every error it originates; a `tokenExchangeCallback` may set its own. An
1648
- * `OAuthError` thrown without one reaches `onError` as
1649
- * `{ category: 'token-exchange-callback', reason: 'callback_error', detail: error }`.
1650
- */
1651
- internal?: OAuthErrorInternal;
1652
- }
1653
- /**
1654
- * Structured OAuth 2.0 token-endpoint error.
1655
- *
1656
- * Throw from a `tokenExchangeCallback` or any code it calls to surface a
1657
- * standard OAuth token response (`{ error, error_description }`) instead of a
1658
- * generic `500 Internal Server Error`.
1659
- *
1660
- * Anything thrown that is **not** an `OAuthError` continues to surface as
1661
- * a 500 so unexpected failures remain visible — the provider does not
1662
- * catch-everything-and-return-400.
1663
- *
1664
- * @example
1665
- * ```ts
1666
- * import { OAuthError } from '@cloudflare/workers-oauth-provider';
1667
- *
1668
- * tokenExchangeCallback: async (options) => {
1669
- * if (options.grantType === 'refresh_token') {
1670
- * // refreshUpstream() may throw OAuthError from any depth
1671
- * return { newProps: await refreshUpstream(options.props) };
1672
- * }
1673
- * }
1674
- *
1675
- * async function refreshUpstream(props) {
1676
- * const res = await fetch(...);
1677
- * if (res.status === 401) {
1678
- * throw new OAuthError('invalid_grant', { description: 'upstream refresh token is invalid' });
1679
- * }
1680
- * if (res.status === 429) {
1681
- * // Mirror upstream's Retry-After if present, otherwise pick a default.
1682
- * throw new OAuthError('temporarily_unavailable', {
1683
- * description: 'upstream rate limited',
1684
- * statusCode: 429,
1685
- * headers: { 'Retry-After': res.headers.get('retry-after') ?? '60' },
1686
- * });
1687
- * }
1688
- * return await res.json();
1689
- * }
1690
- * ```
1691
- */
1692
- declare class OAuthError extends Error {
1693
- /** OAuth 2.0 error code. */
1694
- readonly code: string;
1695
- /** Options controlling the OAuth error response. */
1696
- readonly options: OAuthErrorOptions & {
1697
- statusCode: number;
1698
- };
1699
- /** Human-readable description sent in the `error_description` field. */
1700
- readonly description: string;
1701
- /** HTTP status code for the error response. */
1702
- readonly statusCode: number;
1703
- /** Additional response headers. */
1704
- readonly headers?: Record<string, string>;
1705
- constructor(code: string, options: OAuthErrorOptions);
1706
- }
1707
1935
  /** Options accepted by the {@link ExternalTokenError} constructor. */
1708
1936
  interface ExternalTokenErrorOptions {
1709
1937
  /**
@@ -1780,20 +2008,5 @@ declare class CimdFetchError extends Error {
1780
2008
  */
1781
2009
  constructor(metadataUrl: string, cause: unknown);
1782
2010
  }
1783
- /**
1784
- * Decodes a base64url-encoded string to bytes.
1785
- */
1786
- declare function base64UrlToBytes(base64Url: string): Uint8Array;
1787
- /**
1788
- * Parses a base64url-encoded JWT JSON part into an object.
1789
- */
1790
- declare function parseJwtJsonPart(encoded: string): Record<string, unknown>;
1791
- /**
1792
- * Gets WebCrypto import and verify parameters for supported JOSE algorithms.
1793
- */
1794
- declare function getJwtCryptoAlgorithms(alg: string): {
1795
- importAlgorithm: Parameters<SubtleCrypto['importKey']>[2];
1796
- verifyAlgorithm: Parameters<SubtleCrypto['verify']>[0];
1797
- };
1798
2011
  //#endregion
1799
- export { AuthRequest, AuthorizationError, type AuthorizationErrorCode, type AuthorizationErrorOptions, AuthorizationServerBinding, CimdFetchError, ClientInfo, ClientRegistrationCallbackOptions, ClientRegistrationCallbackResult, CompleteAuthorizationOptions, type EmaClaimsMapper, type EmaClaimsMapperInput, type EmaClaimsMapperResult, type EmaIdJagClaims, type EmaOptions, type EmaTrustedIssuer, type EmaTrustedIssuerResolver, type EmaTrustedIssuerResolverInput, type EmaValidationError, ExchangeTokenOptions, ExternalTokenError, ExternalTokenErrorOptions, Grant, GrantSummary, GrantType, ListOptions, ListResult, OAuthAuthorizationServer, OAuthAuthorizationServerOptions, OAuthError, OAuthErrorInternal, OAuthErrorOptions, OAuthHelpers, OAuthProtectedResourceMetadata, OAuthProvider, OAuthProvider as default, OAuthProviderOptions, OAuthResourceAuth, OAuthResourceContext, OAuthResourceHandler, OAuthResourceMetadata, OAuthResourceServer, OAuthResourceServerOptions, OAuthResourceTokenValidation, OAuthResourceTokenValidator, OAuthTokenErrorCode, PurgeOptions, PurgeResult, ResolveExternalTokenInput, ResolveExternalTokenResult, Token, TokenBase, TokenExchangeCallbackOptions, TokenExchangeCallbackResult, TokenSummary, ValidatedAccessToken, base64UrlToBytes, getJwtCryptoAlgorithms, getOAuthApi, insufficientScope, isValidOAuthScopeToken, parseJwtJsonPart, resourceMatches, validateResourceUri };
2012
+ export { type ApprovedConsent, AuthRequest, AuthorizationError, type AuthorizationErrorCode, type AuthorizationErrorOptions, AuthorizationServerBinding, CimdFetchError, ClientInfo, ClientRegistrationCallbackOptions, ClientRegistrationCallbackResult, CompleteAuthorizationOptions, type ConsentDescription, type ConsentTransaction, type DeniedConsent, type EmaClaimsMapper, type EmaClaimsMapperInput, type EmaClaimsMapperResult, type EmaIdJagClaims, type EmaOptions, type EmaTrustedIssuer, type EmaTrustedIssuerResolver, type EmaTrustedIssuerResolverInput, type EmaValidationError, ExchangeTokenOptions, ExternalTokenError, ExternalTokenErrorOptions, Grant, GrantSummary, GrantType, ListOptions, ListResult, OAuthAuthorizationServer, OAuthAuthorizationServerOptions, OAuthError, type OAuthErrorInternal, type OAuthErrorOptions, OAuthHelpers, OAuthProtectedResourceMetadata, OAuthProvider, OAuthProvider as default, OAuthProviderOptions, OAuthResourceAuth, OAuthResourceContext, OAuthResourceHandler, OAuthResourceMetadata, OAuthResourceServer, OAuthResourceServerOptions, OAuthResourceTokenValidation, OAuthResourceTokenValidator, OAuthTokenErrorCode, PurgeOptions, PurgeResult, type RememberConsentOptions, ResolveExternalTokenInput, ResolveExternalTokenResult, type ResumedUpstream, Token, TokenBase, TokenExchangeCallbackOptions, TokenExchangeCallbackResult, TokenSummary, type UpstreamTransaction, ValidatedAccessToken, authorizationErrorRedirect, getOAuthApi, insufficientScope };