Important
Action required
- If you use a read replica for your verification store, make sure it reads its own writes, because every setup now writes a verification row per revoked grant and a lagging replica can let in-flight requests keep their tokens. (#12581)
- If you want
requireMcpAuthto keep verifying JWT signatures alone instead of checking the provider's records, setjwksUrl. (#12597) - If you have a grant extension that passes
acttoissueTokens, setsub_profile: "service"on client actors, or pairwise clients will see those client IDs as pairwise values. (#12584) - If you have a
may_actthat names a service bysub, addsub_profile: "service"so it keeps its client ID. (#12584) - If you return
undefinedfromcustomAccessTokenClaimsor other claim sources to remove a claim set elsewhere, return a value such asnullinstead, because the lower-priority value now stays. (#12575) - If you have a custom
clientAuthenticationstrategy that comparesaudwithexpectedAudience, move it toacceptedAudiencesand pass that toconsumeClientAssertion. (#12599) - If you have clients that sign client assertions for an endpoint URL, update them to use the issuer as
aud, or setassertionAudience: "issuer-or-endpoint"temporarily to keep accepting them. (#12599) - If your authorization server accepts only its token endpoint URL as the assertion audience, set
audience: "token_endpoint". (#12599) - If you sign in to a Better Auth 1.8 authorization server with
private_key_jwt, setoidcConfig.clientAssertionAudience: "issuer". (#12599) - If you send UserInfo requests with an
Authorizationheader that has noBearerorDPoPscheme, send the token asAuthorization: Bearer <token>. If you handle the old400 invalid_scoperesponse, handle403 insufficient_scopeinstead. (#12605) - If your provider publishes discovery at a non-well-known path or under another name for the same issuer, set
issuerto the issuer identifier the provider documents. If its discovery document has noissuer, configureauthorizationUrl,tokenUrl, anduserInfoUrlinstead ofdiscoveryUrl. (#12585) - If you set
ba_subincustomAccessTokenClaims, an extension, or a resource'scustomClaims, rename your claim. (#12577) - If you call
requireActiveAccessTokenand thenenforceDpopBindingon an endpoint, replace both calls withprovider.verifyResourceRequest(), which checks the DPoP proof. (#12606) - If you pass
authorization_details_types_supportedtogetProtectedResourceMetadata, wrap the value in an array. (#12513) - If you match
body.error === "invalid_token"to catch a missing or unsupportedAuthorizationscheme, check for a 401 withoutbody.errorinstead. (#12512) - If you have exhaustive checks over
DpopBindingErrorCodeorDpopProofErrorCode, add a case foruse_dpop_nonceso the code typechecks. (#12511) - If you use CIBA, run
npx auth@latest migrate, ornpx auth@latest generateand apply the schema with your ORM, before you upgrade to add theacrValuesandauthorizationDetailscolumns to thecibaRequestmodel. (#12509) - If you use the OAuth provider, add the
requirePushedAuthorizationRequestscolumn to theoauthClienttable before deploying, even with PAR disabled, because requests fail until it exists: runnpx auth@latest migratewith the built-in Kysely adapter, ornpx auth@latest generateand apply the schema with your ORM. (#12518) - If you use the OAuth provider, run
npx auth@latest migratebefore upgrading, or runnpx auth@latest generateand apply the schema with your ORM's migration tool, to add theaccessTokenClaimscolumn tooauthAccessToken. (#12508) - If you use
oauthProvider(), before deploying runnpx auth@latest migratewith the built-in Kysely adapter, or runnpx auth@latest generateand apply the schema with your ORM, to add thegrantIdcolumns tooauthRefreshTokenandoauthAccessToken. (#12520) - If you have a custom
OAuthProviderApiimplementation, add thechallengeDpopNonce()method. (#12522)
better-auth
❗ Breaking Changes
- Changed
createPrivateKeyJwtClientAssertionGetterto sign client assertions for the authorization server's issuer by default when the issuer is known (#12599) - Changed Generic OAuth to check the discovery document's
issueragainstdiscoveryUrl, skipping providers whose issuer does not match (#12585)
Features
- Added
createDpopFetchinbetter-auth/oauth2/dpop-clientfor sending DPoP-bound requests with per-origin nonce handling and a single retry onuse_dpop_nonce(#12506) - Added
endpointTypeto custom token endpoint authentication hooks, andissuerto hooks and client assertion getters when Better Auth knows it (#12580) - Added a
dpop: { keyPair }option so Generic OAuth providers can request DPoP-bound tokens (RFC 9449) (#12596) - Added support for pushed authorization requests (RFC 9126) in Generic OAuth sign-in through the new
pushedAuthorizationRequestoption (#12519)
Bug Fixes
- Added
deriveEncryptionKey(secret, purpose)tobetter-auth/cryptoso plugins can derive a separate encryption key for each value they encrypt (#12577)
For detailed changes, see CHANGELOG
@better-auth/oauth-provider
❗ Breaking Changes
- Deprecated the
expectedAudiencestrategy input and parameter in favor ofacceptedAudiencesforclientAuthenticationstrategies andconsumeClientAssertion(#12599) - Changed client assertions such as
private_key_jwtto require the provider's issuer as their onlyaudvalue, rejecting endpoint URLs withinvalid_client(#12599) - Changed access-token claims set to
undefinedto be left out instead of removing a same-named claim from a lower-priority source (#12575) - Changed UserInfo to return
403 insufficient_scopefor access tokens without theopenidscope and to rejectAuthorizationheaders without aBearerorDPoPscheme (#12605) - Changed
requireActiveAccessTokento refuse DPoP-bound access tokens with a401 invalid_tokenand aDPoPchallenge instead of accepting them without checking a proof (#12606)
Features
- Changed token exchange to reject a subject token that carries
authorization_detailswithinvalid_request(#12509) - Added opt-in token exchange for DPoP-bound public clients through
oauthTokenExchange({ publicClients: "dpop-bound" })(#12517) - Added a pushed authorization request endpoint (
POST /oauth2/par, RFC 9126) to the OAuth provider, with optional global or per-client enforcement (#12518) - Added a
clientIdoption toprovider.authenticateClient()so plugin endpoints can authenticate follow-up requests that omitclient_id(#12529) - Added an
expectedJktoption toprovider.verifyDpopProof()so plugin endpoints can require a later request to use the same DPoP key as an earlier one (#12529) - feat(oauth-provider): bring DPoP challenges in line with Bearer (#12579)
- Added the ability for
adminUpdateOAuthClientto grant or revoke RP-initiated logout on any client, including ownerless ones, by sending onlyenable_end_session(#12588) - Fixed
updateOAuthClientto reject newpost_logout_redirect_urisentries on another origin than the client'sredirect_uris, for clients that may use RP-initiated logout (#12588) - Changed OAuth provider discovery to publish combined, de-duplicated lists when multiple extensions advertise the same list, such as
authorization_details_types_supported(#12564) - Added
issueAuthorizationCodeto the OAuth provider API so plugin endpoints can issue authorization codes for a signed-in user without a redirect (#12565) - Changed
/oauth2/authorizeand/oauth2/parto reject requests withauthorization_detailsusinginvalid_authorization_detailswhen discovery advertises supported types (#12591) - Added support for extension grants to issue RFC 9396
authorization_detailsthroughissueTokensin token responses, JWT access tokens, and introspection (#12591) - Added
challengeDpopNonce()so plugin endpoints and before hooks can issue a nonce-only DPoP challenge before staging state (#12522) - Improved OAuth provider extension conflict errors and warnings to name the extensions involved, with an optional
namefor extensions (#12646) - Added opt-in revocation of JWT access tokens at
/oauth2/revokewithrevocableJwtAccessTokens: true(#12507) - Added
resolveSubject({ userId, client })to the provider API so grant extensions and companion plugins can get thesuba client sees for a user (#12505) - Added opt-in pairwise
subin JWT access tokens for pairwise clients withaccessTokenSubject: "pairwise", so decoding a token no longer reveals the user ID (#12505) - Added a
clientRegistrationEnableEndSessionoption to allow RP-initiated logout at/oauth2/end-sessionfor dynamically registered and other registered OAuth clients (#12510) - Fixed introspection of delegated access tokens to show pairwise clients the pairwise
subof each user inactinstead of user IDs (#12523) - Added
accessTokenClaimsfromissueTokensto opaque access token introspection, so custom grants now see those claims (#12508) - Added
sub_profileto each actor in a delegated token'sactclaim, marking clients as"service"and users as"user"(#12584) - Changed token exchange to deny subject tokens whose
may_actnames no actor, withinvalid_request, instead of allowing any actor (#12584) - Fixed introspection and pairwise JWT access tokens exposing the real user ID in
may_act.subto pairwise clients (#12584) - Added revocation of a grant's JWT access tokens when its refresh token is revoked or a code or rotated token is replayed, with
revocableJwtAccessTokens(#12520) - Changed refresh token revocation at
/oauth2/revoketo delete every opaque access token of its grant, including those issued before earlier refreshes (#12520) - Added
verifyResourceRequestto verify JWT and opaque access token requests in-process on plugin endpoints served by the auth server (#12516) - Added a warning when discovery ignores a metadata contribution from an OAuth provider extension or client discovery (#12574)
- Improved discovery metadata warnings to log once per server instead of on every request to the well-known endpoints (#12574)
Bug Fixes
- Fixed
authorization_details_types_supportedin protected resource metadata to be typed asstring[], matching RFC 9728 (#12513) - Changed the OAuth provider to reserve the
ba_subaccess token claim, dropping custom values for it with a warning (#12577) - Fixed token requests with an already-used DPoP proof consuming an unused authorization code or device code, so clients can retry with a fresh proof (#12600)
- Fixed refresh token revocation and replay handling to retry refresh-token family teardown when a delete fails, such as after a MySQL deadlock (#12643)
- Fixed authorization code reuse to revoke issued tokens only when the request comes from the original client, including its
code_verifierfor PKCE codes (#12526) - Fixed OAuth grant revocation to also stop token requests for that grant that are still issuing tokens (#12581)
For detailed changes, see CHANGELOG
@better-auth/core
❗ Breaking Changes
- Changed
createPrivateKeyJwtClientAssertionGetterto sign client assertions for the authorization server's issuer by default when the issuer is known (#12599)
Features
- Added
createDpopFetchinbetter-auth/oauth2/dpop-clientfor sending DPoP-bound requests with per-origin nonce handling and a single retry onuse_dpop_nonce(#12506) - Added an
issueroption tovalidateAuthorizationCode,refreshAccessToken,clientCredentialsToken, andpushAuthorizationRequestfor client assertion getters (#12580) - Added
endpointTypeto custom token endpoint authentication hooks, andissuerto hooks and client assertion getters when Better Auth knows it (#12580) - Added a
fetchoption tovalidateAuthorizationCode,refreshAccessToken,clientCredentialsToken, andpushAuthorizationRequestso requests can send DPoP proofs (#12596) - Added
pushAuthorizationRequestto@better-auth/core/oauth2for sending pushed authorization requests (RFC 9126) and getting the redirect URL (#12519)
Bug Fixes
- Added
use_dpop_nonceto theDpopBindingErrorCodeandDpopProofErrorCodetypes exported from@better-auth/core/oauth2(#12511) - Fixed
verifyAccessTokenRequestto reject a missing or unsupportedAuthorizationscheme without aninvalid_tokenerror, matching RFC 6750 (#12512)
For detailed changes, see CHANGELOG
@better-auth/mcp
❗ Breaking Changes
- Changed
requireMcpAuthto check access tokens against the OAuth provider's records when it runs in the same process, accepting opaque tokens and rejecting revoked ones (#12597)
For detailed changes, see CHANGELOG
@better-auth/sso
❗ Breaking Changes
- Added
oidcConfig.clientAssertionAudienceso OIDC SSO providers usingprivate_key_jwtcan sign client assertions for the provider's registered issuer (#12599)
For detailed changes, see CHANGELOG
@better-auth/ciba
Features
- Added support for
acr_valuesandauthorization_detailsin CIBA requests to/oauth2/bc-authorize(#12509) - Added
authorization_details_typesto client registration so CIBA rejects requests for unregistered authorization details types from clients that set the field (#12591) - Improved OAuth provider extension conflict errors and warnings to name the extensions involved, with an optional
namefor extensions (#12646)
Bug Fixes
- Fixed CIBA approval links and the advertised
backchannel_authentication_endpointcontaining a double slash whenbaseURLends in/(#12638)
For detailed changes, see CHANGELOG
@better-auth/cimd
Features
- Fixed
cimd()startup failing with a duplicate client discovery error when itsinit()runs more than once for the sameoauthProvider()instance (#12646) - Improved OAuth provider extension conflict errors and warnings to name the extensions involved, with an optional
namefor extensions (#12646)
For detailed changes, see CHANGELOG
Contributors
Thanks to everyone who contributed to this release:
Full changelog: v1.8.0-beta.9...v1.8.0-beta.10