Tutorials Search / Backend integrations / Require MFA on your backend
πŸ“ Written ● Intermediate Updated 2026-06-02

How do I add multi-factor auth (MFA) to my backend?

TL;DR: In the console (lingcode.dev/backends β†’ Authentication) turn on Require MFA for all signed-in users. In your app, after sign-in call client.auth.enrollMfa() (show the returned otpauthUrl as a QR for an authenticator app), then client.auth.verifyMfa({ code }). Once required, the data API rejects un-verified sessions until they pass a TOTP code. Access tokens are short-lived and the SDK refreshes them automatically.

A password alone is one stolen credential away from a breach. MFA adds a second factor β€” a rotating 6-digit code from an authenticator app (TOTP) β€” so a leaked password isn't enough. On most stacks that means bolting on an auth vendor; on a LingCode Cloud backend the users system already supports it, and turning it on is a toggle plus two SDK calls.

This builds on a connected managed backend whose users sign in with email/password, magic link, OTP, or social login. MFA layers a TOTP second factor on top, and the backend tracks an assurance level per session: aal1 after a normal sign-in, aal2 once a TOTP code is verified.

What you'll learn

Step 1 β€” require it (console)

In lingcode.dev/backends β†’ Authentication, toggle Require MFA for all signed-in users. With it on, the data API rejects any request from a user session that hasn't reached aal2, returning a mfa_required error β€” your app catches that and walks the user through enrollment/verification. (Leave it off and MFA is opt-in: users can still enroll, but it isn't enforced.) The public anon key is unaffected β€” this gates user sessions, not anonymous reads governed by your RLS.

Step 2 β€” enroll a factor (app)

After the user signs in, offer "Enable two-factor." Enrolling returns a shared secret and an otpauth:// URL β€” render that URL as a QR code (any small QR library) so the user can add it to Google Authenticator, 1Password, etc.:

const { data } = await client.auth.enrollMfa();
// data.otpauthUrl β†’ render as a QR; data.secret β†’ manual-entry fallback

Step 3 β€” verify (and you're at aal2)

Ask for the 6-digit code the app shows and verify it. The first successful verify completes enrollment and returns an aal2 session, which the SDK stores:

const { error } = await client.auth.verifyMfa({ code: userTypedCode });
// no error β†’ session is now aal2; data calls succeed

On a later visit (or after a token refresh drops back to aal1), a protected call returns mfa_required; call client.auth.challengeMfa() to see the user's verified factors, prompt for a code, and verifyMfa again.

Tokens refresh themselves

Sessions use a short-lived access token plus a rotating refresh token. The SDK refreshes transparently when the access token expires β€” and if a refresh token is ever replayed (a sign of theft), the whole token family is revoked. You don't manage any of this; it's why a user stays signed in without you shipping a long-lived token to the browser.

What's next