Error Handling

SSO Version: 5.24.0

Overview

Proper error handling is critical for a robust OAuth 2.0 integration. This guide covers OAuth error codes, app permission errors, and best practices for graceful error recovery.

📝 Note: For complete error code reference, see API Error Codes.

OAuth 2.0 Error Handling

OAuth 2.0 errors occur during the authorization and token exchange flow:

Authorization Endpoint Errors

These errors appear in the redirect URI query parameters:

// Example error redirect
https://yourapp.com/callback?error=access_denied&error_description=User+denied+access&state=abc123

// Handle in your callback
const params = new URLSearchParams(window.location.search);
const error = params.get('error');

if (error === 'access_denied') {
  showMessage('You declined to sign in. Please try again.');
} else if (error === 'unauthorized_client') {
  showMessage('Your application is not authorized. Contact support.');
}

Token Endpoint Errors

These errors occur during token exchange on your backend:

// Backend token exchange error handling
try {
  const tokenResponse = await fetch(
    'https://sso.doneisbetter.com/api/oauth/token',
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        grant_type: 'authorization_code',
        code: authorizationCode,
        redirect_uri: process.env.SSO_REDIRECT_URI,
        client_id: process.env.SSO_CLIENT_ID,
        client_secret: process.env.SSO_CLIENT_SECRET
      })
    }
  );

  if (!tokenResponse.ok) {
    const error = await tokenResponse.json();
    
    switch (error.error) {
      case 'invalid_grant':
        // Authorization code expired or already used
        console.error('Code expired. Redirect user to login again.');
        return res.redirect('/login');
      
      case 'invalid_client':
        // client_id or client_secret is wrong
        console.error('Invalid OAuth credentials. Check env vars.');
        return res.status(500).json({ error: 'Configuration error' });
      
      case 'unsupported_grant_type':
        // Wrong grant_type parameter
        console.error('Invalid grant_type. Should be authorization_code.');
        return res.status(500).json({ error: 'Configuration error' });
      
      default:
        console.error('Token exchange failed:', error);
        return res.status(500).json({ error: 'Authentication failed' });
    }
  }

  const tokens = await tokenResponse.json();
  // Store tokens and proceed
} catch (error) {
  console.error('Token exchange error:', error);
  res.status(500).json({ error: 'Authentication failed' });
}

App Permission Errors

After successful authentication, check the user's permission status:

// Extract permission status from ID token
import jwt from 'jsonwebtoken';

const idToken = req.cookies.id_token;
const decoded = jwt.decode(idToken);
const { permissionStatus, role } = decoded;

// Handle different permission statuses
if (permissionStatus === 'pending') {
  return res.redirect('/access-pending');
} else if (permissionStatus === 'revoked') {
  return res.redirect('/access-denied');
} else if (permissionStatus !== 'approved') {
  return res.status(403).json({
    error: 'APP_ACCESS_DENIED',
    message: 'Access not approved',
    permissionStatus
  });
}

// Check role requirements
if (requireAdmin && role !== 'admin') {
  return res.status(403).json({
    error: 'INSUFFICIENT_ROLE',
    message: 'Admin role required'
  });
}

Common Error Scenarios

1. Token Expired

Error: TOKEN_EXPIRED or INVALID_TOKEN

Solution: Implement automatic token refresh

// Check token expiry and refresh if needed
const decoded = jwt.decode(idToken);
if (decoded.exp * 1000 < Date.now()) {
  await refreshTokens(req, res);
}

2. Invalid Refresh Token

Error: invalid_grant when refreshing

Causes:

  • Refresh token already used (they're single-use)
  • Refresh token expired (30 day lifetime)
  • User's access was revoked

Solution: Redirect to login

try {
  await refreshTokens(req, res);
} catch (error) {
  if (error.error === 'invalid_grant') {
    // Refresh token invalid, must re-authenticate
    res.clearCookie('access_token');
    res.clearCookie('id_token');
    res.clearCookie('refresh_token');
    return res.redirect('/login');
  }
}

3. CORS Errors

Error: Browser console shows CORS error

Solution: Register your origin with SSO admin

// Always include credentials in requests
fetch('https://sso.doneisbetter.com/api/public/session', {
  credentials: 'include' // Required for cookies
});

4. Rate Limiting

Error: 429 Too Many Requests

Solution: Implement exponential backoff

async function retryWithBackoff(operation, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await operation();
    } catch (error) {
      if (error.status === 429) {
        const retryAfter = error.headers.get('Retry-After') || Math.pow(2, i);
        await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
        continue;
      }
      throw error;
    }
  }
  throw new Error('Max retries exceeded');
}

User-Friendly Error Messages

Transform technical errors into user-friendly messages:

function getErrorMessage(error) {
  const messages = {
    // OAuth errors
    'access_denied': 'You declined to sign in. Please try again if this was a mistake.',
    'unauthorized_client': 'This application is not authorized. Please contact support.',
    'invalid_grant': 'Your session has expired. Please sign in again.',
    
    // App permission errors
    'APP_ACCESS_PENDING': 'Your access request is pending approval. You\'ll receive an email when approved.',
    'APP_ACCESS_REVOKED': 'Your access has been revoked. Please contact an administrator.',
    'INSUFFICIENT_ROLE': 'You don\'t have permission to access this feature.',
    
    // Token errors
    'TOKEN_EXPIRED': 'Your session has expired. Redirecting to login...',
    'INVALID_TOKEN': 'Invalid authentication. Please sign in again.',
    
    // Network errors
    'RATE_LIMIT_EXCEEDED': 'Too many requests. Please wait a moment and try again.',
    'NETWORK_ERROR': 'Unable to connect. Please check your internet connection.'
  };

  return messages[error.code] || 'An unexpected error occurred. Please try again.';
}

// Usage
try {
  await someOperation();
} catch (error) {
  showUserMessage(getErrorMessage(error));
}

Error Logging Best Practices

  • ✅ Log errors with full context (user ID, timestamp, request details)
  • ✅ Use structured logging (JSON format)
  • ✅ Include error codes and messages
  • ✅ Never log sensitive data (tokens, secrets, passwords)
  • ✅ Monitor error rates and patterns
// Good error logging example
function logError(error, context) {
  console.error(JSON.stringify({
    timestamp: new Date().toISOString(),
    error: {
      code: error.code,
      message: error.message,
      stack: error.stack
    },
    context: {
      userId: context.user?.userId,
      path: context.req.path,
      method: context.req.method,
      ip: context.req.ip
    }
    // Never log: tokens, client_secret, passwords
  }));
}

Summary

  • ☑️ Handle OAuth 2.0 errors in authorization and token exchange
  • ☑️ Check permissionStatus after authentication
  • ☑️ Implement token refresh with error handling
  • ☑️ Provide user-friendly error messages
  • ☑️ Implement rate limiting backoff
  • ☑️ Log errors with context (but never log secrets)