Step 5: Token Management
Proper token management is crucial for maintaining secure and seamless user sessions. This step covers secure token storage, automatic refresh, and best practices for token lifecycle management.
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:
Store tokens securely in different environments
Implement automatic token refresh
Handle token expiration gracefully
Manage token lifecycle and cleanup
Use tokens for API calls
Implement proper security measures
Secure Token Storage
Server-Side Storage (Recommended)
For server-side applications, store tokens in encrypted form:
const crypto = require('crypto');
class TokenManager {
constructor(encryptionKey) {
this.encryptionKey = encryptionKey;
this.algorithm = 'aes-256-gcm';
}
encrypt(text) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipher(this.algorithm, this.encryptionKey, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return {
encrypted,
iv: iv.toString('hex'),
authTag: authTag.toString('hex')
};
}
decrypt(encryptedData) {
const decipher = crypto.createDecipher(
this.algorithm,
this.encryptionKey,
Buffer.from(encryptedData.iv, 'hex')
);
decipher.setAuthTag(Buffer.from(encryptedData.authTag, 'hex'));
let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
async storeTokens(userId, tokens) {
const tokenData = {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
idToken: tokens.id_token,
expiresAt: new Date(Date.now() + tokens.expires_in * 1000),
tokenType: tokens.token_type || 'Bearer',
scope: tokens.scope
};
// Encrypt sensitive tokens
const encryptedData = {
accessToken: this.encrypt(tokenData.accessToken),
refreshToken: this.encrypt(tokenData.refreshToken),
idToken: this.encrypt(tokenData.idToken),
expiresAt: tokenData.expiresAt,
tokenType: tokenData.tokenType,
scope: tokenData.scope
};
// Store in database
await database.saveUserTokens(userId, encryptedData);
}
async getTokens(userId) {
const encryptedData = await database.getUserTokens(userId);
if (!encryptedData) return null;
return {
accessToken: this.decrypt(encryptedData.accessToken),
refreshToken: this.decrypt(encryptedData.refreshToken),
idToken: this.decrypt(encryptedData.idToken),
expiresAt: encryptedData.expiresAt,
tokenType: encryptedData.tokenType,
scope: encryptedData.scope
};
}
}
// Usage
const tokenManager = new TokenManager(process.env.TOKEN_ENCRYPTION_KEY);Client-Side Storage (SPAs)
For Single Page Applications, use secure browser storage:
class ClientTokenManager {
constructor() {
this.storage = window.sessionStorage; // More secure than localStorage
this.tokenKey = 'st_tokens';
}
storeTokens(tokens) {
const tokenData = {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
idToken: tokens.id_token,
expiresAt: Date.now() + (tokens.expires_in * 1000),
tokenType: tokens.token_type || 'Bearer',
storedAt: Date.now()
};
this.storage.setItem(this.tokenKey, JSON.stringify(tokenData));
}
getTokens() {
const stored = this.storage.getItem(this.tokenKey);
if (!stored) return null;
try {
const tokenData = JSON.parse(stored);
// Check if tokens are expired
if (Date.now() >= tokenData.expiresAt) {
this.clearTokens();
return null;
}
return tokenData;
} catch (error) {
console.error('Error parsing stored tokens:', error);
this.clearTokens();
return null;
}
}
clearTokens() {
this.storage.removeItem(this.tokenKey);
}
isAuthenticated() {
const tokens = this.getTokens();
return tokens && tokens.accessToken;
}
}Automatic Token Refresh
Server-Side Token Refresh
class TokenRefreshManager extends TokenManager {
constructor(encryptionKey, clientConfig) {
super(encryptionKey);
this.clientConfig = clientConfig;
this.refreshBuffer = 5 * 60 * 1000; // Refresh 5 minutes before expiry
}
async getValidAccessToken(userId) {
const tokens = await this.getTokens(userId);
if (!tokens) {
throw new Error('No tokens found - user needs to re-authenticate');
}
// Check if token needs refresh
const now = new Date();
const expiryWithBuffer = new Date(tokens.expiresAt.getTime() - this.refreshBuffer);
if (now >= expiryWithBuffer) {
console.log('Access token expired or expiring soon, refreshing...');
try {
const newTokens = await this.refreshAccessToken(tokens.refreshToken);
await this.storeTokens(userId, newTokens);
return newTokens.access_token;
} catch (error) {
console.error('Token refresh failed:', error);
// Clear invalid tokens
await this.clearUserTokens(userId);
throw new Error('Token refresh failed - user needs to re-authenticate');
}
}
return tokens.accessToken;
}
async refreshAccessToken(refreshToken) {
const tokenRequest = {
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: this.clientConfig.clientId
};
// Add client secret for confidential clients
if (this.clientConfig.clientSecret) {
tokenRequest.client_secret = this.clientConfig.clientSecret;
}
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 refresh failed: ${errorData.error_description || errorData.error}`);
}
const newTokens = await response.json();
// Some implementations return new refresh tokens
if (!newTokens.refresh_token) {
newTokens.refresh_token = refreshToken;
}
return newTokens;
}
async clearUserTokens(userId) {
await database.deleteUserTokens(userId);
}
}Client-Side Token Refresh
class ClientTokenRefreshManager extends ClientTokenManager {
constructor() {
super();
this.refreshPromise = null; // Prevent concurrent refresh attempts
}
async getValidAccessToken() {
const tokens = this.getTokens();
if (!tokens) {
throw new Error('No tokens found');
}
// Check if token needs refresh (5 minutes buffer)
const now = Date.now();
const expiryWithBuffer = tokens.expiresAt - (5 * 60 * 1000);
if (now >= expiryWithBuffer) {
return await this.refreshTokens();
}
return tokens.accessToken;
}
async refreshTokens() {
// Prevent concurrent refresh attempts
if (this.refreshPromise) {
return await this.refreshPromise;
}
this.refreshPromise = this._performRefresh();
try {
const result = await this.refreshPromise;
return result;
} finally {
this.refreshPromise = null;
}
}
async _performRefresh() {
const tokens = this.getTokens();
if (!tokens || !tokens.refreshToken) {
throw new Error('No refresh token available');
}
try {
const response = await fetch('/api/refresh-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
refresh_token: tokens.refreshToken
})
});
if (!response.ok) {
throw new Error('Token refresh failed');
}
const newTokens = await response.json();
this.storeTokens(newTokens);
return newTokens.access_token;
} catch (error) {
console.error('Token refresh failed:', error);
this.clearTokens();
// Redirect to login
window.location.href = '/login';
throw error;
}
}
}🌐 Using Tokens for API Calls
HTTP Client with Automatic Token Refresh
class AuthenticatedHttpClient {
constructor(tokenManager, userId) {
this.tokenManager = tokenManager;
this.userId = userId;
}
async request(url, options = {}) {
try {
// Get valid access token
const accessToken = await this.tokenManager.getValidAccessToken(this.userId);
// Add authorization header
const headers = {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
...options.headers
};
const response = await fetch(url, {
...options,
headers
});
// Handle token expiration
if (response.status === 401) {
console.log('Received 401, attempting token refresh...');
// Try to refresh token and retry once
const newAccessToken = await this.tokenManager.refreshAccessToken();
const retryResponse = await fetch(url, {
...options,
headers: {
...headers,
'Authorization': `Bearer ${newAccessToken}`
}
});
return retryResponse;
}
return response;
} catch (error) {
console.error('API request failed:', error);
throw error;
}
}
async get(url, options = {}) {
return this.request(url, { ...options, method: 'GET' });
}
async post(url, data, options = {}) {
return this.request(url, {
...options,
method: 'POST',
body: JSON.stringify(data)
});
}
async put(url, data, options = {}) {
return this.request(url, {
...options,
method: 'PUT',
body: JSON.stringify(data)
});
}
async delete(url, options = {}) {
return this.request(url, { ...options, method: 'DELETE' });
}
}
// Usage
const httpClient = new AuthenticatedHttpClient(tokenManager, userId);
// Make authenticated API calls
const userProfile = await httpClient.get('/api/user/profile');
const updateResult = await httpClient.put('/api/user/profile', { name: 'New Name' });Axios Interceptor Example
import axios from 'axios';
function setupAxiosInterceptors(tokenManager, userId) {
// Request interceptor to add auth header
axios.interceptors.request.use(async (config) => {
try {
const accessToken = await tokenManager.getValidAccessToken(userId);
config.headers.Authorization = `Bearer ${accessToken}`;
} catch (error) {
console.error('Failed to get access token:', error);
// Redirect to login or handle error
}
return config;
});
// Response interceptor to handle token expiration
axios.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const newAccessToken = await tokenManager.refreshAccessToken();
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
return axios(originalRequest);
} catch (refreshError) {
console.error('Token refresh failed:', refreshError);
// Redirect to login
window.location.href = '/login';
}
}
return Promise.reject(error);
}
);
}Token Lifecycle Management
Token Cleanup and Rotation
class TokenLifecycleManager extends TokenRefreshManager {
constructor(encryptionKey, clientConfig) {
super(encryptionKey, clientConfig);
this.maxTokenAge = 30 * 24 * 60 * 60 * 1000; // 30 days
}
async cleanupExpiredTokens() {
const expiredBefore = new Date(Date.now() - this.maxTokenAge);
await database.deleteExpiredTokens(expiredBefore);
console.log('Cleaned up expired tokens');
}
async revokeTokens(userId) {
const tokens = await this.getTokens(userId);
if (!tokens) return;
try {
// Revoke refresh token at IDP
await this.revokeRefreshToken(tokens.refreshToken);
} catch (error) {
console.error('Failed to revoke token at IDP:', error);
}
// Clear local tokens
await this.clearUserTokens(userId);
}
async revokeRefreshToken(refreshToken) {
const response = await fetch('https://account.oten.com/v1/oauth/revoke', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
token: refreshToken,
token_type_hint: 'refresh_token',
client_id: this.clientConfig.clientId,
client_secret: this.clientConfig.clientSecret
})
});
if (!response.ok) {
throw new Error('Token revocation failed');
}
}
// Schedule periodic cleanup
startCleanupSchedule() {
setInterval(() => {
this.cleanupExpiredTokens().catch(console.error);
}, 24 * 60 * 60 * 1000); // Daily cleanup
}
}Session Management Integration
// Express.js session integration
app.use(async (req, res, next) => {
if (req.session.userId) {
try {
// Ensure user has valid tokens
const accessToken = await tokenManager.getValidAccessToken(req.session.userId);
req.accessToken = accessToken;
req.authenticated = true;
} catch (error) {
console.log('Token validation failed, clearing session');
req.session.destroy();
req.authenticated = false;
}
} else {
req.authenticated = false;
}
next();
});
// Logout handler
app.post('/logout', async (req, res) => {
if (req.session.userId) {
try {
await tokenManager.revokeTokens(req.session.userId);
} catch (error) {
console.error('Token revocation failed:', error);
}
req.session.destroy();
}
res.redirect('/');
});Monitoring and Metrics
Token Usage Metrics
class TokenMetrics {
static recordTokenRefresh(success, error = null) {
if (success) {
metrics.increment('tokens.refresh.success');
} else {
metrics.increment('tokens.refresh.failure', { error: error?.message });
}
}
static recordTokenUsage(endpoint, responseStatus) {
metrics.increment('tokens.usage', {
endpoint,
status: responseStatus
});
}
static recordTokenExpiration(userId) {
metrics.increment('tokens.expired', { user_id: userId });
}
}Token Management Checklist
Ensure your token management:
Navigation
← Previous: Step 4: Handle Callback - Process authorization response
↑ Overview: Integration Flow Overview - See the big picture
Complete: You've finished all integration steps!
Progress: Step 5 of 5 complete ✅
Congratulations!
You've successfully completed the Oten IDP integration! Here's what you've accomplished:
✅ Prerequisites - Set up your environment and registered with Oten ✅ JAR Implementation - Understood and implemented JWT-Secured Authorization Request ✅ Library Selection - Chose the right OAuth library for your stack ✅ Client Configuration - Set up your OAuth client with proper endpoints ✅ Authorization Flow - Implemented JAR-based authorization with PKCE ✅ Callback Handling - Processed OAuth callbacks and exchanged codes for tokens ✅ Token Management - Implemented secure token storage and refresh
🚀 What's Next?
Production Readiness
Security Best Practices - Harden your implementation
Testing Guide - Comprehensive testing strategies
Monitoring & Logging - Production monitoring
Advanced Features
Error Handling - Robust error handling
User Experience - Optimize user flows
Support
Common Errors - Troubleshooting guide
Contact Support - Get help when needed
Happy coding!
Last updated