docs(two-factor): document newSession is null during 2FA challenge (#9957)

This commit is contained in:
Paola Estefanía de Campos
2026-06-09 13:15:00 -07:00
committed by GitHub
parent 6a8dfa4f3e
commit afcb4dd7f3
3 changed files with 18 additions and 1 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"better-auth": patch
---
Document that `ctx.context.newSession` is `null` while a two-factor challenge is in flight. When a 2FA-enabled user signs in through a credential endpoint, the pending session is discarded and `newSession` is reset to `null` (no authenticated session exists until the second factor is verified). Server-side `after` hooks reading `ctx.context.newSession` must null-check it before accessing `newSession.user`. This clarifies the behavior change introduced in #9639.
+4
View File
@@ -120,6 +120,10 @@ By default, 2FA sign-in enforcement applies to the credential-based sign-in endp
If your app needs to require 2FA for those sign-in methods, add custom hook handling for those endpoints and redirect users into your 2FA verification flow before treating the sign-in as complete.
<Callout type="warn">
When a 2FA-enabled user signs in via a credential endpoint, the plugin issues a 2FA challenge instead of completing the sign-in. As part of this, the pending session is discarded and `ctx.context.newSession` is reset to `null` — there is no authenticated session until the user verifies the second factor. Server-side hooks (e.g. `after` hooks on the sign-in endpoints) that read `ctx.context.newSession` must null-check it before accessing `newSession.user`, otherwise they will throw while a 2FA challenge is in flight.
</Callout>
You can handle this in the `onSuccess` callback or by providing a `onTwoFactorRedirect` callback in the plugin config.
```tsx
@@ -469,7 +469,15 @@ export const twoFactor = <O extends TwoFactorOptions>(options?: O) => {
}
/**
* remove the session cookie. It's set by the sign in credential
* Remove the session cookie set by the credential sign-in.
*
* The credential handler already created a session and set
* `ctx.context.newSession`. Since 2FA is still pending, that
* session is deleted here and `newSession` is reset to `null`
* so downstream hooks don't observe a session that no longer
* exists. Hooks that read `ctx.context.newSession` after a
* sign-in must therefore null-check it: it is `null` while a
* 2FA challenge is in flight (no authenticated session yet).
*/
deleteSessionCookie(ctx, true);
await ctx.context.internalAdapter.deleteSession(data.session.token);