Repository navigation
v1.0.0
Major Changes
- #289
a6c2e4aThanks @mattzcarey! - Split the authorization server and resource server roles, and bind every token to one canonical resource. Migration guide: docs/migration-1.0.md — for most 0.x deployments the diff is addingresourceMetadata: { resource }.OAuthAuthorizationServerdeclares itsresourcesat construction and validates its access tokens for any of them withvalidateToken(resource, token, env). Metadata advertises the registry as RFC 9728protected_resources. Every resource is hosted byOAuthResourceServer, in the same Worker or another: itsvalidateTokenoption returns the validator for a request, the host calls it with its own canonical resource and the bearer token, and the two topologies differ only in what that points at,(env) => (resource, token) => authorizationServer.validateToken(resource, token, env)locally or(env) => env.AUTH_SERVER.validateTokenover a Service Binding to aWorkerEntrypoint(AuthorizationServerBindingtypes it).resolveExternalTokenstays on the combinedOAuthProvideronly; a role-based resource that must accept another issuer's tokens does so in its ownvalidateToken.OAuthResourceServerpublishes RFC 9728 metadata (a cache-busting query does not hide it; a resource's own query must be present), issues Bearer challenges, and enforces audience and expiry on whatever the validator returns.- Handlers on both hosts receive
ctx.auth(token,audience,expiresAt,scope,userId,clientId) besidectx.props; the initial401namesscopes_supported;insufficientScope(ctx.auth, scopes)builds the MCP403 insufficient_scopechallenge with every required scope and the resource's metadata URL. A validator may reportscope,userIdandclientId; malformed values fail closed. - Every new grant and access token is bound to exactly one registered resource. A multi-resource server requires
resourceunlessdefaultResourceis set; code exchange and refresh inherit the grant's resource and reject retargeting; a new grant replaces only same-resource grants. - Stored 0.x state keeps working. A sole resource, or
legacyGrantResource, is the migration target for grants and tokens without one; an array audience resolves to the registered resource it contains; a grant that cannot be bound fails refresh withinvalid_grant, which conformant clients answer with a new authorization. - Breaking:
resourceMetadata.resourceis required and canonical (httponly on loopback hosts). Construction rejects routes the resource does not cover, absolute routes on another origin or with a conflicting query, hosted resources whose queries nest, and resources inside the metadata namespace. Removed:resourceMatchOriginOnly,registerResource(), the three-argumentvalidateToken(), theoriginOnlyparameter ofresourceMatches(), andEmaValidationInput.matchOriginOnly.ResolveExternalTokenResult.audienceis required and a single string;ExchangeTokenOptions.aud,AuthRequest.resource, andTokenExchangeCallbackOptions.resourceare single strings. - Protocol:
redirect_urion a code exchange must equal the authorization request's (OAuth 2.1 §4.1.3); registeredgrant_typesare enforced withunauthorized_client; token exchange is bound to the grant's client unlesstokenExchangeCallbackreturnsallowCrossClientExchange: true, and subject-token failures returninvalid_request; an empty resource path equals/; a repeated identicalresourceis accepted; challenges carryresource_metadataon the resource and its path descendants; discovery answers HEAD and OPTIONS, every 405 carriesAllow, and a query-bearing resource accepts extra request parameters.
Minor Changes
-
#317
1e0aa19Thanks @mattzcarey! - Bound the KV workcompleteAuthorization()does to revoke a user's earlier grants for the same client.Every grant key now carries its
clientId,resourceandredirectUrias KV key metadata, so the grants a new authorization replaces are found fromlist()alone. The cost is onelist()per thousand grants the user has instead of oneget()per grant, which exceeded a Worker's subrequest limit for a user with more than a few hundred grants on the paid plan, or a few dozen on the free plan.Grants written before this version have no key metadata and are still read individually;
revokeExistingGrantsBatchSizenow bounds how many of those are read at once (default 50, maximum 1000) rather than thelist()page size. A refresh rewrites its grant with metadata, so that share shrinks on its own. Nothing about tokens, refresh tokens or grant records changes, and no migration is needed. -
#327
6e03aaaThanks @mattzcarey! -onError.internalis now set on every error the library originates, not only the EMA path:{ category, reason, detail? }names the exact check that failed (refresh_token_mismatchvsrefresh_token_expiredvsgrant_not_found,code_replayed,client_secret_mismatch,resource_not_configured, …) while the wire response stays exactly as before (RFC 6749 §5.2). The shape is exported asOAuthErrorInternal; category slugs are kebab-case subsystems, reason slugs snake_case checks, and both are stable.OAuthErroracceptsoptions.internalso atokenExchangeCallbackcan tag its own errors; one thrown without it reachesonErroras{ category: 'token-exchange-callback', reason: 'callback_error', detail: error }. -
#318
2181402Thanks @mattzcarey! - Add opt-in sliding expiry for refresh tokens.A grant's lifetime is fixed at the code exchange by default: it expires
refreshTokenTTLseconds later however often it is refreshed. The newrefreshTokenIdleTTLoption makes that lifetime slide, moving the grant's expiry, and the KV expiration of its record, to that many seconds after every successful refresh.tokenExchangeCallbackcan returnrefreshTokenIdleTTLduring a refresh to set the lifetime for that refresh alone, so a Worker that proxies an upstream OAuth service can give the grant exactly the lifetime of the upstream refresh token it just rotated.The slide happens only when a refresh succeeds: a throwing callback, an expired grant, or a grant that expires while the callback runs leaves the old expiry in place. Returning
refreshTokenIdleTTLfor any other grant type, or a value that is not an integer of at least 60 seconds, is rejected withinvalid_request. Nothing changes for deployments that do not set it. -
#319
2c902a7Thanks @mattzcarey! - Keep dynamically registered clients alive while they are in use.A DCR registration expired
clientRegistrationTTLafter it was created, whatever the client was doing. A grant that outlived its registration, which any never-expiring or long-lived grant does, then failed every refresh withinvalid_clientalthough nothing had been revoked. Registrations written under the TTL now record when they expire, and a successful client-authenticated token endpoint request made in the second half of that lifetime rewrites the registration for the full TTL, at most once per half TTL per client.Clients created through
createClient(), CIMD clients, and registrations written before this version are never rewritten; the last expire on their original schedule and re-register once.
Patch Changes
- #303
742e222Thanks @kanywst! - Fix uncaught 500 when an ID-JAG assertion has under 60 seconds left. The EMA replay marker
took its KV TTL straight from the assertion's remaining lifetime, so KV rejected the write
and the request crashed instead of exchanging. The marker's TTL is now floored at 60s.