Without any
signInGate config the gate still enforces a forced password change, and it requires no second factor. Everything else is opt in.
Turn it on
A user is only challenged when they have a verifiable factor enabled. TOTP counts when you set
totp or use the theauth-2fa plugin. With no enrolled factor the user signs in as before, so turning the gate on does not lock out anyone who never enrolled.
Challenges are signed with
signInGate.secret, falling back to auth.session.secret. With neither, a random secret is generated per process, and challenges stop verifying across restarts and instances. Set one of them.Forced password change
Flag an account with the admin module:password_change_required instead of a session. The flag wins over 2FA, and after the change the gate is evaluated again, so a user with TOTP is still asked for a code.
For a method other than password, the flag only blocks users who actually have a password credential. A social-only account is never locked out by it.
The flag clears on a completed password change or reset. Any password change also revokes every trusted device of that user.
Where it applies
username.signIn and magicLink.verify throw SignInGateError (exported from @glinr/theauth/auth) when the gate does not allow a session. Pass the request as the second argument of signIn so the trusted-device cookie can be read. signInGateResponse(err) turns the error into the standard HTTP answer.
Response shapes
A forced password change answers403:
200, with no session and no cookie:
twoFactorRequired in the body, not just the status. The existing “Password reset required” error message is unchanged.
OAuth redirects
A browser redirect cannot carry a JSON body, and putting a challenge in the query string would send it to servers and referrers. The OAuth callback redirects to{baseUrl}/ and puts the challenge in the URL fragment:
location.hash in your page, then post the challenge to the routes below. The redirect target is fixed to the base URL root. Use signInGateRedirect(location, decision) if you build your own callback.
Routes
The gate adds its routes whenauth.session is configured. A plugin with id theauth-sign-in-gate of your own replaces the default one.
Success on the first two returns
200 with { user: { id }, session: { token, expiresAt } } and sets the session cookie. When trustDevice: true is sent and trusted devices are enabled, the device cookie is set as well. A password change that still needs a second factor returns the twoFactorRequired body with a new challenge instead.
Failures:
Trusted devices
After a successful second factor withtrustDevice: true, the server issues a random secret in an httpOnly cookie and stores an HMAC fingerprint of it. Later sign-ins that present that cookie skip the 2FA step. Trust is tied to the cookie secret, not to the user agent or IP, so a spoofed header cannot skip the check.
This is separate from the standalone trusted device module, which you wire into your own sign-in flow. The gate manages its own devices through the routes above.
Session limit
auth.session.maxConcurrent caps non-expired sessions per user inside sessionManager.create, so every path that issues a session respects it, including the gate’s routes.
Impersonation sessions are neither counted nor limited. The gate’s routes answer
429 when reject fires. The multi-session module is a separate, standalone limiter.
Limits
These are the places the gate is not yet as strong as it looks.- Counters live in process memory. Wrong-code attempts and used challenges are tracked per process. Behind several instances, the attempt limit is effectively multiplied by the instance count, and a used challenge can be presented once more to another instance until it expires (default five minutes). A restart resets the counters. The map is cleared when it grows past 10,000 entries.
- Not every sign-in path is gated. SIWE has no session-issuing path in this library, so the
siwemethod name is reserved and has no call site. Anonymous sessions and admin impersonation are left ungated on purpose, because they do not check a user credential. SSO sign-in is not listed in the table above and is not gated. - Stateless sessions are gated only when you pass
signInGate.createStatelessSessionsandcreateJwtSessionModuledo not go throughsessionManager.create, somaxConcurrentandonRevokedo not apply to them. The gate usessubas the user id, so it only has an effect whensubis a local user id. - Forced password change reaches every gated method for users who have a password credential, including phone, email OTP, one-tap and device sign-ins, even when
require2faForis not set. Social-only accounts that an admin flagged are not locked out. - Passkey as a second factor is available through
createPasskeyTwoFactorFactor(passkeyModule)insignInGate.factors. Do not also listpasskeyinrequire2faFor, or the user is asked for a passkey twice. - The OAuth redirect target is fixed to
{baseUrl}/.
Related
Two-factor authentication
Enroll TOTP and backup codes.
Admin
Flag an account for a forced password change.
Trusted devices
The standalone fingerprint module.
OIDC provider logout
Fan out onRevoke to relying parties.