Step 4: Handle Callback
After users authenticate with Oten IDP, they're redirected back to your application with an authorization code. This step shows you how to handle the callback, validate parameters, and exchange the code for tokens.
๐ Need context? Check the Integration Flow Overview to see how this step fits into the complete process.
๐ฏ What You'll Learn
In this step, you will:
Handle the OAuth callback request
Validate state parameter and other security checks
Exchange authorization code for tokens
Handle errors and edge cases
Implement proper error handling and logging
๐ Callback Flow Overview

๐ Callback Request Structure
Successful Callback
When authentication succeeds, Oten redirects to your callback URL with:
https://yourapp.com/callback?code=AUTH_CODE&state=STATE_VALUEParameters:
code: Authorization code (10-minute expiry)state: The state value you sent (for CSRF protection)
Error Callback
When authentication fails, the callback includes error information:
https://yourapp.com/callback?error=access_denied&error_description=User+denied+access&state=STATE_VALUEError Parameters:
error: Error code (e.g.,access_denied,invalid_request)error_description: Human-readable error descriptionstate: The state value (still present for validation)
๐ Security Validation
State Parameter Validation
function validateState(receivedState, req) {
const storedState = req.session.oauthState;
// Clear stored state (one-time use)
delete req.session.oauthState;
if (!receivedState) {
throw new Error('Missing state parameter');
}
if (!storedState) {
throw new Error('No stored state found - possible session timeout');
}
if (receivedState !== storedState) {
throw new Error('Invalid state parameter - possible CSRF attack');
}
return true;
}Authorization Code Validation
function validateAuthorizationCode(code) {
if (!code) {
throw new Error('Missing authorization code');
}
if (typeof code !== 'string') {
throw new Error('Invalid authorization code format');
}
// Basic length validation (codes are typically 20-512 characters)
if (code.length < 10 || code.length > 512) {
throw new Error('Authorization code length invalid');
}
// Check for valid characters (base64url)
if (!/^[A-Za-z0-9_-]+$/.test(code)) {
throw new Error('Authorization code contains invalid characters');
}
return true;
}๐ Token Exchange
Basic Token Exchange
async function exchangeCodeForTokens(code, codeVerifier = null) {
const tokenRequest = {
grant_type: 'authorization_code',
code: code,
redirect_uri: process.env.OTEN_REDIRECT_URI,
client_id: process.env.OTEN_CLIENT_ID
};
// Add client secret for confidential clients
if (process.env.OTEN_CLIENT_SECRET) {
tokenRequest.client_secret = process.env.OTEN_CLIENT_SECRET;
}
// Add PKCE code verifier for public clients
if (codeVerifier) {
tokenRequest.code_verifier = codeVerifier;
}
try {
const response = await fetch('https://account.oten.com/v1/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json'
},
body: new URLSearchParams(tokenRequest)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(`Token exchange failed: ${errorData.error_description || errorData.error}`);
}
const tokens = await response.json();
return tokens;
} catch (error) {
console.error('Token exchange error:', error);
throw error;
}
}Token Response Structure
// Successful token response
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "def50200...",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"scope": "openid profile email"
}Token Validation
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
// Create JWKS client for token validation
const client = jwksClient({
jwksUri: 'https://account.oten.com/.well-known/jwks.json',
cache: true,
cacheMaxAge: 86400000, // 24 hours
rateLimit: true,
jwksRequestsPerMinute: 5
});
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
if (err) {
callback(err);
return;
}
const signingKey = key.publicKey || key.rsaPublicKey;
callback(null, signingKey);
});
}
async function validateIDToken(idToken) {
return new Promise((resolve, reject) => {
jwt.verify(idToken, getKey, {
issuer: 'https://account.oten.com',
audience: process.env.OTEN_CLIENT_ID,
algorithms: ['RS256']
}, (err, decoded) => {
if (err) {
reject(new Error(`Invalid ID token: ${err.message}`));
return;
}
// Additional validations
const now = Math.floor(Date.now() / 1000);
if (decoded.exp <= now) {
reject(new Error('ID token has expired'));
return;
}
if (decoded.iat > now + 300) { // Allow 5 minutes clock skew
reject(new Error('ID token issued in the future'));
return;
}
resolve(decoded);
});
});
}๐ Implementation Examples
Express.js Callback Handler
app.get('/callback', async (req, res) => {
try {
const { code, state, error, error_description } = req.query;
// Handle OAuth errors
if (error) {
console.error('OAuth error:', error, error_description);
return res.redirect('/login?error=' + encodeURIComponent(error_description || error));
}
// Validate state parameter
validateState(state, req);
// Validate authorization code
validateAuthorizationCode(code);
// Get code verifier from session (if using PKCE)
const codeVerifier = req.session.codeVerifier;
delete req.session.codeVerifier; // Clear after use
// Exchange code for tokens
const tokens = await exchangeCodeForTokens(code, codeVerifier);
// Validate ID token
const idTokenClaims = await validateIDToken(tokens.id_token);
// Store tokens and user information
await storeUserSession(req, tokens, idTokenClaims);
// Redirect to application
res.redirect('/dashboard');
} catch (error) {
console.error('Callback error:', error);
res.redirect('/login?error=' + encodeURIComponent('Authentication failed'));
}
});
async function storeUserSession(req, tokens, userClaims) {
// Store user information in session
req.session.user = {
id: userClaims.sub,
email: userClaims.email,
name: userClaims.name,
workspace: userClaims.workspace
};
// Store tokens securely (encrypted)
req.session.tokens = {
accessToken: encrypt(tokens.access_token),
refreshToken: encrypt(tokens.refresh_token),
idToken: encrypt(tokens.id_token),
expiresAt: new Date(Date.now() + tokens.expires_in * 1000)
};
// Log successful authentication
console.log('User authenticated:', userClaims.sub, userClaims.email);
}React SPA Callback Handler
// CallbackPage.jsx
import React, { useEffect, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
function CallbackPage() {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const [status, setStatus] = useState('processing');
const [error, setError] = useState(null);
useEffect(() => {
handleCallback();
}, []);
async function handleCallback() {
try {
const code = searchParams.get('code');
const state = searchParams.get('state');
const error = searchParams.get('error');
const errorDescription = searchParams.get('error_description');
if (error) {
throw new Error(errorDescription || error);
}
if (!code) {
throw new Error('No authorization code received');
}
// Validate state
const storedState = sessionStorage.getItem('oauth_state');
sessionStorage.removeItem('oauth_state');
if (state !== storedState) {
throw new Error('Invalid state parameter');
}
// Exchange code for tokens
const codeVerifier = sessionStorage.getItem('code_verifier');
sessionStorage.removeItem('code_verifier');
const tokens = await exchangeCodeForTokens(code, codeVerifier);
// Store tokens
localStorage.setItem('access_token', tokens.access_token);
localStorage.setItem('refresh_token', tokens.refresh_token);
localStorage.setItem('id_token', tokens.id_token);
// Parse user info from ID token
const userInfo = parseJWT(tokens.id_token);
localStorage.setItem('user_info', JSON.stringify(userInfo));
setStatus('success');
// Redirect to app
setTimeout(() => {
navigate('/dashboard');
}, 1000);
} catch (error) {
console.error('Callback error:', error);
setError(error.message);
setStatus('error');
}
}
if (status === 'processing') {
return <div>Processing login...</div>;
}
if (status === 'error') {
return (
<div>
<h2>Login Failed</h2>
<p>{error}</p>
<button onClick={() => navigate('/login')}>Try Again</button>
</div>
);
}
return <div>Login successful! Redirecting...</div>;
}
function parseJWT(token) {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
return JSON.parse(jsonPayload);
}Python Flask Callback Handler
@app.route('/callback')
def callback():
try:
# Check for OAuth errors
error = request.args.get('error')
if error:
error_description = request.args.get('error_description', error)
flash(f'Authentication failed: {error_description}')
return redirect(url_for('login'))
# Get authorization code
code = request.args.get('code')
if not code:
flash('No authorization code received')
return redirect(url_for('login'))
# Validate state parameter
received_state = request.args.get('state')
stored_state = session.pop('oauth_state', None)
if not received_state or received_state != stored_state:
flash('Invalid state parameter')
return redirect(url_for('login'))
# Exchange code for tokens
code_verifier = session.pop('code_verifier', None)
token = oten.authorize_access_token(
code_verifier=code_verifier
)
# Parse user info from ID token
user_info = oten.parse_id_token(token)
# Store user session
session['user'] = {
'id': user_info['sub'],
'email': user_info['email'],
'name': user_info['name']
}
# Store tokens (encrypted)
session['tokens'] = encrypt_tokens(token)
flash('Login successful!')
return redirect(url_for('dashboard'))
except Exception as e:
app.logger.error(f'Callback error: {str(e)}')
flash('Authentication failed')
return redirect(url_for('login'))๐จ Error Handling
๐ Complete Error Reference: See Error Codes Reference for all error codes and handling guidelines.
Authorization Callback Error Scenarios
Authorization errors are returned as URL parameters in the callback. Handle these according to OAuth 2.0 standards:
function handleAuthorizationCallbackError(error, errorDescription, state, req, res) {
// Validate state parameter first
const storedState = req.session.oauthState;
if (state !== storedState) {
console.error('State parameter mismatch - possible CSRF attack');
return res.redirect('/login?error=security_error');
}
const errorMap = {
// Standard OAuth 2.0 errors
'access_denied': {
message: 'You denied access to the application',
action: 'Please try again and grant access to continue',
redirect: '/login',
severity: 'info'
},
'invalid_request': {
message: 'Invalid authentication request',
action: 'Please contact support if this persists',
redirect: '/login',
severity: 'error'
},
'invalid_client': {
message: 'Application configuration error',
action: 'Please contact support',
redirect: '/error',
severity: 'error'
},
'unauthorized_client': {
message: 'Application not authorized',
action: 'Please contact your administrator',
redirect: '/error',
severity: 'error'
},
'unsupported_response_type': {
message: 'Authentication method not supported',
action: 'Please contact support',
redirect: '/error',
severity: 'error'
},
'invalid_scope': {
message: 'Requested permissions not available',
action: 'Please contact support',
redirect: '/error',
severity: 'error'
},
'server_error': {
message: 'Authentication service temporarily unavailable',
action: 'Please try again in a few minutes',
redirect: '/login',
severity: 'warning'
},
'temporarily_unavailable': {
message: 'Authentication service is busy',
action: 'Please wait a moment and try again',
redirect: '/login',
severity: 'warning'
},
// JAR-specific errors
'invalid_request_object': {
message: 'Authentication request format error',
action: 'Please contact support',
redirect: '/error',
severity: 'error'
},
'request_not_supported': {
message: 'Authentication method not supported',
action: 'Please contact support',
redirect: '/error',
severity: 'error'
},
// Oten specific scenarios (using standard error codes)
'invalid_request': {
message: 'Authentication request error',
action: 'Please try logging in again or contact support',
redirect: '/login',
severity: 'warning'
},
'workspace_required': {
message: 'Workspace selection required',
action: 'Please select a workspace and try again',
redirect: '/workspace-selection',
severity: 'info'
},
'mfa_required': {
message: 'Multi-factor authentication required',
action: 'Please complete additional authentication steps',
redirect: '/mfa',
severity: 'info'
}
};
const errorInfo = errorMap[error] || {
message: 'Authentication failed',
action: 'Please try again',
redirect: '/login',
severity: 'error'
};
// Log error for debugging
console.error('OAuth authorization callback error:', {
error,
errorDescription,
state,
severity: errorInfo.severity,
userAgent: req.headers['user-agent'],
ip: req.ip,
timestamp: new Date().toISOString()
});
// Clear OAuth session data
delete req.session.oauthState;
delete req.session.codeVerifier;
// Redirect with user-friendly error
const errorQuery = new URLSearchParams({
error: errorInfo.message,
action: errorInfo.action,
severity: errorInfo.severity
});
res.redirect(`${errorInfo.redirect}?${errorQuery}`);
}Token Exchange Error Handling
Token exchange errors are returned as JSON responses. Handle these according to OAuth 2.0 token endpoint specifications:
async function handleTokenExchangeError(error, response) {
// Parse error response
let errorData;
try {
errorData = await response.json();
} catch (parseError) {
errorData = {
error: 'server_error',
error_description: 'Unable to parse error response'
};
}
const { error: errorCode, error_description: errorDescription } = errorData;
// Map token errors to user actions
const tokenErrorMap = {
// Standard OAuth 2.0 token errors
'invalid_request': {
message: 'Invalid token request',
action: 'restart_auth',
retryable: false,
severity: 'error'
},
'invalid_client': {
message: 'Client authentication failed',
action: 'contact_support',
retryable: false,
severity: 'error'
},
'invalid_grant': {
message: 'Authorization code invalid or expired',
action: 'restart_auth',
retryable: false,
severity: 'warning'
},
'unauthorized_client': {
message: 'Client not authorized for this grant type',
action: 'contact_support',
retryable: false,
severity: 'error'
},
'unsupported_grant_type': {
message: 'Grant type not supported',
action: 'contact_support',
retryable: false,
severity: 'error'
},
'invalid_scope': {
message: 'Requested scope invalid',
action: 'restart_auth',
retryable: false,
severity: 'warning'
},
'server_error': {
message: 'Token service temporarily unavailable',
action: 'retry',
retryable: true,
severity: 'warning'
}
};
const errorInfo = tokenErrorMap[errorCode] || {
message: 'Token exchange failed',
action: 'restart_auth',
retryable: false,
severity: 'error'
};
// Log detailed error information
console.error('Token exchange error:', {
errorCode,
errorDescription,
httpStatus: response.status,
action: errorInfo.action,
retryable: errorInfo.retryable,
severity: errorInfo.severity,
timestamp: new Date().toISOString()
});
return {
...errorInfo,
errorCode,
errorDescription,
httpStatus: response.status
};
}
async function exchangeCodeForTokensWithRetry(code, codeVerifier, maxRetries = 3) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch('/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: process.env.REDIRECT_URI,
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET,
code_verifier: codeVerifier
})
});
if (response.ok) {
return await response.json();
}
// Handle token exchange error
const errorInfo = await handleTokenExchangeError(null, response);
// Don't retry for non-retryable errors
if (!errorInfo.retryable) {
throw new Error(`Token exchange failed: ${errorInfo.message}`);
}
lastError = new Error(`Token exchange failed (attempt ${attempt}): ${errorInfo.message}`);
// Exponential backoff for retryable errors
if (attempt < maxRetries) {
const delay = Math.pow(2, attempt) * 1000; // 2s, 4s, 8s
await new Promise(resolve => setTimeout(resolve, delay));
}
} catch (error) {
lastError = error;
// Don't retry on client errors (4xx)
if (error.message.includes('400') || error.message.includes('401')) {
throw error;
}
// Retry on server errors (5xx) with exponential backoff
if (attempt < maxRetries) {
const delay = Math.pow(2, attempt) * 1000; // 2s, 4s, 8s
await new Promise(resolve => setTimeout(resolve, delay));
console.log(`Token exchange attempt ${attempt} failed, retrying in ${delay}ms...`);
}
}
}
throw lastError;
}Logging and Monitoring
Authentication Event Logging
function logAuthenticationEvent(event, details) {
const logEntry = {
timestamp: new Date().toISOString(),
event: event,
details: details,
source: 'oauth-callback'
};
// Send to logging service
console.log('Auth event:', JSON.stringify(logEntry));
// Send metrics to monitoring service
if (event === 'callback_success') {
metrics.increment('auth.callback.success');
} else if (event === 'callback_error') {
metrics.increment('auth.callback.error', { error_type: details.error });
}
}
// Usage in callback handler
logAuthenticationEvent('callback_success', {
userId: userClaims.sub,
email: userClaims.email,
workspace: userClaims.workspace
});Callback Handler Checklist
Ensure your callback handler:
Navigation
โ Previous: Step 3: Authorization Flow - Implement JAR authorization
โ Overview: Integration Flow Overview - See the big picture
โ Next: Step 5: Token Management - Secure token storage and refresh
Progress: Step 4 of 5 complete โ
Last updated