Start after enrolling an authenticator application. Verification is required both to activate the factor and to exchange a pre-authentication token for a full login session.
Verifying TOTP Code to Enable MFA
After the user scans the QR code, require one current TOTP code before treating MFA as enabled.
Verification
import { SsoApiError } from '@drmhse/sso-sdk';
async function verifyAndEnableMfa(code: string) {
try {
const result = await sso.user.mfa.verify(code);
console.log('MFA enabled successfully!');
console.log('Backup codes:', result.backup_codes);
// IMPORTANT: Display backup codes to user.
// The user MUST save these before continuing.
return result.backup_codes;
} catch (error) {
if (error instanceof SsoApiError) {
console.error('Invalid code:', error.message);
}
throw error;
}
}
Important: Backup codes are shown only once during setup. Require the user to save them securely before leaving the flow, and never send them to logs or analytics.
Login with MFA
When a user with MFA enabled signs in, AuthOS returns a short-lived pre-authentication token. The client must verify a TOTP or backup code before it receives and stores a full session.
MFA Login Flow
async function loginWithMfa(email: string, password: string) {
const tokens = await sso.auth.login({ email, password });
if (tokens.expires_in === 300) {
// This is a pre-auth token with a five-minute expiration.
const mfaCode = await promptForMfaCode();
// Successful verification stores the full session.
return sso.auth.verifyMfa(tokens.access_token, mfaCode);
}
// Login already stored the non-MFA session.
return tokens;
}
Do not treat the five-minute token as an authenticated application session. Limit it to the MFA continuation and discard it when the user cancels or it expires.
Complete Login Form with MFA Support
import { useState } from 'react';
import { SsoClient, SsoApiError } from '@drmhse/sso-sdk';
const sso = new SsoClient({
baseURL: process.env.REACT_APP_SSO_URL
});
export function LoginWithMfaForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [mfaCode, setMfaCode] = useState('');
const [preauthToken, setPreauthToken] = useState<string | null>(null);
const [useBackupCode, setUseBackupCode] = useState(false);
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const handlePasswordLogin = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setLoading(true);
try {
const tokens = await sso.auth.login({ email, password });
if (tokens.expires_in === 300) {
setPreauthToken(tokens.access_token);
setLoading(false);
return;
}
window.location.href = '/dashboard';
} catch (err) {
if (err instanceof SsoApiError) {
if (err.statusCode === 401) {
setError('Invalid email or password');
} else if (err.statusCode === 403) {
setError('Please verify your email before logging in');
} else {
setError(err.message);
}
} else {
setError('Login failed. Please try again.');
}
setLoading(false);
}
};
const handleMfaVerification = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setLoading(true);
try {
await sso.auth.verifyMfa(preauthToken!, mfaCode);
window.location.href = '/dashboard';
} catch (err) {
if (err instanceof SsoApiError) {
setError('Invalid code. Please try again.');
} else {
setError('Verification failed. Please try again.');
}
setLoading(false);
}
};
if (preauthToken) {
return (
<form onSubmit={handleMfaVerification}>
<h2>Two-Factor Authentication</h2>
<p>
{useBackupCode
? 'Enter one of your backup codes'
: 'Enter the 6-digit code from your authenticator app'}
</p>
<div className="form-group">
<input
type="text"
value={mfaCode}
onChange={(e) => setMfaCode(useBackupCode
? e.target.value
: e.target.value.replace(/\D/g, ''))}
maxLength={useBackupCode ? 20 : 6}
placeholder={useBackupCode ? 'XXXX-XXXX-XXXX' : '000000'}
required
autoFocus
/>
</div>
{error && <div className="error">{error}</div>}
<button type="submit" disabled={loading}>
{loading ? 'Verifying...' : 'Verify'}
</button>
<div className="mfa-options">
<button type="button" onClick={() => {
setUseBackupCode(!useBackupCode);
setMfaCode('');
setError('');
}}>
{useBackupCode ? 'Use authenticator code' : 'Use backup code'}
</button>
<button type="button" onClick={() => {
setPreauthToken(null);
setMfaCode('');
setUseBackupCode(false);
setError('');
}}>
Back to Login
</button>
</div>
</form>
);
}
return (
<form onSubmit={handlePasswordLogin}>
<h2>Sign In</h2>
<div className="form-group">
<label htmlFor="email">Email Address</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
{error && <div className="error">{error}</div>}
<button type="submit" disabled={loading}>
{loading ? 'Signing In...' : 'Sign In'}
</button>
</form>
);
}
Continue to Backup and recovery to define how users store and use the recovery codes returned by enrollment.