This guide walks you through implementing all authentication flows supported by the AuthOS SDK. You’ll learn how to handle end-user OAuth login, admin authentication, device flows for CLIs, and token lifecycle management.
Prerequisites
Before you begin, make sure you have:
- Installed the SDK:
npm install @drmhse/sso-sdk - An AuthOS instance URL
- Organization and service configured in the platform
End-User OAuth Login (Web Redirect Flow)
The most common authentication flow for web applications. Users authenticate using OAuth providers (GitHub, Google, Microsoft) configured by your organization.
Step 1: Initialize the SDK
import { SsoClient } from '@drmhse/sso-sdk';
const sso = new SsoClient({
baseURL: 'https://sso.example.com'
});
Step 2: Redirect User to OAuth Provider
// Generate the OAuth login URL
const loginUrl = sso.auth.getLoginUrl('github', {
org: 'acme-corp',
service: 'main-app',
redirect_uri: 'https://app.acme.com/callback'
});
// Redirect the user to authenticate
window.location.href = loginUrl;
Supported Providers:
github- GitHub OAuthgoogle- Google OAuthmicrosoft- Microsoft OAuth
Step 3: Handle the Callback
After successful authentication, the user is redirected back to your redirect_uri with tokens in the URL query parameters.
// In your callback route handler (e.g., /callback)
function handleOAuthCallback() {
const params = new URLSearchParams(window.location.search);
const accessToken = params.get('access_token');
const refreshToken = params.get('refresh_token');
if (!accessToken || !refreshToken) {
console.error('Authentication failed - no tokens received');
window.location.href = '/login';
return;
}
// Store tokens securely
localStorage.setItem('sso_access_token', accessToken);
localStorage.setItem('sso_refresh_token', refreshToken);
// Set the token in the SDK for future requests
sso.setAuthToken(accessToken);
// Redirect to the main application
window.location.href = '/dashboard';
}
Step 4: Verify Authentication
// Fetch the authenticated user's profile
try {
const profile = await sso.user.getProfile();
console.log('Logged in as:', profile.email);
console.log('Organization:', profile.org);
console.log('Service:', profile.service);
} catch (error) {
console.error('Failed to fetch profile:', error);
// Token invalid - redirect to login
window.location.href = '/login';
}
Complete Example: React Login Component
import { useEffect } from 'react';
import { SsoClient, SsoApiError } from '@drmhse/sso-sdk';
const sso = new SsoClient({
baseURL: process.env.REACT_APP_SSO_URL
});
export function LoginPage() {
const handleLogin = (provider: 'github' | 'google' | 'microsoft') => {
const loginUrl = sso.auth.getLoginUrl(provider, {
org: 'acme-corp',
service: 'main-app',
redirect_uri: `${window.location.origin}/callback`
});
window.location.href = loginUrl;
};
return (
<div className="login-page">
<h1>Sign In</h1>
<button onClick={() => handleLogin('github')}>
Sign in with GitHub
</button>
<button onClick={() => handleLogin('google')}>
Sign in with Google
</button>
<button onClick={() => handleLogin('microsoft')}>
Sign in with Microsoft
</button>
</div>
);
}
export function CallbackPage() {
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const accessToken = params.get('access_token');
const refreshToken = params.get('refresh_token');
if (accessToken && refreshToken) {
localStorage.setItem('sso_access_token', accessToken);
localStorage.setItem('sso_refresh_token', refreshToken);
sso.setAuthToken(accessToken);
window.location.href = '/dashboard';
} else {
window.location.href = '/login';
}
}, []);
return <div>Completing authentication...</div>;
}
Admin Login (Platform & Organization Admins)
Platform owners and organization admins use a separate authentication flow with dedicated OAuth credentials managed by the platform.
When to Use Admin Login
- Platform owner dashboard
- Organization administration panel
- Service configuration interfaces
- Any interface that requires elevated privileges
Implementation
// Generate admin login URL
const adminLoginUrl = sso.auth.getAdminLoginUrl('github', {
org_slug: 'acme-corp' // Optional: redirect to specific org after login
});
// Redirect admin to authenticate
window.location.href = adminLoginUrl;
// The callback handling is identical to end-user flow
// Tokens are returned in the same way
Admin Login with Organization Context
// Admin dashboard component
export function AdminDashboard() {
const handleAdminLogin = () => {
// After login, user will be directed to acme-corp dashboard
const loginUrl = sso.auth.getAdminLoginUrl('github', {
org_slug: 'acme-corp'
});
window.location.href = loginUrl;
};
return (
<div>
<h1>Admin Panel</h1>
<button onClick={handleAdminLogin}>Sign in as Admin</button>
</div>
);
}
Key Differences from End-User Flow:
- Uses platform OAuth credentials instead of tenant credentials
- Grants admin-level permissions
- Can access platform-wide resources
- JWT contains admin role claims
Device Flow (CLIs and Mobile Apps)
The device authorization flow (RFC 8628) enables authentication on devices that don’t have a browser or have limited input capabilities, such as CLIs, IoT devices, or smart TVs.
How Device Flow Works
- Device requests a user code
- Device displays the code and verification URL to the user
- User visits the URL on another device (phone/computer)
- User enters the code and authenticates
- Device polls for the token
- Token is granted after user approves
CLI Implementation
import { SsoClient } from '@drmhse/sso-sdk';
const sso = new SsoClient({
baseURL: 'https://sso.example.com'
});
async function loginCLI() {
// Step 1: Request device code
const deviceAuth = await sso.auth.deviceCode.request({
client_id: 'acme-cli',
org: 'acme-corp',
service: 'acme-cli'
});
console.log('\n=================================');
console.log('To authenticate, visit:');
console.log(deviceAuth.verification_uri);
console.log('\nAnd enter code:');
console.log(deviceAuth.user_code);
console.log('=================================\n');
// Step 2: Poll for token
const token = await pollForToken(deviceAuth.device_code, deviceAuth.interval);
if (token) {
// Save token to config file
console.log('Authentication successful!');
return token.access_token;
}
}
async function pollForToken(deviceCode: string, interval: number) {
const maxAttempts = 60; // 5 minutes with 5-second intervals
let attempts = 0;
while (attempts < maxAttempts) {
await sleep(interval * 1000);
attempts++;
try {
const tokenResponse = await sso.auth.deviceCode.exchangeToken({
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
device_code: deviceCode,
client_id: 'acme-cli'
});
return tokenResponse;
} catch (error) {
if (error instanceof SsoApiError) {
if (error.errorCode === 'authorization_pending') {
// User hasn't authorized yet, keep polling
process.stdout.write('.');
continue;
} else if (error.errorCode === 'slow_down') {
// Increase interval if requested
interval += 5;
continue;
} else if (error.errorCode === 'expired_token') {
console.error('\nDevice code expired. Please try again.');
return null;
} else if (error.errorCode === 'access_denied') {
console.error('\nUser denied the request.');
return null;
}
}
throw error;
}
}
console.error('\nAuthentication timed out.');
return null;
}
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
Web Application: Device Code Verification
Your web application needs a route to handle device code verification (typically at /activate).
// Activation page component
import { useState } from 'react';
import { SsoClient } from '@drmhse/sso-sdk';
const sso = new SsoClient({
baseURL: process.env.REACT_APP_SSO_URL
});
export function DeviceActivationPage() {
const [userCode, setUserCode] = useState('');
const [error, setError] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
try {
// Verify the user code
const context = await sso.auth.deviceCode.verify(userCode.trim().toUpperCase());
// Redirect to OAuth flow with user_code parameter
const loginUrl = sso.auth.getLoginUrl('github', {
org: context.org_slug,
service: context.service_slug,
user_code: userCode.trim().toUpperCase(), // CRITICAL: pass user_code
redirect_uri: `${window.location.origin}/device-callback`
});
window.location.href = loginUrl;
} catch (err) {
if (err instanceof SsoApiError) {
setError(err.message);
} else {
setError('Failed to verify code');
}
}
};
return (
<div className="activation-page">
<h1>Device Activation</h1>
<p>Enter the code displayed on your device</p>
<form onSubmit={handleSubmit}>
<input
type="text"
value={userCode}
onChange={(e) => setUserCode(e.target.value)}
placeholder="XXXX-XXXX"
maxLength={9}
style={{ textTransform: 'uppercase' }}
/>
<button type="submit">Continue</button>
</form>
{error && <div className="error">{error}</div>}
</div>
);
}
Complete Device Flow: Full Example
// cli.ts - Complete CLI tool with device flow
import { SsoClient, SsoApiError } from '@drmhse/sso-sdk';
import * as fs from 'fs';
import * as path from 'path';
const CONFIG_PATH = path.join(process.env.HOME!, '.acme-cli', 'config.json');
class AcmeCLI {
private sso: SsoClient;
constructor() {
this.sso = new SsoClient({
baseURL: 'https://sso.example.com'
});
}
async login() {
console.log('Starting authentication...\n');
const deviceAuth = await this.sso.auth.deviceCode.request({
client_id: 'acme-cli',
org: 'acme-corp',
service: 'acme-cli'
});
console.log('To authenticate, visit:');
console.log(` ${deviceAuth.verification_uri}`);
console.log('\nAnd enter code:');
console.log(` ${deviceAuth.user_code}`);
console.log('\nWaiting for authentication', { flush: true });
const token = await this.pollForToken(
deviceAuth.device_code,
deviceAuth.interval
);
if (token) {
this.saveToken(token.access_token, token.refresh_token);
console.log('\nAuthentication successful!');
return true;
}
return false;
}
private async pollForToken(deviceCode: string, interval: number) {
const maxTime = 300; // 5 minutes
const startTime = Date.now();
while (Date.now() - startTime < maxTime * 1000) {
await this.sleep(interval * 1000);
try {
const tokenResponse = await this.sso.auth.deviceCode.exchangeToken({
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
device_code: deviceCode,
client_id: 'acme-cli'
});
return tokenResponse;
} catch (error) {
if (error instanceof SsoApiError) {
if (error.errorCode === 'authorization_pending') {
process.stdout.write('.');
continue;
} else if (error.errorCode === 'slow_down') {
interval += 5;
continue;
} else if (error.errorCode === 'expired_token') {
console.error('\n\nDevice code expired. Please try again.');
return null;
} else if (error.errorCode === 'access_denied') {
console.error('\n\nAuthentication denied.');
return null;
}
}
throw error;
}
}
console.error('\n\nAuthentication timed out.');
return null;
}
private saveToken(accessToken: string, refreshToken: string) {
const dir = path.dirname(CONFIG_PATH);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(
CONFIG_PATH,
JSON.stringify({ accessToken, refreshToken }, null, 2)
);
}
private sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// CLI entry point
const cli = new AcmeCLI();
cli.login();
Token Refresh Workflow
Access tokens expire after a period of time (typically 1 hour). Use refresh tokens to obtain new access tokens without requiring the user to log in again.
When to Refresh Tokens
- When an API call returns a 401 Unauthorized error
- Proactively before the access token expires (if you track expiration)
- On application startup if the stored token might be expired
Basic Token Refresh
async function refreshAccessToken() {
const refreshToken = localStorage.getItem('sso_refresh_token');
if (!refreshToken) {
// No refresh token - user needs to log in
window.location.href = '/login';
return null;
}
try {
const tokens = await sso.auth.refreshToken(refreshToken);
// Store new tokens (both are rotated)
localStorage.setItem('sso_access_token', tokens.access_token);
localStorage.setItem('sso_refresh_token', tokens.refresh_token);
// Update SDK
sso.setAuthToken(tokens.access_token);
return tokens.access_token;
} catch (error) {
console.error('Token refresh failed:', error);
// Refresh token invalid or expired - user must log in
localStorage.removeItem('sso_access_token');
localStorage.removeItem('sso_refresh_token');
window.location.href = '/login';
return null;
}
}
Automatic Token Refresh with Interceptor
Implement automatic token refresh by intercepting 401 errors:
import { SsoClient, SsoApiError } from '@drmhse/sso-sdk';
class AuthenticatedSsoClient {
private sso: SsoClient;
private isRefreshing: boolean = false;
private refreshPromise: Promise<string | null> | null = null;
constructor(baseURL: string) {
const accessToken = localStorage.getItem('sso_access_token');
this.sso = new SsoClient({ baseURL, token: accessToken || undefined });
}
async request<T>(fn: () => Promise<T>): Promise<T> {
try {
return await fn();
} catch (error) {
if (error instanceof SsoApiError && error.statusCode === 401) {
// Token expired, try to refresh
const newToken = await this.refreshToken();
if (newToken) {
// Retry the original request with new token
return await fn();
}
}
throw error;
}
}
private async refreshToken(): Promise<string | null> {
// Prevent multiple simultaneous refresh attempts
if (this.isRefreshing && this.refreshPromise) {
return this.refreshPromise;
}
this.isRefreshing = true;
this.refreshPromise = this.performRefresh();
const result = await this.refreshPromise;
this.isRefreshing = false;
this.refreshPromise = null;
return result;
}
private async performRefresh(): Promise<string | null> {
const refreshToken = localStorage.getItem('sso_refresh_token');
if (!refreshToken) {
this.redirectToLogin();
return null;
}
try {
const tokens = await this.sso.auth.refreshToken(refreshToken);
localStorage.setItem('sso_access_token', tokens.access_token);
localStorage.setItem('sso_refresh_token', tokens.refresh_token);
this.sso.setAuthToken(tokens.access_token);
return tokens.access_token;
} catch (error) {
console.error('Token refresh failed:', error);
this.redirectToLogin();
return null;
}
}
private redirectToLogin() {
localStorage.removeItem('sso_access_token');
localStorage.removeItem('sso_refresh_token');
window.location.href = '/login';
}
getClient(): SsoClient {
return this.sso;
}
}
// Usage
const authClient = new AuthenticatedSsoClient('https://sso.example.com');
// Wrap all API calls with automatic retry
const profile = await authClient.request(() =>
authClient.getClient().user.getProfile()
);
React Hook for Token Management
import { useEffect, useState } from 'react';
import { SsoClient } from '@drmhse/sso-sdk';
export function useAuth() {
const [sso, setSso] = useState<SsoClient | null>(null);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
initializeAuth();
}, []);
const initializeAuth = async () => {
const accessToken = localStorage.getItem('sso_access_token');
const refreshToken = localStorage.getItem('sso_refresh_token');
if (!accessToken || !refreshToken) {
setIsLoading(false);
return;
}
const client = new SsoClient({
baseURL: process.env.REACT_APP_SSO_URL!,
token: accessToken
});
try {
// Try to fetch profile to verify token
await client.user.getProfile();
setSso(client);
setIsAuthenticated(true);
} catch (error) {
// Token invalid, try to refresh
try {
const tokens = await client.auth.refreshToken(refreshToken);
localStorage.setItem('sso_access_token', tokens.access_token);
localStorage.setItem('sso_refresh_token', tokens.refresh_token);
client.setAuthToken(tokens.access_token);
setSso(client);
setIsAuthenticated(true);
} catch (refreshError) {
// Refresh failed, clear tokens
localStorage.removeItem('sso_access_token');
localStorage.removeItem('sso_refresh_token');
}
} finally {
setIsLoading(false);
}
};
const logout = async () => {
if (sso) {
try {
await sso.auth.logout();
} catch (error) {
console.error('Logout error:', error);
}
}
localStorage.removeItem('sso_access_token');
localStorage.removeItem('sso_refresh_token');
setSso(null);
setIsAuthenticated(false);
};
return {
sso,
isAuthenticated,
isLoading,
logout
};
}
// Usage in components
function App() {
const { sso, isAuthenticated, isLoading } = useAuth();
if (isLoading) {
return <div>Loading...</div>;
}
if (!isAuthenticated) {
return <LoginPage />;
}
return <Dashboard sso={sso!} />;
}
Risk-Based Authentication
AuthOS includes an intelligent Risk Engine that evaluates login attempts in real-time. When suspicious activity is detected, the platform may require additional verification through MFA, even if the user’s credentials are correct.
Understanding Risk Assessment
The Risk Engine evaluates multiple signals during every authentication attempt:
- Geographic Location: Logins from new countries or impossible travel patterns
- Device Trust: Logins from previously unseen devices
- Velocity Detection: Rapid login attempts or suspicious patterns
- User Agent Analysis: Unusual browsers or automation tools
Based on the risk score (0-100), the platform takes one of these actions:
| Risk Score | Action | What Happens |
|---|---|---|
| 0-40 | Allow | Login succeeds normally |
| 41-60 | Log Only | Login succeeds, event logged for review |
| 61-80 | Challenge MFA | Additional MFA verification required |
| 81-100 | Block | Login denied |
Handling MFA Challenges
When the Risk Engine determines additional verification is needed, authentication responses will include special indicators:
Password Login with Risk Challenge
async function handlePasswordLogin(email: string, password: string) {
try {
const response = await sso.auth.login({ email, password });
// Check if MFA is required due to risk assessment
if (response.expires_in === 300) {
// This is a pre-auth token (5 minutes expiry)
// User needs to provide MFA code
return {
type: 'mfa_required',
preauthToken: response.access_token,
message: 'Additional verification required'
};
}
// Normal login - no MFA required
sso.setAuthToken(response.access_token);
localStorage.setItem('sso_access_token', response.access_token);
localStorage.setItem('sso_refresh_token', response.refresh_token);
return { type: 'success' };
} catch (error) {
return { type: 'error', error };
}
}
Magic Link with Risk Challenge
async function handleMagicLinkVerification(token: string) {
try {
const response = await sso.magicLinks.verify(token);
// Check if response indicates MFA is required
if (response.requires_mfa) {
return {
type: 'mfa_required',
preauthToken: response.preauth_token,
message: response.message || 'Additional verification required due to unusual activity'
};
}
// Normal verification - store tokens
sso.setAuthToken(response.access_token);
localStorage.setItem('sso_access_token', response.access_token);
localStorage.setItem('sso_refresh_token', response.refresh_token);
return { type: 'success' };
} catch (error) {
return { type: 'error', error };
}
}
Completing MFA Verification
Once you have a pre-auth token from a risk challenge, prompt the user for their MFA code:
async function completeMfaChallenge(preauthToken: string, mfaCode: string) {
try {
const tokens = await sso.auth.verifyMfa(preauthToken, mfaCode);
// MFA verification successful - store full tokens
sso.setAuthToken(tokens.access_token);
localStorage.setItem('sso_access_token', tokens.access_token);
localStorage.setItem('sso_refresh_token', tokens.refresh_token);
return { type: 'success' };
} catch (error) {
if (error instanceof SsoApiError) {
if (error.statusCode === 401) {
return {
type: 'invalid_code',
message: 'Invalid MFA code. Please try again.'
};
} else if (error.statusCode === 410) {
return {
type: 'expired',
message: 'Verification expired. Please log in again.'
};
}
}
return { type: 'error', error };
}
}
Complete React Risk-Based Authentication
import { useState } from 'react';
import { SsoClient, SsoApiError } from '@drmhse/sso-sdk';
const sso = new SsoClient({
baseURL: process.env.REACT_APP_SSO_URL!
});
type AuthState =
| { step: 'credentials' }
| { step: 'mfa_required'; preauthToken: string; message: string }
| { step: 'success' };
export function RiskAwareLogin() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [mfaCode, setMfaCode] = useState('');
const [authState, setAuthState] = useState<AuthState>({ step: 'credentials' });
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setIsLoading(true);
try {
const response = await sso.auth.login({ email, password });
// Check for pre-auth token (MFA required)
if (response.expires_in === 300) {
setAuthState({
step: 'mfa_required',
preauthToken: response.access_token,
message: 'We detected unusual activity. Please verify your identity with MFA.'
});
setIsLoading(false);
} else {
// Normal login
sso.setAuthToken(response.access_token);
localStorage.setItem('sso_access_token', response.access_token);
localStorage.setItem('sso_refresh_token', response.refresh_token);
setAuthState({ step: 'success' });
window.location.href = '/dashboard';
}
} catch (err) {
setIsLoading(false);
if (err instanceof SsoApiError) {
setError(err.message);
} else {
setError('Login failed. Please try again.');
}
}
};
const handleMfaVerification = async (e: React.FormEvent) => {
e.preventDefault();
if (authState.step !== 'mfa_required') return;
setError('');
setIsLoading(true);
try {
const tokens = await sso.auth.verifyMfa(authState.preauthToken, mfaCode);
sso.setAuthToken(tokens.access_token);
localStorage.setItem('sso_access_token', tokens.access_token);
localStorage.setItem('sso_refresh_token', tokens.refresh_token);
setAuthState({ step: 'success' });
window.location.href = '/dashboard';
} catch (err) {
setIsLoading(false);
if (err instanceof SsoApiError) {
if (err.statusCode === 401) {
setError('Invalid MFA code. Please try again.');
} else if (err.statusCode === 410) {
setError('Verification expired. Please log in again.');
setAuthState({ step: 'credentials' });
} else {
setError(err.message);
}
}
}
};
// Step 1: Credentials
if (authState.step === 'credentials') {
return (
<div className="login-form">
<h2>Sign In</h2>
<form onSubmit={handleLogin}>
<div className="form-group">
<label>Email</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
autoFocus
/>
</div>
<div className="form-group">
<label>Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Signing in...' : 'Sign In'}
</button>
</form>
{error && <div className="alert alert-error">{error}</div>}
</div>
);
}
// Step 2: MFA Challenge
if (authState.step === 'mfa_required') {
return (
<div className="mfa-challenge">
<h2>Additional Verification Required</h2>
<div className="alert alert-warning">
<p>{authState.message}</p>
</div>
<p>Please enter your 6-digit MFA code from your authenticator app:</p>
<form onSubmit={handleMfaVerification}>
<div className="form-group">
<label>MFA Code</label>
<input
type="text"
value={mfaCode}
onChange={(e) => setMfaCode(e.target.value.replace(/\D/g, ''))}
placeholder="000000"
maxLength={6}
pattern="\d{6}"
required
autoFocus
/>
</div>
<button type="submit" disabled={isLoading || mfaCode.length !== 6}>
{isLoading ? 'Verifying...' : 'Verify'}
</button>
</form>
{error && <div className="alert alert-error">{error}</div>}
<div className="help-text">
<p>
This additional step is required because we detected unusual activity,
such as a login from a new location or device.
</p>
</div>
<button
className="secondary-button"
onClick={() => setAuthState({ step: 'credentials' })}
>
Cancel and try again
</button>
</div>
);
}
return null;
}
OAuth Flow with Risk Assessment
OAuth flows can also trigger MFA challenges. Handle this in your callback:
export function OAuthCallback() {
const [status, setStatus] = useState<'processing' | 'mfa_required' | 'success'>('processing');
const [preauthToken, setPreauthToken] = useState('');
const [mfaCode, setMfaCode] = useState('');
const [error, setError] = useState('');
useEffect(() => {
handleCallback();
}, []);
const handleCallback = async () => {
const params = new URLSearchParams(window.location.search);
const accessToken = params.get('access_token');
const requiresMfa = params.get('requires_mfa');
if (requiresMfa === 'true' && accessToken) {
// Risk assessment triggered MFA requirement
setPreauthToken(accessToken);
setStatus('mfa_required');
return;
}
if (accessToken) {
const refreshToken = params.get('refresh_token');
localStorage.setItem('sso_access_token', accessToken);
if (refreshToken) {
localStorage.setItem('sso_refresh_token', refreshToken);
}
sso.setAuthToken(accessToken);
setStatus('success');
window.location.href = '/dashboard';
}
};
const handleMfaSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
const tokens = await sso.auth.verifyMfa(preauthToken, mfaCode);
localStorage.setItem('sso_access_token', tokens.access_token);
localStorage.setItem('sso_refresh_token', tokens.refresh_token);
sso.setAuthToken(tokens.access_token);
window.location.href = '/dashboard';
} catch (err) {
setError('MFA verification failed');
}
};
if (status === 'mfa_required') {
return (
<div className="mfa-challenge">
<h2>Additional Verification Required</h2>
<p>Please enter your MFA code to complete login:</p>
<form onSubmit={handleMfaSubmit}>
<input
type="text"
value={mfaCode}
onChange={(e) => setMfaCode(e.target.value)}
placeholder="000000"
maxLength={6}
required
/>
<button type="submit">Verify</button>
</form>
{error && <div className="error">{error}</div>}
</div>
);
}
return <div>Processing authentication...</div>;
}
User Experience Best Practices
- Clear Communication: Explain why additional verification is needed
- Contextual Messages: Show location/device information that triggered the challenge
- Backup Options: Allow users to use backup codes if they don’t have their authenticator
- Session Persistence: Remember trusted devices to reduce friction for returning users
- Graceful Fallbacks: Provide alternative authentication methods if MFA fails
Detecting Risk Challenges
Pre-auth tokens have a short expiration (5 minutes) to indicate MFA is required:
function isPreAuthToken(response: RefreshTokenResponse): boolean {
// Pre-auth tokens expire in 5 minutes (300 seconds)
// Normal tokens expire in 15 minutes (900 seconds)
return response.expires_in === 300;
}
function isMfaRequired(response: any): boolean {
return response.requires_mfa === true || isPreAuthToken(response);
}
Handling Backup Codes
Users can use backup codes instead of TOTP codes during MFA challenges:
async function verifyWithBackupCode(preauthToken: string, backupCode: string) {
try {
// Backup codes are treated the same as TOTP codes
const tokens = await sso.auth.verifyMfa(preauthToken, backupCode);
return { success: true, tokens };
} catch (error) {
if (error instanceof SsoApiError && error.statusCode === 401) {
return {
success: false,
error: 'Invalid backup code or code already used'
};
}
throw error;
}
}
Testing Risk-Based Flows
When testing risk-based authentication:
- New Device: Clear cookies to simulate a new device
- New Location: Use a VPN to test geographic anomaly detection
- Rapid Attempts: Test velocity detection with multiple quick login attempts
- Monitor Mode: Set organization risk settings to “monitor” mode during development
// Set organization to monitor mode for testing
await sso.organizations.updateRiskSettings('acme-corp', {
enabled: true,
mode: 'monitor', // Logs risk but doesn't enforce
block_threshold: 80,
mfa_threshold: 60
});
Best Practices
Token Storage
Web Applications:
- Use
localStoragefor single-tab persistence - Use
sessionStoragefor single-session security - Consider using secure HTTP-only cookies for sensitive applications
Mobile Applications:
- Use secure storage APIs (iOS Keychain, Android Keystore)
- Never store tokens in plain text files
CLI Applications:
- Store tokens in user home directory with restricted permissions
- Use OS-specific secure storage when available
Security Considerations
- HTTPS Only: Always use HTTPS in production
- Token Rotation: The platform implements automatic refresh token rotation
- XSS Protection: Sanitize all user inputs to prevent token theft
- CSRF Protection: Use state parameters for OAuth flows
- Logout Everywhere: Call
sso.auth.logout()to revoke tokens server-side
Error Handling
import { SsoApiError } from '@drmhse/sso-sdk';
try {
await sso.user.getProfile();
} catch (error) {
if (error instanceof SsoApiError) {
switch (error.statusCode) {
case 401:
// Token expired or invalid
await refreshAccessToken();
break;
case 403:
// Insufficient permissions
console.error('Access denied');
break;
case 404:
// Resource not found
console.error('Resource not found');
break;
case 500:
// Server error
console.error('Server error, please try again later');
break;
default:
console.error(`Error: ${error.message}`);
}
} else {
// Network or other error
console.error('Network error:', error);
}
}
Redirect URI Configuration
Always whitelist your redirect URIs in the service configuration:
// When creating a service via API or admin dashboard
const service = await sso.services.create('acme-corp', {
slug: 'main-app',
name: 'Main Application',
service_type: 'web',
redirect_uris: [
'https://app.acme.com/callback',
'https://app.acme.com/device-callback',
'http://localhost:3000/callback' // for development
]
});
Next Steps
- Password Authentication Guide - Learn about email/password login
- MFA Management Guide - Implement multi-factor authentication
- API Reference - Detailed API documentation