Adding MFA to your TanStack Start app
A complete guide to TOTP authentication and step-up verification in TanStack Start, so your most sensitive server functions stay protected even hours after sign-in.
Most MFA guides stop at the login screen. Enable the feature, scan the QR code, enter the six-digit code, done. That is the easy part.
The harder question is what to do after login. Should a user who authenticated 12 hours ago with MFA be allowed to delete their account? Transfer funds? Change their email address? MFA at login gives you a stronger initial authentication signal. It does not protect you from session hijacking, stolen cookies, or a compromised device that stays authenticated for days.
This guide covers MFA in TanStack Start from both angles: how to add it to the login flow, and how to enforce step-up authentication on specific server functions so that high-stakes operations require a fresh verification regardless of when the user last signed in.
What MFA actually protects against
MFA adds a second factor to authentication: something the user knows (password) plus something they have (an authenticator app). Even if an attacker has the user's password, they cannot complete login without the current TOTP code from the user's device.
What MFA does not protect against: a valid session that was stolen after a successful MFA login. If an attacker gets hold of a session cookie through XSS, a network interception, or physical access to a device, they inherit whatever the session allows. This is why step-up authentication exists: for the most sensitive operations, require a fresh MFA code at the point of the action, not just at the point of login.
The combination of MFA at login plus step-up authentication on high-risk server functions covers both attack surfaces.
Why not SMS?
When developers think MFA, they often think SMS: send a six-digit code to a phone number, user enters it, done. SMS MFA is better than no MFA, but it has serious structural weaknesses that make it a poor default for any application handling sensitive data.
- SIM swapping is the most direct attack: an attacker convinces a mobile carrier's customer support to transfer the victim's phone number to a new SIM under the attacker's control. Once they have the number, they receive every SMS, including OTPs. This attack does not require technical skill, only social engineering, and it has been used to drain cryptocurrency wallets, compromise developer accounts, and bypass MFA at major companies.
- SMS is not end-to-end encrypted. OTP codes can be intercepted in transit over the cellular network, by malware on the user's device, or through APIs that SMS aggregators expose to third parties.
- Real-time phishing kits can capture SMS OTPs automatically. A user enters credentials on a convincing fake login page, the attacker triggers a real MFA prompt, the user receives and enters the genuine SMS code on the fake page, and the attacker completes the login within seconds. SMS MFA is not phishing-resistant.
- Phone numbers are not stable identifiers. Carriers recycle numbers. A user who changes providers loses their number. If a recycled number reaches a new owner, that person could receive OTPs for services tied to the old owner's account.
NIST deprecated SMS as a secure MFA method in their 2017 revision of Special Publication 800-63 and has not reversed that position. Enterprise security reviewers routinely flag SMS MFA as a compliance concern.
WorkOS AuthKit does not support SMS MFA for these reasons. The supported method is TOTP via an authenticator app: Google Authenticator, Authy, 1Password, or any app that supports the TOTP standard. TOTP codes are generated on-device, never transmitted over a network until the user enters them, and are time-limited to 30-second windows. They are phishing-resistant in a way that SMS is not: a code entered on a fake site is expired by the time an attacker could use it.
How MFA works in TanStack Start
In TanStack Start, authentication lives in server functions and the session. MFA adds a layer on top of both.
At login, WorkOS AuthKit handles the MFA challenge as part of the hosted authentication flow. If the user has MFA enabled (or if MFA is required globally), they see a TOTP prompt after entering their password. AuthKit handles the TOTP validation and only creates a session after both factors are verified.
For step-up authentication, WorkOS now ships a first-class mechanism: every access token carries an auth_time claim (a Unix timestamp of the user's last active authentication), and the authorization endpoint accepts a max_age parameter. When you want to gate a sensitive operation, you check auth_time from the token against your threshold. If the session is stale, you redirect to /user_management/authorize with max_age set to the required freshness window. WorkOS re-challenges the user using whatever method they originally authenticated with and issues fresh tokens with an updated auth_time. The session ID stays the same throughout.
Enabling MFA with WorkOS AuthKit
The hosted flow
For most applications, enabling MFA is a single dashboard toggle. In the WorkOS dashboard under Authentication, enable Multi-Factor Authentication. From that point, new and existing users are required to enroll in TOTP with an authenticator app before they can sign in.
AuthKit handles the full enrollment UX: it prompts the user to scan a QR code with their authenticator app, asks them to verify they can generate codes correctly, and only completes login after successful verification. On subsequent logins, it prompts for the current TOTP code after the password step.
The MFA requirement does not apply to SSO users. Enterprise users who authenticate through their company's identity provider are governed by that provider's MFA policies, not yours.
Your TanStack Start code does not change for the hosted flow. The session established after a successful MFA login is identical in structure to a session from password-only login. The difference is that auth.authenticationMethod will reflect the additional factor.
Letting users manage their own MFA
If you want to give users control over their own MFA enrollment rather than making it mandatory globally, the WorkOS MFA API lets you trigger enrollment programmatically. A user clicks "Enable two-factor authentication" in your settings UI, you call the enrollment endpoint, and AuthKit walks them through the QR code scan.
The enrollment response includes a base64-encoded QR code image and a plain-text secret. Show the QR code to the user so they can scan it with their authenticator app. Show the secret as a fallback for users who cannot scan QR codes. After enrollment, ask them to enter a code to confirm the setup worked before saving the factor.
Listing enrolled factors
To show users their current MFA status, or to let them manage or remove enrolled factors, use the list endpoint:
Step-up authentication on server functions
This is the part most MFA guides skip. Once a user is signed in with MFA, subsequent sensitive operations should not just trust the session. A session that is eight hours old is not the same security assurance as one from five minutes ago.
WorkOS now ships step-up authentication as a first-class feature. Every access token carries an auth_time claim: the Unix timestamp of the user's last active authentication. Refreshing the token does not bump auth_time. Only a real interactive re-authentication does. This gives you a reliable signal for freshness that does not depend on session metadata you manage yourself.
Step-up authentication is the right pattern for:
- Deleting an account or organization
- Changing an email address or password
- Transferring ownership
- Approving a payment or financial transaction
- Viewing sensitive data like API keys or credentials
- Any action that is difficult or impossible to reverse
Checking auth_time in a server function
The flow is:
- Your server function reads
auth_timefrom the access token and checks it against your threshold. - If the session is stale, you return a signal to the client to trigger re-authentication.
- The client redirects to
/user_management/authorizewithmax_ageset to your freshness window in seconds. - WorkOS re-challenges the user using whatever method they originally authenticated with: TOTP prompt for MFA users, password screen for password users, provider redirect for social and SSO users.
- After re-authentication, WorkOS issues fresh tokens with an updated
auth_timeand redirects back to your callback. - The client retries the original action with the fresh session.
The session ID (sid) is unchanged throughout. The user does not have to log in again, only re-verify.
Triggering the re-authentication redirect
When a server function returns requiresStepUp: true, the client redirects to the WorkOS authorization endpoint with max_age. WorkOS handles the challenge UI entirely. After re-authentication, the user is redirected to your callback route, the session tokens are refreshed with a new auth_time, and the client can retry the original action.
The returnTo value in the step-up URL's state parameter lets your callback redirect the user back to the page they were on after re-authentication completes. Wire it up in your callback handler:
Using max_age=0 to always force re-authentication
For the most sensitive operations, you may want to require re-authentication regardless of how recently the user last authenticated. Pass max_age=0 to always trigger a fresh challenge:
This is the right choice for payment confirmation, account deletion, or any action where you want a guaranteed fresh identity signal rather than a freshness window.
MFA for organizational policies
For B2B applications, MFA enforcement often needs to be per-organization rather than global. Some customers require MFA for all users. Others leave it optional. Still others enforce it only for admin users.
WorkOS organization policies let you set MFA requirements at the organization level. Navigate to an organization in the WorkOS dashboard and set the authentication policy. Users in that organization will be required to complete MFA on login regardless of the global setting.
This gives enterprise customers a way to enforce their own security policies without your team managing it per-tenant. Their IT admin can toggle MFA requirements for their organization through the WorkOS Admin Portal, and the enforcement happens automatically in the authentication flow.
Handling MFA in server function middleware
If you have many server functions that require step-up authentication, a middleware approach removes the duplication. Rather than calling checkAuthFreshness in every handler, you can create a middleware that performs the check and passes the verified user through context.
Because step-up authentication requires a client-side redirect rather than a thrown error, the middleware is best used for operations where you want a hard server-side rejection rather than a graceful redirect. For the redirect pattern, keep the checkAuthFreshness call in the handler and return requiresStepUp: true as shown above.
For operations where you want the server to reject the request outright if auth_time is stale (for example, in an API context where there is no redirect possible), use middleware that throws:
Letting users see and manage their factors
A good security settings page shows users which factors are enrolled and lets them remove them. Do not let users remove their only enrolled factor without confirming they understand they will lose MFA protection:
Testing MFA flows
MFA flows are some of the most important to test because a bug here can lock users out of their accounts entirely. Two categories of tests matter:
Unit tests for the step-up logic. The freshness check is a pure function of the session timestamp and the configured window. Test it directly:
Integration tests for protected server functions. Mock the getAuth response to simulate different MFA states and verify that the server function returns the expected result:
Production checklist
- Enable MFA in the WorkOS dashboard under Authentication before testing. The toggle must be on for new users to see the enrollment prompt.
- If you are building optional MFA enrollment, always confirm enrollment by asking the user to enter a code immediately after scanning the QR code. Do not save the factor until the code is verified.
- Show the TOTP secret as a fallback for users who cannot scan QR codes. Some password managers and enterprise authenticator apps require manual entry.
- Set
max_agebased on the sensitivity of the operation. Five minutes is a reasonable default for account changes and credential rotation. Usemax_age=0for payment confirmation or any action where a guaranteed fresh identity signal matters more than convenience. - Always include a
returnToparameter in your step-up state so users land back on the page they were on after re-authentication completes. - Test the locked-out path: what happens if a user loses access to their authenticator app? Define and document your account recovery process before enabling mandatory MFA globally.
- The MFA requirement does not apply to SSO users. If you are enforcing MFA org-wide, communicate to customers that their SSO users are governed by their identity provider's MFA policies, not yours.
- Do not log TOTP secrets or QR codes. They are single-use enrollment values and should never appear in application logs.
- Test code expiry: TOTP codes are valid for a 30-second window. Test that entering a code from the previous window is rejected, and that the current code succeeds consistently.
- The
authentication.reauthenticatedevent fires on every successful step-up. Wire it into your audit log pipeline if your application is subject to SOC 2, HIPAA, or PCI-DSS compliance requirements.
Conclusion
MFA in TanStack Start is straightforward at the login level: enable the WorkOS dashboard toggle, and AuthKit handles enrollment and verification through the hosted flow. The more interesting work is step-up authentication, which extends the security guarantee beyond the login event and into the sensitive operations that live in your server functions.
WorkOS now ships step-up authentication as a first-class feature, built on auth_time JWT claims and max_age on the authorization endpoint. There is no custom challenge UI to build, no TOTP verification to wire up manually. The re-authentication flow is hosted by AuthKit and adapts to whatever method the user originally authenticated with.
The architecture is the same one that runs through every guide in this series: server functions are the security boundary, and the check belongs inside the function, not just in the route guard that renders the page. Step-up MFA applies that same principle to the highest-risk operations in your application.
WorkOS includes MFA and step-up authentication with no additional charge, up to 1 million monthly active users on the free plan.
Sign up for WorkOS and add MFA to your TanStack Start application.