Repository navigation
v1.2.0
Minor Changes
-
#338
b61bfc0Thanks @mattzcarey! - Error redirects back to the client no longer need building by hand.AuthorizationError.redirectTois the ready-made redirect (error,error_description,state,iss), set only when a redirect is safe, i.e. whenredirectUriwas validated.authorizationErrorRedirect(request, code, description?)builds the same for an error the application decides on, such as a decline at a third-party provider, from a request the library validated.denyConsent()uses it. -
#339
c801878Thanks @mattzcarey! -OAuthHelpers.describeConsent(request)returns what a consent page must show, per the MCP authorization spec: the client's name (or ID), its verified domain for a Client ID Metadata Document client, the redirect URI's hostname, whether the redirect goes to a local app (show a warning), and the scopes. Every string may come from the client, so escape it before rendering. -
#340
b9a0307Thanks @mattzcarey! - Redirect URIs must usehttps, orhttpon a loopback host (localhost,127.0.0.0/8,::1), as MCP and OAuth 2.1 require, with no userinfo or fragment. The rule is enforced at dynamic registration, increateClient()andupdateClient()(which previously validated nothing), and on every authorization request andcompleteAuthorization(), so clients registered before the policy are held to it: their authorizations fail with a locally renderedinvalid_request, never a redirect. A Client ID Metadata Document, which every server the client uses reads, may still list other redirect URIs next to a compliant one; only a request that uses one is refused.Breaking: clients registered with a remote
httpredirect URI or a private-use scheme stop authorizing. Native apps that rely on RFC 8252 private-use schemes (com.example.app:/cb) keep working with the newallowPrivateUseRedirectUris: true; remotehttpis never accepted. -
#381
95924a3Thanks @mattzcarey! - A resource's required scopes have their own option,requiredScopes, onOAuthResourceServerandOAuthProvider: the scopes any access needs, published as the protected resource metadata'sscopes_supportedand named in the401challenge, so MCP clients request them first. It sits besideresourceMetadatainstead of inside it, where it read like the authorization server'sscopesSupportedcatalogue. It is advertised, not enforced: handlers checkctx.auth.scope, since only they know which scopes imply others.resourceMetadata.scopes_supportedstill works but is deprecated in favour ofrequiredScopes; setting both throws at construction. -
#337
9bab931Thanks @mattzcarey! -tokenExchangeCallbackreceives the request'senv(TokenExchangeCallbackOptions<Env>['env']), asresolveExternalTokenalready does. A callback that needs secrets or bindings, such as an upstream OAuth client secret, no longer forces the provider to be rebuilt on every request to close overenv. -
#383
d53af65Thanks @mattzcarey! - AnOAuthResourceServer'svalidateTokencan throwOAuthErrorto choose the response, as atokenExchangeCallbackalready can.temporarily_unavailablewithstatusCode: 429andRetry-Afterpasses through;insufficient_scopebecomes the MCP403challenge naming the newOAuthErrorrequiredScopesoption (or the resource'srequiredScopes);invalid_tokenbecomes a401with the Bearer challenge. Anything else thrown is still a503. This lets one resource validate an upstream API's own credentials invalidateTokenbefore falling back to its authorization server.OAuthProvider'sresolveExternalTokenandExternalTokenErrorare unchanged.
Patch Changes
-
#385
ebb2efdThanks @mattzcarey! -OAuthAuthorizationServer'sauthorizeEndpointandtokenEndpointare optional and default to${issuer}/authorizeand${issuer}/oauth/token, under the issuer's path if it has one.OAuthProviderstill requires both.Construction also rejects any endpoint another would claim, so it could never be reached: a token, registration or authorization endpoint on the metadata path with a query (metadata is served whatever the query), an endpoint that is the token endpoint plus a query, or an authorization endpoint equal to the token or registration endpoint.
-
#388
bd97dc1Thanks @mattzcarey! -OAuthAuthorizationServer.fetch()no longer writesenv.OAUTH_PROVIDERinto the caller'senvwhen it answers a path that isn't one of its endpoints. It answers404directly; a dead branch meant it fell through to a placeholder handler after setting the helpers, which only the combinedOAuthProvider'sdefaultHandlerreads. UseauthorizationServer.getOAuthApi(env)for the helpers, as documented. -
#361
36cb1b4Thanks @mattzcarey! -tokenExchangeCallbacklifetimes no longer fail requests or silently remove expiry.refreshTokenTTL: undefinedin a callback result now keeps the provider's lifetime; it used to make the grant never expire, which is what a callback passing through an upstream's missingrefresh_expires_indid.refreshTokenTTLis validated (0, or an integer of at least 60 seconds) instead of failing the code exchange with a storage error. Each lifetime is ignored where it doesn't apply, rather than rejected after the callback ran: returningrefreshTokenTTLon refresh, orrefreshTokenIdleTTLat code exchange or token exchange, used to fail the request after the callback's side effects, such as rotating an upstream refresh token. -
#372
78b3c80Thanks @mattzcarey! -createClient()andupdateClient()check grant and response types against what the server implements, as dynamic registration does, so a client can no longer be stored with, say,grantTypes: ['implicit']and then fail every authorization.updateClient()refuses a Client ID Metadata Document client while CIMD is enabled, instead of writing a stored copy that did nothing then and would have become the client if CIMD were later turned off.deleteClient()on a CIMD client revokes its grants, as before, and is now documented as doing so. -
#363
958d792Thanks @mattzcarey! -deleteClient()deletes the client before revoking its grants, so the client stops working at once even if revocation fails partway; calling it again finishes the revocation. It finds the client's grants from their KV key metadata instead of reading every grant in the namespace one by one, which could exceed a Worker's subrequest limit and, because the client record was deleted last, leave the client active. -
#366
0cecdbaThanks @mattzcarey! - WithdisallowPublicClientRegistration, a dynamic registration that prefersnonebut also supports a secret method is registered with the secret method instead of being refused.noneis simply not on offer during negotiation; a client that supports onlynoneis still refused. -
#371
a6d99adThanks @mattzcarey! - A grant issued without a refresh token (refreshTokenTTL: 0, from the option or atokenExchangeCallback) now expires with its access token. It used to be stored with no expiry, so every such authorization left a grant record in KV for good. -
#365
3c940ffThanks @mattzcarey! - Six internal helpers are no longer exported from the package:base64UrlToBytes,parseJwtJsonPart,getJwtCryptoAlgorithms,isValidOAuthScopeToken,resourceMatchesandvalidateResourceUri. They were exported by accident in 1.0 (the first three only so the enterprise-managed authorization modules could import them back from the package entry) and were never documented. A test now pins the package's runtime exports. -
#342
696045aThanks @mattzcarey! - A bearer token in this provider's ownuserId:grantId:secretformat that isn't found in storage (an expired or revoked access token, or a forgery of the format) is now answeredinvalid_tokendirectly instead of being handed toresolveExternalToken. Resolvers that validate foreign credentials upstream were forwarding our expired tokens to third-party APIs: about 24k calls an hour in one production deployment, from MCP clients presenting a stale access token before refreshing. -
#362
23c151eThanks @mattzcarey! -purgeExpiredData()is resumable. It returns acursorwhenever the sweep isn't finished, and takes it back asPurgeOptions.cursor. Previously every invocation started from the first grant, so with more thanbatchSizelive grants a scheduled sweep re-checked the same records forever, never reached later grants, and never swept orphaned tokens at all. Persist the cursor between runs; the example indocs/advanced-configuration.mdstores it in KV. -
#368
bf61067Thanks @mattzcarey! - A token request whosescopenames only scopes the grant doesn't hold is refused withinvalid_scope(RFC 6749 §6) at code exchange, refresh and token exchange, instead of succeeding with a token that carries no scope. Requests that name at least one granted scope are still narrowed silently, as before. -
#374
fe5e1e2Thanks @mattzcarey! - Dynamic client registration stops reading a request body at 1 MiB even when it has noContent-Length(a chunked body), instead of reading all of it first. WhenclientRegistrationCallbackthrows, the client gets a fixedClient registration callback faileddescription rather than the error's message, which may be internal;onErrorstill receives the error. -
#344
19c4d96Thanks @mattzcarey! -completeAuthorization()rejects auserIdcontaining:, as the enterprise-managed authorization mapper already did.:separates the parts of issued tokens and grant keys, so such a user's tokens could never be validated, and its grant keys (grant:a:b:…) matched another user'sgrant:a:prefix.listUserGrants()and the revocation of earlier grants now also skip those legacy keys, so a stored grant for usera:bnever appears in, or is revoked by, usera's operations. -
#369
a8843d8Thanks @mattzcarey! - The implicit grant is removed, along with theallowImplicitFlowoption. OAuth 2.1 dropped it and MCP requires the authorization code flow with PKCE.response_type=tokenis now always answered withunsupported_response_type, and passingallowImplicitFlow: truethrows at construction instead of being silently ignored. Implicit grants were stored without an expiry, so each implicit authorization left a grant record in KV for good. -
#370
5234cc8Thanks @mattzcarey! - TheallowPlainPKCEoption is removed; only S256 PKCE is accepted, as MCP requires. PassingallowPlainPKCE: truethrows at construction instead of being silently ignored. An authorization code issued with a plain challenge before the upgrade is refused at the token endpoint withinvalid_grant. -
#375
9db9a62Thanks @mattzcarey! - TherevokeExistingGrantsBatchSizeoption ofcompleteAuthorization()is removed. Since 1.0 every grant carries key metadata, so earlier grants are found without reading them; the option only set how many pre-1.0 grants were read at once, and those disappear as they refresh. They are now read 50 at a time. -
#376
baf2787Thanks @mattzcarey! - CORS headers an API handler sets itself (Access-Control-Allow-Origin,-Methods,-Headers,-Max-Age) are kept instead of being overwritten, so a handler can narrow its own CORS policy;Vary: Originand the exposedWWW-AuthenticateandRetry-Afterheaders are still added.OAuthProviderandOAuthResourceServernow share one implementation of CORS, header merging and resource scope filtering. -
#373
9e97803Thanks @mattzcarey! - A token from an allowed cross-client token exchange is issued to the requesting client (RFC 8693) instead of being recorded as the subject token's client.ctx.auth.clientIdandunwrapToken()now name the client that holds the token, and that client can revoke it; previously revocation failed its ownership check. The token still lives under the subject's grant, so revoking the grant removes it. -
#364
cd28310Thanks @mattzcarey! -refreshTokenTTLandclientRegistrationTTLare validated at construction, likeaccessTokenTTL. A value Cloudflare KV can't store (under 60 seconds, or not an integer) used to surface only at runtime, as a 500 on every code exchange or every dynamic client registration.refreshTokenTTLaccepts0(no refresh tokens),undefined(no expiry) or an integer of at least 60;clientRegistrationTTLacceptsundefined(no expiry) or an integer of at least 60.0is no longer accepted forclientRegistrationTTL, where it used to be treated as "no expiry" in one place and as a TTL in another.