← Back to Learn

JWT Token Lifecycle

Understanding the complete lifecycle of JWT tokens: generation, validation, refresh, and revocation.

1. Token Generation

Token Generation
// Step 1: User authenticates
async function login(username, password) {
  // Verify credentials
  const user = await verifyCredentials(username, password);
  if (!user) {
    throw new Error('Invalid credentials');
  }
  
  // Step 2: Generate access token
  const accessToken = jwt.sign(
    {
      sub: user.id,
      role: user.role,
      permissions: user.permissions,
      iat: Math.floor(Date.now() / 1000)
    },
    ACCESS_SECRET,
    {
      algorithm: 'HS256',
      expiresIn: '15m',  // Short-lived
      issuer: 'my-app',
      audience: 'my-api'
    }
  );
  
  // Step 3: Generate refresh token
  const refreshToken = jwt.sign(
    {
      sub: user.id,
      type: 'refresh',
      iat: Math.floor(Date.now() / 1000)
    },
    REFRESH_SECRET,
    {
      algorithm: 'HS256',
      expiresIn: '7d',  // Longer-lived
      issuer: 'my-app'
    }
  );
  
  // Step 4: Return tokens
  return {
    accessToken,
    refreshToken,
    expiresIn: 900  // 15 minutes in seconds
  };
}

2. Token Validation

Token Validation
// Middleware for token validation
async function validateToken(req, res, next) {
  try {
    // Step 1: Extract token from header
    const authHeader = req.headers.authorization;
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      return res.status(401).json({ error: 'No token provided' });
    }
    
    const token = authHeader.substring(7);
    
    // Step 2: Verify signature and expiration
    const decoded = jwt.verify(token, ACCESS_SECRET, {
      algorithms: ['HS256'],
      issuer: 'my-app',
      audience: 'my-api',
      clockTolerance: 60  // Allow 60s clock skew
    });
    
    // Step 3: Check if token is blacklisted
    if (await isTokenBlacklisted(token)) {
      return res.status(401).json({ error: 'Token revoked' });
    }
    
    // Step 4: Verify token version (if using versioning)
    const user = await db.users.findById(decoded.sub);
    if (user.tokenVersion !== decoded.tokenVersion) {
      return res.status(401).json({ error: 'Token invalidated' });
    }
    
    // Step 5: Attach user info to request
    req.user = decoded;
    req.userId = decoded.sub;
    
    next();
  } catch (error) {
    if (error.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expired' });
    }
    if (error.name === 'JsonWebTokenError') {
      return res.status(401).json({ error: 'Invalid token' });
    }
    return res.status(500).json({ error: 'Token validation failed' });
  }
}

3. Token Refresh

Token Refresh Flow
// Client-side: Automatic token refresh
let accessToken = null;
let refreshToken = null;

async function refreshAccessToken() {
  if (!refreshToken) {
    redirectToLogin();
    return;
  }
  
  try {
    const response = await fetch('/api/auth/refresh', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ refreshToken }),
      credentials: 'include'
    });
    
    if (!response.ok) {
      throw new Error('Refresh failed');
    }
    
    const data = await response.json();
    accessToken = data.accessToken;
    
    // Optionally rotate refresh token
    if (data.refreshToken) {
      refreshToken = data.refreshToken;
    }
    
    return accessToken;
  } catch (error) {
    // Refresh token expired - redirect to login
    redirectToLogin();
  }
}

// Server-side: Refresh endpoint
app.post('/api/auth/refresh', async (req, res) => {
  try {
    const { refreshToken } = req.body;
    
    // Verify refresh token
    const decoded = jwt.verify(refreshToken, REFRESH_SECRET, {
      algorithms: ['HS256'],
      issuer: 'my-app'
    });
    
    // Check if refresh token is blacklisted
    if (await isTokenBlacklisted(refreshToken)) {
      return res.status(401).json({ error: 'Refresh token revoked' });
    }
    
    // Get current user data
    const user = await db.users.findById(decoded.sub);
    
    // Generate new access token
    const newAccessToken = jwt.sign(
      {
        sub: user.id,
        role: user.role,
        permissions: user.permissions,
        tokenVersion: user.tokenVersion,
        iat: Math.floor(Date.now() / 1000)
      },
      ACCESS_SECRET,
      {
        algorithm: 'HS256',
        expiresIn: '15m',
        issuer: 'my-app',
        audience: 'my-api'
      }
    );
    
    // Optionally rotate refresh token
    const newRefreshToken = jwt.sign(
      {
        sub: user.id,
        type: 'refresh',
        iat: Math.floor(Date.now() / 1000)
      },
      REFRESH_SECRET,
      {
        algorithm: 'HS256',
        expiresIn: '7d',
        issuer: 'my-app'
      }
    );
    
    // Blacklist old refresh token (optional)
    await blacklistToken(refreshToken);
    
    res.json({
      accessToken: newAccessToken,
      refreshToken: newRefreshToken,
      expiresIn: 900
    });
  } catch (error) {
    res.status(401).json({ error: 'Invalid refresh token' });
  }
});

4. Token Revocation

Token Revocation
// Logout - Revoke tokens
async function logout(req, res) {
  try {
    const token = extractToken(req);
    const refreshToken = req.body.refreshToken;
    
    // Decode token to get jti
    const decoded = jwt.decode(token);
    const refreshDecoded = jwt.decode(refreshToken);
    
    // Add to blacklist
    if (decoded?.jti) {
      const ttl = decoded.exp - Math.floor(Date.now() / 1000);
      if (ttl > 0) {
        await redis.setex(`blacklist:${decoded.jti}`, ttl, '1');
      }
    }
    
    if (refreshDecoded?.jti) {
      const ttl = refreshDecoded.exp - Math.floor(Date.now() / 1000);
      if (ttl > 0) {
        await redis.setex(`blacklist:${refreshDecoded.jti}`, ttl, '1');
      }
    }
    
    // Alternative: Increment token version
    await db.users.updateOne(
      { _id: req.userId },
      { $inc: { tokenVersion: 1 } }
    );
    
    res.json({ message: 'Logged out successfully' });
  } catch (error) {
    res.status(500).json({ error: 'Logout failed' });
  }
}

// Check blacklist during validation
async function isTokenBlacklisted(token) {
  const decoded = jwt.decode(token);
  if (!decoded?.jti) {
    return false;
  }
  
  const blacklisted = await redis.exists(`blacklist:${decoded.jti}`);
  return blacklisted === 1;
}

5. Complete Lifecycle Flow

  1. Authentication: User provides credentials, server validates and generates access + refresh tokens
  2. Storage: Client stores tokens securely (HttpOnly cookies or memory)
  3. Usage: Client includes access token in Authorization header for each request
  4. Validation: Server validates token signature, expiration, and claims
  5. Expiration: When access token expires, client uses refresh token to get new access token
  6. Refresh: Server validates refresh token and issues new access token (optionally new refresh token)
  7. Revocation: On logout or security event, tokens are blacklisted or invalidated
  8. Cleanup: Expired tokens are automatically removed from blacklist

6. Best Practices for Lifecycle Management

  • Use short-lived access tokens (15-60 minutes)
  • Use longer-lived refresh tokens (7-30 days)
  • Implement automatic token refresh before expiration
  • Rotate refresh tokens on use (optional but recommended)
  • Use jti (JWT ID) for token identification and blacklisting
  • Implement token versioning for password changes
  • Monitor token usage patterns for anomalies
  • Set up alerts for suspicious token activity
  • Clean up expired tokens from blacklist regularly
  • Log authentication events (without tokens) for auditing