Integration Guide

Error Handling

Production-safe handling of OAuth, session, and permission failures.

SSO Version

5.29.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.

Reference 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 backend-derived permission status:

// Fetch canonical permission state from your backend session layer
import jwt from 'jsonwebtoken';

const idToken = req.cookies.id_token;
const decoded = jwt.decode(idToken);
const permission = await getPermissionForUserAndClient({
  userId: decoded.sub,
  clientId: process.env.SSO_CLIENT_ID
});
const role = permission?.role ?? decoded.role;

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

// 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 backend-derived permission status 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)