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
- Token Inspection: Middleware checks if the access token will expire within 5 minutes
- Automatic Refresh: If expiring, attempts to refresh using the refresh token cookie
- Request Processing: Updates the request with the new token and continues processing
- 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
useTokenRefreshhook 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
WakeUpDetectorfor tab sleep handling - Request/response interceptors with refresh queuing
- Automatic refresh on 401 errors
SessionManagerfor 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
Cookie Configuration
@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:
- Immediate Token Check - Validate current access token
- Automatic Refresh - Use refresh cookie if token expired
- Silent Recovery - No user interruption if refresh succeeds
- 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:
- Open app in Safari - Test on actual iOS/iPadOS devices
- Switch to another app - Let Safari tab go to background
- Wait 10+ minutes - Ensure tab enters sleep state
- Return to app - Should automatically restore session
- 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:
- Enable Web Inspector - Settings > Advanced > Web Inspector
- Connect to device - Safari > Develop > [Device Name]
- Monitor console - Watch for wake-up detection logs
- Network tab - Verify refresh requests succeed
- 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
- Proactive Refresh: Refresh tokens before they expire
- Queue Management: Handle concurrent requests during refresh
- Error Recovery: Graceful fallback to login when refresh fails
- Security: Use HttpOnly cookies for refresh tokens
- Monitoring: Log refresh attempts for debugging
- Testing: Comprehensive test coverage for all scenarios
- Mobile Optimization: Design for tab sleep and network interruptions
- Safari Testing: Always test on actual Apple devices
- Consistent Implementation: Use identical patterns across all frontend applications
- Avoid Conflicting Hooks: Don't create custom refresh logic that conflicts with built-in mechanisms
Troubleshooting
Common Issues
- Infinite Refresh Loops: Ensure auth endpoints are excluded from middleware
- Cookie Issues: Verify CORS and cookie settings for cross-origin requests
- Token Validation: Check JWT claims and signature validation
- Timing Issues: Adjust refresh threshold based on network latency
- Admin Panel Logout Issues: If admin panel loses authentication frequently, verify it uses the same pattern as frontend (see Admin Panel Authentication Sync Fix)
- Conflicting Refresh Logic: Avoid multiple token refresh implementations in the same application
Debug Steps
- Check browser network tab for refresh requests
- Verify refresh token cookie is present and valid
- Check backend logs for refresh attempts
- Validate JWT token structure and claims
- 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.