Authentication Troubleshooting

Handle risk-based MFA challenges, SDK errors, and redirect configuration failures

AuthOS release 0.8.2 TypeScript SDK 0.8.0 Latest-only documentation
Updated Jul 15, 2026 Edit this page
On this page

Use this guide when a login is challenged or fails. For token expiry and refresh behavior, see Tokens and Sessions.

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 };
  }
}
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 () => {
    // Check query parameters for MFA challenge
    const params = new URLSearchParams(window.location.search);
    const preauth = params.get('preauth_token');
    const mfaRequired = params.get('mfa_required');

    if (mfaRequired === 'true' && preauth) {
      // Risk assessment or user settings triggered MFA requirement
      setPreauthToken(preauth);
      setStatus('mfa_required');
      return;
    }

    // Normal tokens are returned in the fragment (#)
    const hashParams = new URLSearchParams(window.location.hash.substring(1));
    const accessToken = hashParams.get('access_token');
    const refreshToken = hashParams.get('refresh_token');

    if (accessToken) {
      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

  1. Clear Communication: Explain why additional verification is needed
  2. Contextual Messages: Show location/device information that triggered the challenge
  3. Backup Options: Allow users to use backup codes if they don’t have their authenticator
  4. Session Persistence: Remember trusted devices to reduce friction for returning users
  5. 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.mfa_required === 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:

  1. New Device: Clear cookies to simulate a new device
  2. New Location: Use a VPN to test geographic anomaly detection
  3. Rapid Attempts: Test velocity detection with multiple quick login attempts
  4. 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
});

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 your own management UI
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:3001/callback' // for development
  ]
});