Blog post: Better Auth 1.6
better-auth
Features
- feat(two-factor): include enabled 2fa methods in sign-in redirect response
The 2FA sign-in redirect now returns twoFactorMethods (e.g. ["totp", "otp"]) so frontends can render the correct verification UI without guessing. The onTwoFactorRedirect client callback receives twoFactorMethods as a context parameter.
- TOTP is included only when the user has a verified TOTP secret and TOTP is not disabled in config.
- OTP is included when
otpOptions.sendOTPis configured. - Unverified TOTP enrollments are excluded from the methods list. (#8772)
Bug Fixes
- fix(next-js): replace cookie probe with header-based RSC detection in
nextCookies()to prevent infinite router refresh loops and eliminate leaked__better-auth-cookie-storecookie. Also fix two-factor enrollment flows to set the new session cookie before deleting the old session. (#9059) - fix(oauth2): prevent cross-provider account collision in link-social callback
The link-social callback used findAccount(accountId) which matched by account ID across all providers. When two providers return the same numeric ID (e.g. both Google and GitHub assign 99999), the lookup could match the wrong provider's account, causing a spurious account_already_linked_to_different_user error or silently updating the wrong account's tokens.
Replaced with findAccountByProviderId(accountId, providerId) to scope the lookup to the correct provider, matching the pattern already used in the generic OAuth plugin. (#8983)
- security: verify OAuth state parameter against cookie-stored nonce to prevent CSRF on cookie-backed flows (#8949)
auth
⚠️ Breaking Changes
BREAKING: fix(two-factor): prevent unverified TOTP enrollment from gating sign-in
Adds a verified boolean column to the twoFactor table that tracks whether a TOTP secret has been confirmed by the user.
- First-time enrollment:
enableTwoFactorcreates the row withverified: false. The row is promoted toverified: trueonly afterverifyTOTPsucceeds with a valid code. - Re-enrollment (calling
enableTwoFactorwhen TOTP is already verified): the new row preservesverified: true, so the user is never locked out of sign-in while rotating their TOTP secret. - Sign-in:
verifyTOTPrejects rows whereverified === false, preventing abandoned enrollments from blocking authentication. Backup codes and OTP are unaffected and work as fallbacks during unfinished enrollment.
Migration: The new column defaults to true, so existing twoFactor rows are treated as verified. No data migration is required. skipVerificationOnEnable: true is also unaffected — the row is created as verified: true in that mode. (#8711)
@better-auth/oauth-provider
Bug Fixes
-
fix(oauth-provider): preserve multi-valued query params through prompt redirects
-
serializeAuthorizationQuerynow usesparams.append()for array values instead ofString(array)which collapsed them into a single comma-joined entry. -
deleteFromPromptreturn type widens fromRecord<string, string>toRecord<string, string | string[]>. The previous type was incorrect —Object.fromEntries()silently dropped duplicate keys, so the narrower type only held because the data was being corrupted. (#9060) -
Typescript specifies skip_consent type never and errors through zod (#8998)
@better-auth/sso
Bug Fixes
-
fix(sso): include RelayState in signed SAML AuthnRequests per SAML 2.0 Bindings §3.4.4.1
-
RelayState is now passed to samlify's ServiceProvider constructor so it is included in the redirect binding signature. Previously it was appended after the signature, causing spec-compliant IdPs to reject signed AuthnRequests.
-
authnRequestsSigned: truewithout a private key now throws instead of silently sending unsigned requests. (#9058) -
fix(sso): strip whitespace from SAMLResponse before base64 decoding
Some SAML IDPs send SAMLResponse with line-wrapped base64 (per RFC 2045), which caused decoding failures. Whitespace is now stripped at the request boundary before any processing. (#8968)
Contributors
Thanks to everyone who contributed to this release:
@aarmful, @cyphercodes, @dvanmali, @gustavovalverde, @jaydeep-pipaliya, @ping-maxwell
Full changelog: v1.6.1...v1.6.2