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.
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
permissionStatusafter authentication - ☑️ Implement token refresh with error handling
- ☑️ Provide user-friendly error messages
- ☑️ Implement rate limiting backoff
- ☑️ Log errors with context (but never log secrets)