Authentication Flows#
This page walks through every authentication flow in AuthHero step by step, showing the sequence of API calls, what happens on the server, and what the frontend does at each stage.
Registration Flow#
User submits the registration form
Frontend sends POST /auth/register with fullname, email, and password.
Server validates and creates the user
The Zod schema validates the input. If the email already exists, an error with code EMAIL_ALREADY_EXISTS is returned. Otherwise, the password is hashed with argon2 and a new User record is created.
Verification email is queued
A random 72-hex-character token is generated. Its SHA-256 hash is stored in the EmailVerification table. The plaintext token is passed to BullMQ, which sends the email asynchronously (non-blocking).
User clicks the email link
The link points to {FRONTEND_URL}/verify-email?token={token}. The frontend extracts the token and sends POST /auth/verify-email.
Server verifies the token
The server hashes the received token, looks it up in the database, checks it hasn't expired or been used, then flips emailVerified to true.
If the token expires, the user must request a new one (re-register or trigger a verification resend).
Email/Password Login Flow#
Without MFA#
User submits login form
Frontend sends POST /auth/login with email and password.
Server verifies credentials
Looks up user by email. Checks emailVerified (returns EMAIL_NOT_VERIFIED if false). Verifies password against stored argon2 hash using constant-time comparison.
Session created
A random 80-hex-char refresh token is generated. Its SHA-256 hash is stored in the Session table. A 15-minute JWT access token is signed with ACCESS_TOKEN_SECRET containing { userId, sessionId }.
Tokens returned
The access token is in the JSON response body. The refresh token is set as an httpOnly, secure, SameSite=strict cookie (never exposed to JavaScript).
With MFA Enabled#
User submits login form
Same as above.
Server detects MFA is enabled
After password verification succeeds, the server checks user.mfaEnabled. Instead of creating a session, it generates a 5-minute MFA temp token signed with MFA_TEMP_TOKEN_SECRET.
Frontend receives MFA challenge
Response: { mfaRequired: true, tempToken: "..." }. The frontend stores the tempToken in Zustand state and redirects to the /mfa challenge page.
User enters TOTP code
Frontend sends POST /auth/mfa/challenge with tempToken and the 6-digit code (or 8-char backup code).
Server verifies and creates session
The temp token is decoded to extract userId. The TOTP code is verified against the decrypted secret. If using a backup code, the matching hash is compared with argon2 and deleted. On success, a full session is created and tokens are returned.
OAuth Login Flow#
User clicks a social login button
Frontend opens GET /auth/oauth/:provider (e.g., /auth/oauth/google). The server generates a random CSRF state, stores it in an httpOnly cookie, and redirects the user to the provider's consent screen.
User grants permission
The OAuth provider redirects back to GET /auth/oauth/callback/:provider?code=...&state=....
Server validates the callback
The CSRF state from the cookie is compared with the query parameter. The authorization code is exchanged for an access token with the provider, then the user's profile (email, name, provider ID) is fetched.
User is synced with the database
In a Prisma transaction: if an OAuthAccount exists, the linked user is returned. If a user with that email exists, the OAuth account is linked. Otherwise, a new User + OAuthAccount are created.
One-time code redirect
The server does NOT put tokens in the redirect URL (they would leak via browser history and Referer headers). Instead, tokens are stored in Redis behind a random one-time code. The user is redirected to {FRONTEND_URL}/auth/callback?code={oneTimeCode}.
Frontend exchanges the code
The callback page sends POST /auth/oauth/exchange with the one-time code. The server looks it up in Redis, deletes it (one-time use), and returns the access token + sets the refresh cookie.
If the OAuth user has MFA enabled, the one-time code resolves to a temp token instead. The frontend then shows the MFA challenge page (same flow as email/password MFA).
Token Refresh Flow#
Access token expires
The Axios interceptor in the frontend detects a 401 response (access token expired or invalid).
Automatic refresh
The interceptor sends POST /auth/refresh-token with withCredentials: true (sends the httpOnly refresh cookie automatically).
Token rotation
The server hashes the received refresh token, looks up the session, verifies it's not revoked or expired. Then it generates a new refresh token, updates the session with the new hash, and returns a new access token.
Reuse detection
If the old refresh token is reused after rotation (meaning someone stole it), the hash won't match. The server immediately revokes all sessions for that user and returns SESSION_REVOKED.
If AuthHero detects refresh token reuse, it assumes the token was stolen. The nuclear option (revoking all sessions) forces the attacker and the real user to re-authenticate, which is the safest response.
Password Reset Flow#
User clicks 'Forgot password'
Frontend sends POST /auth/forgot-password with email. The response always says "If an account exists, a reset link has been sent." to prevent email enumeration.
Reset email is sent (if user exists)
A 72-hex-character token is generated. Its SHA-256 hash is stored in PasswordReset. The plaintext token is sent via BullMQ email worker. Link format: {FRONTEND_URL}/reset-password?token={token}.
User clicks the link and sets a new password
Frontend sends POST /auth/reset-password with token and newPassword.
Server resets the password
Validates the token (not expired, not used), hashes the new password with argon2, updates the user, marks the token as used, and revokes all existing sessions for security.
Token Lifecycle Summary#
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā LOGIN (no MFA) ā
ā ā
ā Client Server ā
ā ā ā ā
ā āā POST /auth/login āāāāāāāŗ Verify credentials ā
ā ā ā Create Session in DB ā
ā ā ā Sign access token (JWT) ā
ā āā { accessToken } āāāāāā⤠Set refreshToken cookie ā
ā ā ā ā
ā ā ... 15 min later ... ā ā
ā ā ā ā
ā āā GET /auth/me āāāāāāāāāāāŗ 401 (token expired) ā
ā ā ā ā
ā āā POST /refresh-token āāāāŗ Rotate refresh token ā
ā ā (cookie sent auto) ā Sign new access token ā
ā āā { accessToken } āāāāāā⤠Update session hash ā
ā ā ā ā
ā āā GET /auth/me āāāāāāāāāāāŗ 200 (works!) ā
ā ā (retry original req) ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā LOGIN (with MFA) ā
ā ā
ā Client Server ā
ā ā ā ā
ā āā POST /auth/login āāāāāāāŗ Verify credentials ā
ā ā ā Detect mfaEnabled ā
ā āā { mfaRequired, ā Sign MFA temp token ā
ā ā tempToken } āāāāāāāāā⤠(5-min lifetime) ā
ā ā ā ā
ā ā Redirect to /mfa page ā ā
ā ā ā ā
ā āā POST /mfa/challenge āāāāŗ Verify tempToken ā
ā ā { tempToken, code } ā Decrypt TOTP secret ā
ā ā ā Verify the 6-digit code ā
ā ā ā Create Session in DB ā
ā āā { accessToken } āāāāāā⤠Set refreshToken cookie ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā