@cloudflare/workers-oauth-provider 0.10.4 → 1.1.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.
@@ -324,6 +324,178 @@ declare class AuthorizationError extends Error {
324
324
  }
325
325
  declare function isValidOAuthScopeToken(scopeToken: string): boolean;
326
326
  //#endregion
327
+ //#region src/oauth-resource-server.d.ts
328
+ /** RFC 9728 metadata published by a standalone OAuth resource server. */
329
+ interface OAuthResourceMetadata {
330
+ /** The one canonical HTTPS identifier for this protected resource. */
331
+ resource: string;
332
+ /** Authorization server issuers that can issue tokens for this resource. */
333
+ authorization_servers: string[];
334
+ /** Minimal scopes used to access the protected resource. */
335
+ scopes_supported?: string[];
336
+ /** Bearer-token presentation methods. This implementation supports only `header`. */
337
+ bearer_methods_supported?: string[];
338
+ /** Human-readable protected-resource name. */
339
+ resource_name?: string;
340
+ }
341
+ /** Successful result returned by the application's token validator. */
342
+ interface OAuthResourceTokenValidation<Props> {
343
+ /** Application data exposed to the protected handler as `ctx.props`. */
344
+ props: Props;
345
+ /** Canonical audience to which the token is bound. */
346
+ audience: string;
347
+ /** Optional absolute expiry as seconds since the Unix epoch. */
348
+ expiresAt?: number;
349
+ /**
350
+ * Scopes the token carries. `OAuthAuthorizationServer.validateToken()` always reports them;
351
+ * a handler needs them to answer with {@link insufficientScope}.
352
+ */
353
+ scope?: string[];
354
+ /** Subject the token was issued for. */
355
+ userId?: string;
356
+ /** Client the token was issued to. */
357
+ clientId?: string;
358
+ }
359
+ /**
360
+ * What the host verified about the bearer token before calling the handler, exposed as
361
+ * `ctx.auth` beside the application's `ctx.props`. Both hosts set it: `OAuthResourceServer`
362
+ * from the validator's result, `OAuthProvider` from its own token record.
363
+ */
364
+ interface OAuthResourceAuth {
365
+ /** The token as presented. It is already in the request's `Authorization` header; never log it. */
366
+ token: string;
367
+ /** Canonical resource the token was accepted for. */
368
+ audience: string;
369
+ /** Absolute expiry as seconds since the Unix epoch, when known. */
370
+ expiresAt?: number;
371
+ /** Scopes the token carries; empty when the validator reported none. */
372
+ scope: string[];
373
+ /** Subject the token was issued for, when known. */
374
+ userId?: string;
375
+ /** Client the token was issued to, when known. */
376
+ clientId?: string;
377
+ }
378
+ /** The execution context a protected handler receives: `props` from the validator, `auth` from the host. */
379
+ type OAuthResourceContext<Props> = ExecutionContext<Props> & {
380
+ readonly auth: OAuthResourceAuth;
381
+ };
382
+ /**
383
+ * Validates a bearer token presented to one resource. `null` for a token that is not
384
+ * valid for that resource; a thrown error fails closed as `503`.
385
+ */
386
+ type OAuthResourceTokenValidator<Props> = (resource: string, token: string) => Promise<OAuthResourceTokenValidation<Props> | null>;
387
+ /**
388
+ * Protected application handler called after successful token validation: an object with
389
+ * `fetch`, or a `WorkerEntrypoint` subclass instantiated per request with `(ctx, env)`. Either
390
+ * way `ctx.props` carries what the validator returned and `ctx.auth` what the host verified.
391
+ */
392
+ type OAuthResourceHandler<Env, Props> = {
393
+ fetch(request: Request, env: Env, ctx: OAuthResourceContext<Props>): Response | Promise<Response>;
394
+ } | (new (ctx: OAuthResourceContext<Props>, env: Env) => {
395
+ fetch(request: Request): Response | Promise<Response>;
396
+ });
397
+ /** Configuration for {@link OAuthResourceServer}. */
398
+ interface OAuthResourceServerOptions<Env = Cloudflare.Env, Props = unknown> {
399
+ /** RFC 9728 metadata, including this server's one canonical resource. */
400
+ resourceMetadata: OAuthResourceMetadata;
401
+ /** Application handler for the canonical resource URL and its path descendants. */
402
+ handler: OAuthResourceHandler<Env, Props>;
403
+ /**
404
+ * The validator for a request. The host calls what you return with this server's
405
+ * canonical resource and the bearer token, so neither is repeated here.
406
+ *
407
+ * - The authorization server in another Worker, over a Service Binding to a
408
+ * `WorkerEntrypoint` that exposes `OAuthAuthorizationServer.validateToken()`:
409
+ * `(env) => env.AUTH_SERVER.validateToken`
410
+ * - The authorization server in this Worker:
411
+ * `(env) => (resource, token) => authorizationServer.validateToken(resource, token, env)`
412
+ * - Anything else, at your own risk: a function that validates the token for `resource`.
413
+ */
414
+ validateToken(env: Env, request: Request): OAuthResourceTokenValidator<Props>;
415
+ }
416
+ /**
417
+ * The response for a valid token that lacks the scopes an operation needs (RFC 6750 §3.1,
418
+ * MCP scope challenge handling): `403` with `WWW-Authenticate: Bearer error="insufficient_scope"`,
419
+ * the `scope` the operation requires and the `resource_metadata` URL the client already knows.
420
+ * Name every scope the operation needs in one response; clients treat the list as complete.
421
+ *
422
+ * ```ts
423
+ * if (!ctx.auth.scope.includes('calendar:write')) return insufficientScope(ctx.auth, ['calendar:write']);
424
+ * ```
425
+ */
426
+ declare function insufficientScope(auth: OAuthResourceAuth, scope: string[], description?: string): Response;
427
+ /**
428
+ * One OAuth protected resource, in the authorization server's Worker or its own. Publishes
429
+ * RFC 9728 metadata, challenges unauthenticated requests, validates the bearer token through
430
+ * the configured `validateToken`, enforces audience and expiry on what it returns, and routes
431
+ * only the canonical resource and its descendants to the application handler.
432
+ *
433
+ * `export default new OAuthResourceServer({ … })`, or dispatch to `fetch()` from your own
434
+ * router, exactly as with `OAuthAuthorizationServer`.
435
+ */
436
+ declare class OAuthResourceServer<Env = Cloudflare.Env, Props = unknown> {
437
+ #private;
438
+ constructor(options: OAuthResourceServerOptions<Env, Props>);
439
+ fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response>;
440
+ }
441
+ //#endregion
442
+ //#region src/oauth-consent.d.ts
443
+ /** Remember an approval so the consent page can be skipped for requests it already covers. */
444
+ interface RememberConsentOptions {
445
+ /** HMAC key for the approvals cookie: at least 32 characters, from a Worker secret. */
446
+ secret: string;
447
+ /** How long an approval is remembered. Defaults to 30 days. */
448
+ maxAgeSeconds?: number;
449
+ /**
450
+ * The signed-in user, when you know them before consent (your own sign-in). The approval is then
451
+ * bound to them as well, so another account on the same browser is asked again. Without it an
452
+ * approval belongs to the browser, which suits a proxy server that only learns the user from the
453
+ * third party after consent.
454
+ */
455
+ subject?: string;
456
+ }
457
+ /** A consent page to render: post `handle` back to {@link approveConsent}; send `headers` with the page. */
458
+ interface ConsentTransaction {
459
+ handle: string;
460
+ headers: Headers;
461
+ }
462
+ /** The approved authorization request, with the cookies to set on the next response. */
463
+ interface ApprovedConsent {
464
+ request: AuthRequest;
465
+ headers: Headers;
466
+ }
467
+ /** Where to send the browser after the user declined, with the cookies to set on that redirect. */
468
+ interface DeniedConsent {
469
+ request: AuthRequest;
470
+ /** The client's redirect URI with `error=access_denied`, its `state`, and `iss` (RFC 9207). */
471
+ redirectTo: string;
472
+ headers: Headers;
473
+ }
474
+ /** The `state` to send to the third-party provider, with the binding cookie to set on the redirect. */
475
+ interface UpstreamTransaction {
476
+ state: string;
477
+ headers: Headers;
478
+ }
479
+ /** The authorization request and data saved by `beginUpstream()`, recovered at the callback. */
480
+ interface ResumedUpstream<Data = unknown> {
481
+ request: AuthRequest;
482
+ data: Data;
483
+ headers: Headers;
484
+ }
485
+ //#endregion
486
+ //#region src/oauth-resource.d.ts
487
+ /** Validate an RFC 3986-safe HTTP(S) resource identifier for RFC 8707. */
488
+ declare function validateResourceUri(uri: string): boolean;
489
+ /**
490
+ * Whether a requested resource identifies a granted or configured resource.
491
+ * Only ASCII case in the URI scheme and host is ignored, and an empty path is
492
+ * equivalent to `/` (RFC 3986 §6.2.3), so a client that round-trips
493
+ * `https://example.com` through a URL parser as `https://example.com/` still
494
+ * names it. Port, path, query, a trailing slash after a path segment, user
495
+ * information, and every other byte remain significant.
496
+ */
497
+ declare function resourceMatches(requested: string, granted: string): boolean;
498
+ //#endregion
327
499
  //#region src/oauth-provider.d.ts
328
500
  /**
329
501
  * Enum representing OAuth grant types
@@ -337,8 +509,8 @@ declare enum GrantType {
337
509
  /**
338
510
  * Aliases for either type of Handler that makes .fetch required
339
511
  */
340
- type ExportedHandlerWithFetch<Env = Cloudflare.Env> = ExportedHandler<Env> & Pick<Required<ExportedHandler<Env>>, 'fetch'>;
341
- type WorkerEntrypointWithFetch<Env = Cloudflare.Env> = WorkerEntrypoint<Env> & {
512
+ type ExportedHandlerWithFetch<Env = Cloudflare.Env, Props = unknown> = ExportedHandler<Env, unknown, unknown, Props> & Pick<Required<ExportedHandler<Env, unknown, unknown, Props>>, 'fetch'>;
513
+ type WorkerEntrypointWithFetch<Env = Cloudflare.Env, Props = {}> = WorkerEntrypoint<Env, Props> & {
342
514
  fetch: NonNullable<WorkerEntrypoint['fetch']>;
343
515
  };
344
516
  /**
@@ -382,16 +554,36 @@ interface TokenExchangeCallbackResult {
382
554
  /**
383
555
  * Override the default refresh token TTL (time-to-live) for this specific grant.
384
556
  * Value should be in seconds.
385
- * Note: This is only honored during authorization code exchange. If returned during
386
- * refresh token exchange, it will be ignored.
557
+ * Note: This is only honored during authorization code exchange. Returning it during
558
+ * refresh token exchange is rejected with `invalid_request`; to extend a grant on
559
+ * refresh, return `refreshTokenIdleTTL`.
387
560
  */
388
561
  refreshTokenTTL?: number;
562
+ /**
563
+ * Idle lifetime for this grant's refresh token, in seconds from this refresh. The
564
+ * grant then expires that long after this refresh unless it is refreshed again. Only
565
+ * honored during refresh token exchange; returning it for another grant type is
566
+ * rejected with `invalid_request`. Overrides the provider's `refreshTokenIdleTTL`
567
+ * for this refresh.
568
+ *
569
+ * Useful when the Worker is itself an OAuth client and has just rotated an upstream
570
+ * refresh token: return the upstream lifetime and the grant lives exactly as long as
571
+ * the credentials it proxies. Must be an integer of at least 60 seconds.
572
+ */
573
+ refreshTokenIdleTTL?: number;
389
574
  /**
390
575
  * Optional scopes for the new access token. Values outside the scope ceiling
391
576
  * for the current grant flow are ignored. If omitted, the effective requested
392
577
  * scopes are used.
393
578
  */
394
579
  accessTokenScope?: string[];
580
+ /**
581
+ * Permit a token exchange whose authenticated client differs from the client the
582
+ * subject token's grant was issued to. Cross-client exchange is rejected with
583
+ * `invalid_request` unless the callback returns `true` here, so impersonation
584
+ * across clients is always a deliberate policy decision.
585
+ */
586
+ allowCrossClientExchange?: boolean;
395
587
  }
396
588
  /**
397
589
  * Options for token exchange callback functions
@@ -402,9 +594,17 @@ interface TokenExchangeCallbackOptions {
402
594
  */
403
595
  grantType: GrantType;
404
596
  /**
405
- * Client that received this grant
597
+ * Client authenticated on this token request. For `authorization_code` and
598
+ * `refresh_token` it is always the grant's client. For token exchange it is the
599
+ * exchanging client, which may differ from {@link subjectClientId}.
406
600
  */
407
601
  clientId: string;
602
+ /**
603
+ * Client the underlying grant was issued to. Equal to `clientId` except during a
604
+ * cross-client token exchange, which the callback must explicitly allow with
605
+ * {@link TokenExchangeCallbackResult.allowCrossClientExchange}.
606
+ */
607
+ subjectClientId: string;
408
608
  /**
409
609
  * User who authorized this grant
410
610
  */
@@ -425,6 +625,8 @@ interface TokenExchangeCallbackOptions {
425
625
  * Effective scopes selected for this token before applying the callback result.
426
626
  */
427
627
  requestedScope: string[];
628
+ /** Canonical protected resource selected for this grant and token. */
629
+ resource: string;
428
630
  /**
429
631
  * Application-specific properties currently associated with this grant
430
632
  */
@@ -504,11 +706,40 @@ interface ResolveExternalTokenResult {
504
706
  *
505
707
  * A JWT may carry this value as an `aud` claim. For an opaque API token or
506
708
  * PAT, the callback can supply the local resource URI as policy after
507
- * successful validation. When `resourceMetadata.resource` is configured,
508
- * this value is required and must match it exactly.
709
+ * successful validation. This value is required and must identify the
710
+ * configured canonical `resourceMetadata.resource`: ASCII case in the scheme
711
+ * and host is folded and an empty path equals `/`, while port, path, query,
712
+ * and trailing slash are compared exactly.
509
713
  */
510
- audience?: string | string[];
714
+ audience: string;
511
715
  }
716
+ /** RFC 9728 metadata owned by one protected resource server. */
717
+ interface OAuthProtectedResourceMetadata {
718
+ /**
719
+ * The protected resource identifier HTTPS URL (RFC 9728 `resource` field).
720
+ * Configure an RFC 3986-safe HTTPS producer URL with lowercase scheme/host
721
+ * and no userinfo, default port, fragment, or dot segments.
722
+ */
723
+ resource: string;
724
+ /**
725
+ * Authorization server issuers that can issue tokens for this resource.
726
+ * In the legacy combined configuration this defaults to the token endpoint
727
+ * origin. In the role-based configuration it defaults to the configured AS
728
+ * issuer.
729
+ */
730
+ authorization_servers?: string[];
731
+ /** Minimal scopes required for basic protected-resource functionality. */
732
+ scopes_supported?: string[];
733
+ /** Methods by which bearer tokens can be presented. Defaults to `["header"]`. */
734
+ bearer_methods_supported?: string[];
735
+ /** Human-readable name for this resource. */
736
+ resource_name?: string;
737
+ }
738
+ /**
739
+ * Existing combined authorization-server and protected-resource configuration.
740
+ * This shape remains supported in 1.0 and is normalized to a one-resource
741
+ * role-based provider internally.
742
+ */
512
743
  interface OAuthProviderOptions<Env = Cloudflare.Env> {
513
744
  /**
514
745
  * URL(s) for API routes. Requests with URLs starting with any of these prefixes
@@ -572,6 +803,18 @@ interface OAuthProviderOptions<Env = Cloudflare.Env> {
572
803
  * For example: 3600 = 1 hour, 2592000 = 30 days
573
804
  */
574
805
  refreshTokenTTL?: number;
806
+ /**
807
+ * Idle lifetime for refresh tokens in seconds. When set, every successful refresh
808
+ * moves the grant's expiry to this many seconds after the refresh, so a grant lives
809
+ * for as long as the client keeps using it and expires once it has been idle for
810
+ * this long. `refreshTokenTTL` still sets the lifetime of a new grant, and
811
+ * `tokenExchangeCallback` can override the idle lifetime per refresh by returning
812
+ * `refreshTokenIdleTTL`. Must be an integer of at least 60 seconds.
813
+ *
814
+ * Leave unset for a fixed lifetime, where a grant expires `refreshTokenTTL` seconds
815
+ * after the code exchange however often it is refreshed.
816
+ */
817
+ refreshTokenIdleTTL?: number;
575
818
  /**
576
819
  * Time-to-live for dynamically registered clients in seconds.
577
820
  * Defaults to 90 days (7,776,000 seconds).
@@ -655,10 +898,10 @@ interface OAuthProviderOptions<Env = Cloudflare.Env> {
655
898
  *
656
899
  * If the function returns a Response, that will be used in place of the OAuthProvider's default one.
657
900
  *
658
- * `internal` (when present) carries a tagged, server-side-only reason that the library
659
- * deliberately did NOT put on the wire — used for richer diagnostics where the public
660
- * response must stay generic (e.g. JWT validation failures on the EMA path). Backwards
661
- * compatible: existing callbacks ignoring this field continue to work unchanged.
901
+ * `internal` carries the stable, server-side-only reason for the error — often more
902
+ * specific than what the wire deliberately reveals (RFC 6749 §5.2), e.g. which refresh-token
903
+ * check failed or which EMA validation fired. It is never placed on the wire. See
904
+ * {@link OAuthErrorInternal}.
662
905
  *
663
906
  * `request` (when present) is the HTTP request that produced the error response, so the
664
907
  * callback can correlate the error with per-request state such as request-keyed telemetry.
@@ -670,11 +913,7 @@ interface OAuthProviderOptions<Env = Cloudflare.Env> {
670
913
  description: string;
671
914
  status: number;
672
915
  headers: Record<string, string>;
673
- internal?: {
674
- category: string;
675
- reason: string;
676
- detail?: unknown;
677
- };
916
+ internal: OAuthErrorInternal;
678
917
  request?: Request;
679
918
  }) => Response | void;
680
919
  /**
@@ -685,59 +924,58 @@ interface OAuthProviderOptions<Env = Cloudflare.Env> {
685
924
  */
686
925
  clientIdMetadataDocumentEnabled?: boolean;
687
926
  /**
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.
693
- *
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`.
927
+ * Prefix for the cookies the consent and upstream helpers set (`<prefix>consent`,
928
+ * `<prefix>upstream`, `<prefix>approvals`). Must start with `__Host-`, which keeps them
929
+ * `Secure`, host-only and `Path=/`. Defaults to `__Host-oauth-`.
698
930
  */
699
- resourceMatchOriginOnly?: boolean;
931
+ cookiePrefix?: string;
700
932
  /**
701
- * Optional metadata for RFC 9728 OAuth 2.0 Protected Resource Metadata.
933
+ * Metadata for RFC 9728 OAuth 2.0 Protected Resource Metadata.
702
934
  * Controls the response served at /.well-known/oauth-protected-resource.
703
- *
704
- * If not provided, the endpoint will be automatically generated using the request origin
705
- * as the resource identifier, and the token endpoint's origin as the authorization server.
706
935
  */
707
- resourceMetadata?: {
708
- /**
709
- * The protected resource identifier URL (RFC 9728 `resource` field).
710
- *
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.
716
- */
717
- resource?: string;
718
- /**
719
- * List of authorization server issuer URLs that can issue tokens for this resource.
720
- * If not set, defaults to the token endpoint's origin (consistent with the issuer
721
- * in authorization server metadata).
722
- */
723
- authorization_servers?: string[];
724
- /**
725
- * Minimal scopes required for basic protected resource functionality.
726
- * These scopes are advertised in Protected Resource Metadata and used as
727
- * baseline bearer challenge guidance. `offline_access` is omitted because
728
- * refresh-token issuance is an authorization server capability.
729
- */
730
- scopes_supported?: string[];
731
- /**
732
- * Methods by which bearer tokens can be presented to this resource.
733
- * Defaults to ["header"].
734
- */
735
- bearer_methods_supported?: string[];
736
- /**
737
- * Human-readable name for this resource.
738
- */
739
- resource_name?: string;
740
- };
936
+ resourceMetadata: OAuthProtectedResourceMetadata;
937
+ }
938
+ /**
939
+ * Functional authorization-server surface for the role-based API.
940
+ * The application owns the interactive authorization route and uses
941
+ * `getOAuthApi()` to parse and complete it; `fetch()` serves protocol-owned AS
942
+ * endpoints such as metadata, token, revocation, and optional registration.
943
+ */
944
+ type OAuthAuthorizationServerOptions<Env = Cloudflare.Env> = Omit<OAuthProviderOptions<Env>, 'apiRoute' | 'apiHandler' | 'apiHandlers' | 'defaultHandler' | 'resourceMetadata' | 'resolveExternalToken'> & {
945
+ /** Canonical RFC 8414 issuer. */
946
+ issuer: string;
947
+ /**
948
+ * Canonical identifiers of every protected resource this authorization server issues
949
+ * tokens for, whether hosted in this Worker through `OAuthResourceServer` or by another
950
+ * Worker or service. At least one is required. The registry is fixed at construction,
951
+ * so `defaultResource` and `legacyGrantResource` are checked before the first request,
952
+ * and `validateToken()` rejects for a resource that is not listed.
953
+ */
954
+ resources: readonly string[];
955
+ /** Resource selected for a new authorization request that omits `resource`. */
956
+ defaultResource?: string;
957
+ /**
958
+ * Migration destination for pre-resource grants and access tokens. Changing it
959
+ * re-targets every surviving unbound record, so keep it fixed for the migration window.
960
+ */
961
+ legacyGrantResource?: string;
962
+ };
963
+ /** Audience-checked token context suitable for a private Service Binding. */
964
+ interface ValidatedAccessToken<T = any> {
965
+ props: T;
966
+ audience: string;
967
+ expiresAt: number;
968
+ scope: string[];
969
+ userId: string;
970
+ clientId: string;
971
+ }
972
+ /**
973
+ * The RPC surface a `WorkerEntrypoint` exposes around {@link OAuthAuthorizationServer.validateToken},
974
+ * as a resource Worker sees it through a Service Binding. Hand a resource server the bound
975
+ * method as its validator: `validateToken: (env) => env.AUTH_SERVER.validateToken`.
976
+ */
977
+ interface AuthorizationServerBinding<Props = any> {
978
+ validateToken(resource: string, token: string): Promise<ValidatedAccessToken<Props> | null>;
741
979
  }
742
980
  /**
743
981
  * Helper methods for OAuth operations provided to handler functions
@@ -768,6 +1006,53 @@ interface OAuthHelpers {
768
1006
  completeAuthorization(options: CompleteAuthorizationOptions): Promise<{
769
1007
  redirectTo: string;
770
1008
  }>;
1009
+ /**
1010
+ * Whether an approval remembered by `approveConsent(…, { remember })` covers this request: the
1011
+ * same client, redirect URI and resource, asking for a subset of the approved scopes. Pass the
1012
+ * same `secret`, and the same `subject` if the approval was bound to a user. Don't call it to ask
1013
+ * on every authorization.
1014
+ */
1015
+ isConsentRemembered(request: Request, authRequest: AuthRequest, remember: Pick<RememberConsentOptions, 'secret' | 'subject'>): Promise<boolean>;
1016
+ /**
1017
+ * Start a consent page for a request. Render your page with `handle` in the form and send
1018
+ * `headers` with it: they bind the handle to this browser and forbid framing the page.
1019
+ */
1020
+ beginConsent(authRequest: AuthRequest): Promise<ConsentTransaction>;
1021
+ /**
1022
+ * Accept the consent form. `handle` is the value your form posted; the browser's binding cookie
1023
+ * must match it, and it works once. `scope` is what the user approved: fewer or more than the
1024
+ * client requested, each one in `scopesSupported`. `remember` stores the approval in a signed
1025
+ * cookie for `isConsentRemembered()`. Send the returned `headers` on the next response.
1026
+ * @throws AuthorizationError when the handle is missing, unbound, expired, or already used
1027
+ */
1028
+ approveConsent(request: Request, handle: string, options?: {
1029
+ scope?: string[];
1030
+ remember?: RememberConsentOptions;
1031
+ }): Promise<ApprovedConsent>;
1032
+ /**
1033
+ * Decline the consent form: consumes the handle like `approveConsent()` and returns the redirect
1034
+ * back to the client with `error=access_denied`, its `state` and `iss`. Send `headers` with the
1035
+ * redirect (they include `Location`).
1036
+ * @throws AuthorizationError when the handle is missing, unbound, expired, or already used
1037
+ */
1038
+ denyConsent(request: Request, handle: string, options?: {
1039
+ description?: string;
1040
+ }): Promise<DeniedConsent>;
1041
+ /**
1042
+ * Save an approved request before redirecting to a third-party provider, and get the `state`
1043
+ * to send it. Call only after consent. `data` is returned at the callback, e.g. a PKCE verifier.
1044
+ * Pass `headers` to add the binding cookie to headers you are already sending.
1045
+ */
1046
+ beginUpstream(authRequest: AuthRequest, options?: {
1047
+ data?: unknown;
1048
+ headers?: Headers;
1049
+ }): Promise<UpstreamTransaction>;
1050
+ /**
1051
+ * At the third-party provider's callback, recover the approved request and your `data` from the
1052
+ * `state` parameter. The browser's binding cookie must match, and it works once.
1053
+ * @throws AuthorizationError when `state` is missing, unbound, expired, or already used
1054
+ */
1055
+ finishUpstream<Data = unknown>(request: Request): Promise<ResumedUpstream<Data>>;
771
1056
  /**
772
1057
  * Creates a new OAuth client
773
1058
  * @param clientInfo - Partial client information to create the client with
@@ -852,9 +1137,11 @@ interface ExchangeTokenOptions {
852
1137
  */
853
1138
  scope?: string[];
854
1139
  /**
855
- * Optional target audience/resource for the new token (maps to resource parameter per RFC 8707)
1140
+ * Optional canonical target audience/resource for the new token (maps to the
1141
+ * resource parameter per RFC 8707). When present, it must match the
1142
+ * provider's configured resource.
856
1143
  */
857
- aud?: string | string[];
1144
+ aud?: string;
858
1145
  /**
859
1146
  * Optional TTL override for the new token in seconds (must not exceed subject token's remaining lifetime)
860
1147
  */
@@ -893,9 +1180,10 @@ interface AuthRequest {
893
1180
  */
894
1181
  codeChallengeMethod?: string;
895
1182
  /**
896
- * Resource parameter indicating target resource(s) (RFC 8707)
1183
+ * Canonical target resource (RFC 8707). Parsed authorization requests
1184
+ * always contain the provider's configured resource.
897
1185
  */
898
- resource?: string | string[];
1186
+ resource?: string;
899
1187
  /**
900
1188
  * Authorization server issuer recorded while parsing this request.
901
1189
  * Include it as `iss` in successful and error authorization responses.
@@ -1015,10 +1303,12 @@ interface CompleteAuthorizationOptions {
1015
1303
  */
1016
1304
  revokeExistingGrants?: boolean;
1017
1305
  /**
1018
- * Maximum number of grants to fetch per page when revoking existing
1019
- * grants. Only used when revokeExistingGrants is not false.
1020
- * Must be a positive integer. Values above Cloudflare KV's 1000-key page
1021
- * limit are clamped to 1000. Defaults to 50.
1306
+ * How many grants written by a version before 1.0 are read at once while
1307
+ * revoking existing grants. Grants written by 1.0 or later carry their client,
1308
+ * resource and redirect URI as KV key metadata and are matched from `list()`
1309
+ * without being read, so this only bounds the fan-out for older grants. Only
1310
+ * used when revokeExistingGrants is not false. Must be a positive integer;
1311
+ * values above 1000 are clamped. Defaults to 50.
1022
1312
  */
1023
1313
  revokeExistingGrantsBatchSize?: number;
1024
1314
  }
@@ -1125,7 +1415,7 @@ interface TokenResponse {
1125
1415
  * Resource indicator(s) for the issued access token (RFC 8707 Section 2.2)
1126
1416
  * SHOULD be included to indicate the resource server(s) for which the token is valid
1127
1417
  */
1128
- resource?: string | string[];
1418
+ resource: string;
1129
1419
  }
1130
1420
  /**
1131
1421
  * Shared fields for Token and TokenSummary
@@ -1328,6 +1618,8 @@ interface GrantSummary {
1328
1618
  * grants created before this field was introduced.
1329
1619
  */
1330
1620
  redirectUri?: string;
1621
+ /** Canonical protected resource, absent only on a pre-resource legacy grant. */
1622
+ resource?: string | string[];
1331
1623
  }
1332
1624
  /**
1333
1625
  * OAuth 2.0 Provider implementation for Cloudflare Workers
@@ -1360,6 +1652,43 @@ declare class OAuthProvider<Env = Cloudflare.Env> {
1360
1652
  */
1361
1653
  purgeExpiredData(env: Env, options?: PurgeOptions): Promise<PurgeResult>;
1362
1654
  }
1655
+ /**
1656
+ * The authorization-server role on its own. It serves discovery, token, revocation and
1657
+ * registration endpoints from `fetch()`, exposes the interactive authorization flow through
1658
+ * `getOAuthApi()`, and validates its access tokens for any declared resource through
1659
+ * `validateToken()`. Resources are hosted, in this Worker or another, by
1660
+ * `OAuthResourceServer`, whose `validateToken` points back here.
1661
+ */
1662
+ declare class OAuthAuthorizationServer<Env = Cloudflare.Env> {
1663
+ #private;
1664
+ constructor(options: OAuthAuthorizationServerOptions<Env>);
1665
+ /**
1666
+ * Validate an access token for one declared resource: the token's KV record is loaded, its
1667
+ * audience checked against `resource`, and its props decrypted. Resolves `null` for a token
1668
+ * that is not valid for that resource, and rejects for a resource this server does not
1669
+ * declare.
1670
+ *
1671
+ * This is what a resource server calls, whether it shares this Worker or reaches it over a
1672
+ * Service Binding. For the latter, expose it from a `WorkerEntrypoint`:
1673
+ *
1674
+ * ```ts
1675
+ * export default class AuthServer extends WorkerEntrypoint<Env> {
1676
+ * fetch(request: Request) {
1677
+ * return authorizationServer.fetch(request, this.env, this.ctx);
1678
+ * }
1679
+ * validateToken(resource: string, token: string) {
1680
+ * return authorizationServer.validateToken(resource, token, this.env);
1681
+ * }
1682
+ * }
1683
+ * ```
1684
+ */
1685
+ validateToken<T = any>(resource: string, token: string, env: Env): Promise<ValidatedAccessToken<T> | null>;
1686
+ /** Serve protocol-owned authorization-server endpoints, excluding the application authorization UI. */
1687
+ fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response>;
1688
+ /** Obtain helpers for scheduled jobs or custom code outside a fetch dispatch. */
1689
+ getOAuthApi(env: Env): OAuthHelpers;
1690
+ purgeExpiredData(env: Env, options?: PurgeOptions): Promise<PurgeResult>;
1691
+ }
1363
1692
  /**
1364
1693
  * Gets OAuthHelpers for the given environment
1365
1694
  * @param options - Configuration options for the OAuth provider
@@ -1371,6 +1700,24 @@ declare function getOAuthApi<Env = Cloudflare.Env>(options: OAuthProviderOptions
1371
1700
  * Error class for OAuth operations
1372
1701
  * Carries OAuth error code and description for proper error responses
1373
1702
  */
1703
+ /**
1704
+ * The internal reason behind an error response, forwarded to the `onError` hook and never
1705
+ * placed on the wire. `category` is a stable kebab-case subsystem (`client-authentication`,
1706
+ * `authorization-code-grant`, `refresh-token-grant`, `token-exchange-grant`,
1707
+ * `token-endpoint-request`, `token-revocation`, `token-issuance`, `resource-indicator`,
1708
+ * `client-registration`, `client-id-metadata-document`, `protected-resource`,
1709
+ * `enterprise-managed-authorization`, `token-exchange-callback`); `reason` is a stable
1710
+ * snake_case slug naming the exact check that failed. Treat both like enum members in semver.
1711
+ * `detail` may carry structured context such as a caught error; it is never a secret.
1712
+ */
1713
+ interface OAuthErrorInternal {
1714
+ /** Stable kebab-case subsystem that produced the error. */
1715
+ category: string;
1716
+ /** Stable snake_case slug naming the failed check, often more specific than the wire description. */
1717
+ reason: string;
1718
+ /** Optional structured context, e.g. the caught error or the offending parameter. */
1719
+ detail?: unknown;
1720
+ }
1374
1721
  /**
1375
1722
  * Options accepted by the {@link OAuthError} constructor.
1376
1723
  */
@@ -1392,6 +1739,13 @@ interface OAuthErrorOptions {
1392
1739
  * number of seconds or an HTTP-date.
1393
1740
  */
1394
1741
  headers?: Record<string, string>;
1742
+ /**
1743
+ * Internal reason forwarded to the `onError` hook and never sent on the wire. The library
1744
+ * sets it on every error it originates; a `tokenExchangeCallback` may set its own. An
1745
+ * `OAuthError` thrown without one reaches `onError` as
1746
+ * `{ category: 'token-exchange-callback', reason: 'callback_error', detail: error }`.
1747
+ */
1748
+ internal?: OAuthErrorInternal;
1395
1749
  }
1396
1750
  /**
1397
1751
  * Structured OAuth 2.0 token-endpoint error.
@@ -1418,6 +1772,7 @@ interface OAuthErrorOptions {
1418
1772
  * async function refreshUpstream(props) {
1419
1773
  * const res = await fetch(...);
1420
1774
  * if (res.status === 401) {
1775
+ * // invalid_grant can never recover: the provider also revokes this grant and its tokens.
1421
1776
  * throw new OAuthError('invalid_grant', { description: 'upstream refresh token is invalid' });
1422
1777
  * }
1423
1778
  * if (res.status === 429) {
@@ -1523,18 +1878,6 @@ declare class CimdFetchError extends Error {
1523
1878
  */
1524
1879
  constructor(metadataUrl: string, cause: unknown);
1525
1880
  }
1526
- /**
1527
- * Validates a resource URI per RFC 8707 Section 2
1528
- * @param uri - The URI string to validate
1529
- * @returns true if valid, false otherwise
1530
- */
1531
- declare function validateResourceUri(uri: string): boolean;
1532
- /**
1533
- * Checks if a requested resource matches a granted resource.
1534
- * When originOnly is true, compares only the origin (scheme + host + port),
1535
- * allowing path-aware resources to match origin-only grants.
1536
- */
1537
- declare function resourceMatches(requested: string, granted: string, originOnly: boolean): boolean;
1538
1881
  /**
1539
1882
  * Decodes a base64url-encoded string to bytes.
1540
1883
  */
@@ -1551,4 +1894,4 @@ declare function getJwtCryptoAlgorithms(alg: string): {
1551
1894
  verifyAlgorithm: Parameters<SubtleCrypto['verify']>[0];
1552
1895
  };
1553
1896
  //#endregion
1554
- export { AuthRequest, AuthorizationError, type AuthorizationErrorCode, type AuthorizationErrorOptions, 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, OAuthError, OAuthErrorOptions, OAuthHelpers, OAuthProvider, OAuthProvider as default, OAuthProviderOptions, OAuthTokenErrorCode, PurgeOptions, PurgeResult, ResolveExternalTokenInput, ResolveExternalTokenResult, Token, TokenBase, TokenExchangeCallbackOptions, TokenExchangeCallbackResult, TokenSummary, base64UrlToBytes, getJwtCryptoAlgorithms, getOAuthApi, isValidOAuthScopeToken, parseJwtJsonPart, resourceMatches, validateResourceUri };
1897
+ export { type ApprovedConsent, AuthRequest, AuthorizationError, type AuthorizationErrorCode, type AuthorizationErrorOptions, AuthorizationServerBinding, CimdFetchError, ClientInfo, ClientRegistrationCallbackOptions, ClientRegistrationCallbackResult, CompleteAuthorizationOptions, 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, OAuthErrorInternal, 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, base64UrlToBytes, getJwtCryptoAlgorithms, getOAuthApi, insufficientScope, isValidOAuthScopeToken, parseJwtJsonPart, resourceMatches, validateResourceUri };