@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.
- package/README.md +200 -136
- package/dist/oauth-provider.d.ts +431 -88
- package/dist/oauth-provider.js +1756 -316
- package/docs/advanced-configuration.md +370 -0
- package/docs/consent-page.md +138 -0
- package/docs/migration-1.0.md +97 -0
- package/docs/resource-servers.md +153 -0
- package/docs/upstream-sign-in.md +93 -0
- package/package.json +4 -2
- package/skills/migrate-to-1.0/SKILL.md +35 -0
package/dist/oauth-provider.d.ts
CHANGED
|
@@ -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.
|
|
386
|
-
* refresh token exchange
|
|
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
|
|
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.
|
|
508
|
-
*
|
|
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
|
|
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`
|
|
659
|
-
*
|
|
660
|
-
*
|
|
661
|
-
*
|
|
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
|
-
*
|
|
689
|
-
*
|
|
690
|
-
*
|
|
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
|
-
|
|
931
|
+
cookiePrefix?: string;
|
|
700
932
|
/**
|
|
701
|
-
*
|
|
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
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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
|
|
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
|
|
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
|
-
*
|
|
1183
|
+
* Canonical target resource (RFC 8707). Parsed authorization requests
|
|
1184
|
+
* always contain the provider's configured resource.
|
|
897
1185
|
*/
|
|
898
|
-
resource?: 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
|
-
*
|
|
1019
|
-
* grants.
|
|
1020
|
-
*
|
|
1021
|
-
*
|
|
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
|
|
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 };
|