Skip to content

JWT Token Refresh Implementation

This guide explains how JWT token refresh is implemented in the tracker system to provide seamless authentication without requiring users to re-login when tokens expire.

Overview

The system uses a dual-token approach:

  • Access Token: Medium-lived (4 hours by default), used for API requests
  • Refresh Token: Long-lived (7 days by default), used only to obtain new access tokens

Backend Implementation

Middleware-Based Refresh

The backend includes middleware (TokenRefreshMiddleware) that automatically handles token refresh:

# app/middleware/auth_middleware.py
class TokenRefreshMiddleware(BaseHTTPMiddleware):
    def __init__(self, app, refresh_threshold_minutes: int = 5):
        super().__init__(app)
        self.refresh_threshold_seconds = refresh_threshold_minutes * 60

How It Works

  1. Token Inspection: Middleware checks if the access token will expire within 5 minutes
  2. Automatic Refresh: If expiring, attempts to refresh using the refresh token cookie
  3. Request Processing: Updates the request with the new token and continues processing
  4. Response Headers: Adds the new token to response headers for frontend consumption

Key Features

  • Proactive Refresh: Refreshes tokens before they expire
  • Seamless Operation: No interruption to API requests
  • Security: Only refreshes when necessary, validates refresh tokens properly
  • Logging: Comprehensive logging for debugging and monitoring

Refresh endpoints

The system provides dedicated refresh endpoints:

@router.get("/auth/refresh-cookie")
def refresh_access_token_cookie(
    response: Response,
    refresh_token: str = Depends(deps.get_refresh_token_from_cookie),
    db: Session = Depends(deps.get_db),
) -> Any:
    """Refresh token endpoint using cookie."""
    return auth_controller.refresh_token_from_cookie(
        db=db, refresh_token=refresh_token, response=response
    )

Frontend Implementation

Both the admin panel and main frontend use identical comprehensive JWT refresh capabilities including wake-up session restoration for handling sleeping tabs (especially on Apple devices).

Note: As of January 2025, the admin panel authentication has been synchronized with the frontend to resolve frequent logout issues. See Admin Panel Authentication Synchronization Fix for details.

Wake-Up Session Restoration

The system now includes advanced features to handle tab sleeping scenarios:

Session Manager

const SessionManager = {
  // Attempt to restore session using refresh cookie
  restoreSession: async (): Promise<string | null> => {
    // Check for existing valid token first
    const existingToken = localStorage.getItem("token");
    if (existingToken && !TokenValidator.isExpired(existingToken)) {
      return existingToken;
    }

    // Try to get new token using refresh cookie
    const response = await axios.get("/api/v1/auth/refresh-cookie", {
      withCredentials: true,
    });

    const { access_token } = response.data;
    localStorage.setItem("token", access_token);
    return access_token;
  },

  // Initialize session on app startup
  initializeSession: async (): Promise<boolean> => {
    const token = await SessionManager.restoreSession();
    return token !== null;
  },
};

Wake-Up Detection

const WakeUpDetector = {
  // Detect if tab was sleeping based on activity gap
  wasTabSleeping: (): boolean => {
    const timeSinceLastActivity = Date.now() - WakeUpDetector.lastActivity;
    return timeSinceLastActivity > 5 * 60 * 1000; // 5 minutes
  },

  // Handle tab wake-up events
  handleWakeUp: async (): Promise<void> => {
    console.log("Tab wake-up detected, checking session...");
    await SessionManager.checkAndRefreshToken();
  },

  // Initialize event listeners
  initialize: (): void => {
    document.addEventListener("visibilitychange", () => {
      if (!document.hidden && WakeUpDetector.wasTabSleeping()) {
        WakeUpDetector.handleWakeUp();
      }
    });

    window.addEventListener("focus", () => {
      if (WakeUpDetector.wasTabSleeping()) {
        WakeUpDetector.handleWakeUp();
      }
    });
  },
};

Admin Panel (tracker-admin)

The admin panel now uses the same sophisticated token refresh system as the frontend, with:

Important: The admin panel previously had a conflicting useTokenRefresh hook that caused authentication issues. This has been removed and replaced with the unified SessionManager approach. See Admin Panel Authentication Sync Fix for migration details.

Token Validation

const TokenValidator = {
  isExpired: (token: string): boolean => {
    try {
      const decoded = jwtDecode<{ exp: number }>(token);
      const currentTime = Date.now() / 1000;
      return decoded.exp < currentTime;
    } catch {
      return true;
    }
  },

  isAboutToExpire: (token: string): boolean => {
    try {
      const decoded = jwtDecode<{ exp: number }>(token);
      const currentTime = Date.now() / 1000;
      return decoded.exp - currentTime < 300; // 5 minutes
    } catch {
      return true;
    }
  },
};

Request Interceptor

  • Checks token expiration before each request
  • Proactively refreshes tokens about to expire
  • Queues concurrent requests during refresh

Response Interceptor

  • Handles 401 errors by attempting token refresh
  • Retries failed requests with new tokens
  • Manages refresh queue for concurrent requests

Main Frontend (tracker-frontend)

The main frontend uses the same comprehensive approach as the admin panel:

  • Token validation utilities with TokenValidator
  • Comprehensive WakeUpDetector for tab sleep handling
  • Request/response interceptors with refresh queuing
  • Automatic refresh on 401 errors
  • SessionManager for session restoration
  • Graceful error handling with user feedback

Both applications now share identical authentication patterns to ensure consistent behavior.

Security Considerations

Token Storage

  • Access Tokens: Stored in localStorage for easy access
  • Refresh Tokens: Stored as HttpOnly cookies for security
@dataclass
class CookieSettings:
    key: str = "refresh_token"
    httponly: bool = True
    samesite: str = "lax"

    @property
    def secure(self) -> bool:
        return settings.ENVIRONMENT != "development"

Validation

  • Refresh tokens are validated for type, expiration, and signature
  • Access tokens include comprehensive claims (iss, aud, jti, iat)
  • Middleware skips auth endpoints to prevent infinite loops

Error Handling

Backend Errors

  • Invalid refresh tokens return 403 Forbidden
  • Missing refresh tokens return 400 Bad Request
  • Expired refresh tokens trigger re-authentication

Frontend Errors

  • Failed refresh attempts redirect to login
  • Network errors show appropriate user messages
  • Graceful degradation when refresh is unavailable

Configuration

Backend Settings

# app/core/config.py
ACCESS_TOKEN_EXPIRE_MINUTES: int = 240  # 4 hours - reduced likelihood of expiry during sleep
REFRESH_TOKEN_EXPIRE_DAYS: int = 7
JWT_ISSUER: str = "tracker-api"
JWT_AUDIENCE: str = "tracker-clients"

Frontend Configuration

// Token refresh threshold (5 minutes)
const REFRESH_THRESHOLD = 300; // seconds

// Refresh queue management
let isRefreshing = false;
let refreshQueue: Array<{
  resolve: (token: string) => void;
  reject: (error: any) => void;
}> = [];

Testing

Comprehensive tests cover:

  • Middleware token refresh functionality
  • Endpoint refresh operations
  • Error handling scenarios
  • Concurrent request handling
  • Cookie management

Example Test

def test_middleware_refreshes_expiring_token(self, client: TestClient, db_session):
    """Test that middleware automatically refreshes tokens about to expire."""
    user = create_test_user(db_session, email="test@example.com")

    # Create token expiring in 2 minutes
    short_expiry = timedelta(minutes=2)
    access_token = create_access_token(user.id, expires_delta=short_expiry)
    refresh_token = create_refresh_token(user.id)

    client.cookies.set("refresh_token", refresh_token)

    response = client.get(
        "/api/v1/users/me/",
        headers={"Authorization": f"Bearer {access_token}"}
    )

    assert response.status_code == 200
    assert "X-New-Access-Token" in response.headers

Monitoring and Debugging

Logging

The system logs refresh attempts for monitoring:

logger.info(f"Successfully refreshed token for user {user_id}")
logger.debug("Access token is expiring soon, attempting refresh")

Response Headers

New tokens are provided in response headers:

response.headers["X-New-Access-Token"] = new_token

Apple Safari and Tab Sleep Handling

The Tab Sleep Problem

Apple Safari (especially on iOS and iPadOS) aggressively puts tabs to sleep to conserve battery and memory. This creates unique challenges for web applications:

What Happens During Tab Sleep:

  • JavaScript execution stops - No timers, no background refresh attempts
  • Network requests are paused - Ongoing API calls may fail or timeout
  • Memory state is preserved - But no active code runs
  • Wake-up triggers - User interaction, tab focus, or visibility changes

Impact on JWT Authentication:

  • Proactive refresh fails - 5-minute timer doesn't fire when tab is sleeping
  • Access tokens expire silently - User returns to expired session
  • First API call fails - 401 error when tab wakes up
  • Poor user experience - Unexpected login prompts after breaks

Our Solution: Wake-Up Session Restoration

Detection Mechanisms:

// Detect tab sleep based on activity gap
const wasTabSleeping = (): boolean => {
  const timeSinceLastActivity = Date.now() - lastActivity;
  return timeSinceLastActivity > 5 * 60 * 1000; // 5 minutes
};

// Listen for tab becoming visible
document.addEventListener("visibilitychange", () => {
  if (!document.hidden && wasTabSleeping()) {
    handleWakeUp();
  }
});

// Listen for window focus (browser activation)
window.addEventListener("focus", () => {
  if (wasTabSleeping()) {
    handleWakeUp();
  }
});

Wake-Up Response:

  1. Immediate Token Check - Validate current access token
  2. Automatic Refresh - Use refresh cookie if token expired
  3. Silent Recovery - No user interruption if refresh succeeds
  4. Graceful Fallback - Redirect to login only if refresh fails

Activity Tracking:

// Track user interactions to determine sleep state
["mousedown", "keydown", "scroll", "touchstart"].forEach((event) => {
  document.addEventListener(event, updateActivity, { passive: true });
});

Safari-Specific Considerations

iOS Safari Behavior:

  • Aggressive tab sleeping - Tabs sleep after just a few minutes of inactivity
  • Background app suspension - Entire browser may be suspended
  • Memory pressure - Tabs may be completely unloaded and reloaded
  • Network limitations - Background network requests are restricted

Our Optimizations:

  • Extended token lifetime - 4-hour access tokens reduce sleep impact
  • Immediate wake-up validation - Check tokens as soon as tab becomes active
  • Refresh cookie fallback - Always attempt session restoration first
  • Activity-based detection - Smart detection of actual vs. perceived sleep

Testing on Apple Devices:

  1. Open app in Safari - Test on actual iOS/iPadOS devices
  2. Switch to another app - Let Safari tab go to background
  3. Wait 10+ minutes - Ensure tab enters sleep state
  4. Return to app - Should automatically restore session
  5. Check console logs - Verify wake-up detection and token refresh

Configuration for Mobile Optimization

Backend Settings:

# Longer token lifetime for mobile users
ACCESS_TOKEN_EXPIRE_MINUTES: int = 240  # 4 hours
REFRESH_TOKEN_EXPIRE_DAYS: int = 7      # 1 week

# Middleware refresh threshold
REFRESH_THRESHOLD_MINUTES: int = 5      # Refresh 5 min before expiry

Frontend Settings:

// Wake-up detection threshold
const SLEEP_DETECTION_THRESHOLD = 5 * 60 * 1000; // 5 minutes

// Activity tracking events (optimized for mobile)
const ACTIVITY_EVENTS = [
  "mousedown", // Desktop clicks
  "keydown", // Keyboard input
  "scroll", // Page scrolling
  "touchstart", // Mobile touches
];

Troubleshooting Safari Issues

Common Problems:

1. Session Not Restoring After Sleep

  • Check refresh cookie is present and valid
  • Verify wake-up event listeners are active
  • Look for console errors during restoration

2. Multiple Login Prompts

  • Ensure proper activity tracking
  • Check for race conditions in wake-up handling
  • Verify token refresh queue management

3. Poor Performance on Mobile

  • Monitor network requests during wake-up
  • Optimize API calls for mobile networks
  • Consider offline capability for critical features

Debug Steps:

// Enable detailed logging for Safari debugging
console.log("Tab visibility:", document.hidden);
console.log("Last activity:", new Date(lastActivity));
console.log("Time since activity:", Date.now() - lastActivity);
console.log(
  "Refresh cookie present:",
  document.cookie.includes("refresh_token"),
);

Safari Developer Tools:

  1. Enable Web Inspector - Settings > Advanced > Web Inspector
  2. Connect to device - Safari > Develop > [Device Name]
  3. Monitor console - Watch for wake-up detection logs
  4. Network tab - Verify refresh requests succeed
  5. Application tab - Check cookie storage

Best Practices for Mobile Web Apps

Design Considerations:

  • Assume tab sleep will happen - Design for interruption
  • Minimize login friction - Use automatic session restoration
  • Provide clear feedback - Show loading states during restoration
  • Handle offline scenarios - Graceful degradation when network fails

Performance Optimization:

  • Lazy load resources - Don't load everything on wake-up
  • Cache critical data - Reduce API calls after sleep
  • Optimize bundle size - Faster loading on mobile networks
  • Use service workers - Background sync where supported

Best Practices

  1. Proactive Refresh: Refresh tokens before they expire
  2. Queue Management: Handle concurrent requests during refresh
  3. Error Recovery: Graceful fallback to login when refresh fails
  4. Security: Use HttpOnly cookies for refresh tokens
  5. Monitoring: Log refresh attempts for debugging
  6. Testing: Comprehensive test coverage for all scenarios
  7. Mobile Optimization: Design for tab sleep and network interruptions
  8. Safari Testing: Always test on actual Apple devices
  9. Consistent Implementation: Use identical patterns across all frontend applications
  10. Avoid Conflicting Hooks: Don't create custom refresh logic that conflicts with built-in mechanisms

Troubleshooting

Common Issues

  1. Infinite Refresh Loops: Ensure auth endpoints are excluded from middleware
  2. Cookie Issues: Verify CORS and cookie settings for cross-origin requests
  3. Token Validation: Check JWT claims and signature validation
  4. Timing Issues: Adjust refresh threshold based on network latency
  5. Admin Panel Logout Issues: If admin panel loses authentication frequently, verify it uses the same pattern as frontend (see Admin Panel Authentication Sync Fix)
  6. Conflicting Refresh Logic: Avoid multiple token refresh implementations in the same application

Debug Steps

  1. Check browser network tab for refresh requests
  2. Verify refresh token cookie is present and valid
  3. Check backend logs for refresh attempts
  4. Validate JWT token structure and claims
  5. Test with different token expiration times

This implementation provides a robust, secure, and user-friendly authentication experience that minimizes disruption while maintaining security best practices.