Files
SerpentRace/SerpentRace_Backend/JWT_REFRESH_TOKEN_GUIDE.md
2025-09-22 11:14:32 +02:00

9.7 KiB

JWT Refresh Token Implementation Guide

Overview

The JWT authentication system supports both cookie-based and header-based (Bearer token) authentication with comprehensive refresh token functionality and proper logout logic. All authentication methods now use refresh tokens - there is no legacy single-token mode.

Features

  • Dual Authentication Methods: Support for both cookie-based and Bearer token authentication
  • Universal Refresh Tokens: All logins receive both access and refresh tokens
  • Automatic Token Refresh: Tokens are refreshed when 75% of their lifetime has passed
  • Logout Functionality: Proper token blacklisting and cleanup
  • Security: Short-lived access tokens (30 minutes) and longer-lived refresh tokens (7 days)

Authentication Methods

  • Access token stored in auth_token cookie
  • Refresh token stored in refresh_token cookie
  • Suitable for web applications with same-origin requests
  • Tokens also returned in response body

2. Bearer Token Authentication

  • Access token sent in Authorization: Bearer <token> header
  • Refresh token sent in X-Refresh-Token header
  • Suitable for mobile apps, SPAs, and API integrations
  • Tokens returned in response body

API Endpoints

Login

POST /api/user/login
Content-Type: application/json

{
  "username": "user@example.com", 
  "password": "password123"
}

Response (all logins):

{
  "user": { ... },
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

For cookie-based auth, tokens are also set as httpOnly cookies.

Refresh Token

POST /api/user/refresh-token

For Cookie-based auth:

  • Refresh token is read from refresh_token cookie
  • New tokens are set as cookies AND returned in response body

For Bearer token auth:

POST /api/user/refresh-token
X-Refresh-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Response:

{
  "success": true,
  "message": "Tokens refreshed successfully", 
  "accessToken": "new_access_token",
  "refreshToken": "new_refresh_token"
}

Logout

POST /api/user/logout
Authorization: Bearer <access_token>

Response:

{
  "success": true
}

Environment Variables

# JWT Configuration
JWT_SECRET=your-secret-key-for-access-tokens
JWT_REFRESH_SECRET=your-secret-key-for-refresh-tokens

# Access Token Expiry (use one of these)
JWT_ACCESS_TOKEN_EXPIRY=1800              # Access token expiry in seconds (30 minutes)
JWT_ACCESS_TOKEN_EXPIRATION=30m           # Access token expiry (supports s, m, h, d)
JWT_EXPIRY=1800                          # Legacy: Access token expiry in seconds
JWT_EXPIRATION=30m                       # Legacy: Access token expiry with duration

# Refresh Token Expiry (use one of these)  
JWT_REFRESH_TOKEN_EXPIRY=604800          # Refresh token expiry in seconds (7 days)
JWT_REFRESH_TOKEN_EXPIRATION=7d          # Refresh token expiry (supports s, m, h, d)
JWT_REFRESH_EXPIRATION=7d                # Legacy: Refresh token expiry with duration

# Cookie Names (optional)
JWT_COOKIE_NAME=auth_token               # Access token cookie name (default: auth_token)
JWT_REFRESH_COOKIE_NAME=refresh_token    # Refresh token cookie name (default: refresh_token)

Environment Variable Priority

Access Token Expiry (checked in order):

  1. JWT_ACCESS_TOKEN_EXPIRY (seconds)
  2. JWT_ACCESS_TOKEN_EXPIRATION (duration string)
  3. JWT_EXPIRY (seconds) - legacy
  4. JWT_EXPIRATION (duration string) - legacy
  5. Default: 1800 seconds (30 minutes)

Refresh Token Expiry (checked in order):

  1. JWT_REFRESH_TOKEN_EXPIRY (seconds)
  2. JWT_REFRESH_TOKEN_EXPIRATION (duration string)
  3. JWT_REFRESH_EXPIRATION (duration string) - legacy
  4. Default: 604800 seconds (7 days)

Duration String Format

Supports: s (seconds), m (minutes), h (hours), d (days) Examples: 30s, 15m, 2h, 7d

Token Structure

Access Token Payload

{
  "userId": "user-uuid",
  "authLevel": 0,
  "userStatus": 1,
  "orgId": "org-uuid",
  "type": "access",
  "iat": 1640995200,
  "exp": 1640997000
}

Refresh Token Payload

{
  "userId": "user-uuid",
  "orgId": "org-uuid",
  "type": "refresh",
  "iat": 1640995200,
  "exp": 1641600000
}

Automatic Token Refresh

The system automatically refreshes tokens when:

  • Token is within 25% of its expiration time (75% of lifetime has passed)
  • Valid refresh token is available
  • User makes an authenticated request

Automatic refresh happens on every authenticated API call - no manual intervention needed!

Response Headers

For Bearer token authentication, refresh responses include:

  • X-New-Access-Token: New access token
  • X-New-Refresh-Token: New refresh token
  • X-Token-Refreshed: "true" indicator

Manual Refresh (Optional)

While automatic refresh handles most scenarios, manual refresh is available for:

  • Proactive refresh: Before critical operations
  • Background apps: Long-running applications that need fresh tokens
  • Offline recovery: When app reconnects after being offline
POST /api/user/refresh-token
X-Refresh-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Client Implementation Examples

JavaScript/TypeScript (Fetch API)

class ApiClient {
  private accessToken: string = '';
  private refreshToken: string = '';

  async login(username: string, password: string) {
    const response = await fetch('/api/user/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ username, password })
    });
    
    const data = await response.json();
    this.accessToken = data.token;
    this.refreshToken = data.refreshToken; // Always present now
    return data;
  }

  async makeAuthenticatedRequest(url: string, options: RequestInit = {}) {
    const headers = {
      'Authorization': `Bearer ${this.accessToken}`,
      ...options.headers
    };

    let response = await fetch(url, { ...options, headers });
    
    // Automatically handle token refresh (tokens updated in response headers)
    if (response.headers.get('X-Token-Refreshed') === 'true') {
      const newAccessToken = response.headers.get('X-New-Access-Token');
      const newRefreshToken = response.headers.get('X-New-Refresh-Token');
      
      if (newAccessToken) this.accessToken = newAccessToken;
      if (newRefreshToken) this.refreshToken = newRefreshToken;
    }
    
    return response;
  }

  // Optional: Manual refresh (usually not needed due to automatic refresh)
  async refreshTokens() {
    const response = await fetch('/api/user/refresh-token', {
      method: 'POST',
      headers: {
        'X-Refresh-Token': this.refreshToken
      }
    });
    
    if (response.ok) {
      const data = await response.json();
      this.accessToken = data.accessToken;
      this.refreshToken = data.refreshToken;
      return true;
    }
    return false;
  }

  async logout() {
    await fetch('/api/user/logout', {
      method: 'POST',
      headers: { 'Authorization': `Bearer ${this.accessToken}` }
    });
    
    this.accessToken = '';
    this.refreshToken = '';
  }
}

React Hook Example

import { useState, useCallback } from 'react';

export const useAuth = () => {
  const [accessToken, setAccessToken] = useState<string>('');
  const [refreshToken, setRefreshToken] = useState<string>('');

  const login = useCallback(async (username: string, password: string) => {
    const response = await fetch('/api/user/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ username, password })
    });
    
    const data = await response.json();
    setAccessToken(data.token);
    setRefreshToken(data.refreshToken); // Always present
    return data;
  }, []);

  const logout = useCallback(async () => {
    if (accessToken) {
      await fetch('/api/user/logout', {
        method: 'POST',
        headers: { 'Authorization': `Bearer ${accessToken}` }
      });
    }
    setAccessToken('');
    setRefreshToken('');
  }, [accessToken]);

  return { accessToken, refreshToken, login, logout };
};

Security Considerations

  1. Token Blacklisting: Logout tokens are blacklisted in Redis with TTL matching token expiration
  2. Short-lived Access Tokens: 30-minute expiry reduces exposure window
  3. Secure Cookies: httpOnly, secure, sameSite attributes for cookie-based auth
  4. Token Rotation: Refresh tokens are rotated on each refresh
  5. Environment-specific Secrets: Different secrets for access and refresh tokens

Migration Guide

From Single Token to Refresh Token System

Since this is a new implementation, all clients should expect:

  1. Login Response: Always includes both token (access) and refreshToken
  2. Token Storage: Store both tokens securely
  3. API Requests: Use access token in Authorization header
  4. Automatic Refresh: Tokens refresh automatically - just watch for response headers
  5. Logout: Call logout endpoint to invalidate tokens

Key Point: Manual refresh is optional since automatic refresh handles token renewal seamlessly.

No backward compatibility needed - this is the only authentication method.

Testing

# Login and get tokens
curl -X POST http://localhost:3000/api/user/login \
  -H "Content-Type: application/json" \
  -d '{"username": "test@example.com", "password": "password"}'

# Use access token
curl -X GET http://localhost:3000/api/user/profile \
  -H "Authorization: Bearer <access_token>"

# Refresh tokens
curl -X POST http://localhost:3000/api/user/refresh-token \
  -H "X-Refresh-Token: <refresh_token>"

# Logout
curl -X POST http://localhost:3000/api/user/logout \
  -H "Authorization: Bearer <access_token>"