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

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:


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

Advanced Features

Support

Happy coding!

Last updated