Files
2026-01-28 11:41:24 -05:00

4.1 KiB

Route Authentication and Protection

Status: Implemented | Token expiry validation, auto-refresh, 401 handling

Authentication and authorization system protecting routes, ensuring only authenticated users can access protected pages.

Protection Strategy

Client-Side: React components check auth state, redirect to login if needed, preserve original URL Server-Side: API routes validate JWT tokens via middleware, return 401/403 for unauthorized

Routes

Public: /login, /setup, /api/* (handle auth independently) Protected: / (home), /search, /requests, /profile Admin: /admin/* - requires admin role

ProtectedRoute Component

Location: src/components/auth/ProtectedRoute.tsx

Behavior:

  1. Check auth state from AuthContext
  2. Optionally check admin role
  3. Show loading spinner while checking
  4. Redirect to /login if unauthenticated
  5. Redirect to / if admin required but not admin
  6. Render children if authorized

API Middleware

Location: src/lib/middleware/auth.ts

Server-side validation:

  • requireAuth() - validates JWT, adds user to request
  • requireAdmin() - checks admin role, chains after requireAuth
  • Returns 401 for invalid/expired tokens
  • Returns 403 for insufficient permissions

Token Management

Location: src/contexts/AuthContext.tsx, src/lib/utils/jwt-client.ts

Token Validation on Mount:

  • Decodes access token to check expiry
  • If expired but refresh token valid → auto-refresh
  • If both expired → clear storage, redirect to login
  • Cross-tab logout sync via storage events

Auto-Refresh (5 mins before expiry):

const refreshTimeMs = getRefreshTimeMs(token);
setTimeout(() => refreshToken(), refreshTimeMs);

Schedule:

  • After login → schedule first refresh
  • After token refresh → schedule next refresh
  • Cleanup on logout or unmount

API Client with 401 Handling

Location: src/lib/utils/api.ts

fetchWithAuth():

  • Adds Authorization header automatically
  • Catches 401 responses
  • Attempts token refresh once
  • Retries original request with new token
  • Logs out if refresh fails
  • Prevents duplicate refresh requests

Usage:

// In hooks/components
import { fetchWithAuth, fetchJSON } from '@/lib/utils/api';

// GET request
const response = await fetchWithAuth('/api/requests');

// POST with JSON
const data = await fetchJSON('/api/requests', {
  method: 'POST',
  body: JSON.stringify({ audiobook }),
});

Error Handling

401 Unauthorized:

  1. Attempt token refresh automatically
  2. Retry original request with new token
  3. If still 401 or refresh fails → logout (clears storage + redirects to /login)

403 Forbidden:

  • Valid token but insufficient permissions
  • Return error, don't logout

Logout Behavior

Global redirect on logout:

  • logout() from AuthContext → clears storage + redirects to /login
  • API 401 errors → performLogout() → clears storage + redirects to /login
  • Cross-tab logout → storage event triggers redirect to /login
  • Ensures user never remains on authenticated pages after logout

Cross-Tab Sync

Storage Events:

  • Logout in one tab → logout + redirect to login in all tabs
  • Login in one tab → sync auth state to all tabs
  • Prevents stale sessions across browser tabs

Security

  • Never log tokens
  • HTTPS only in production
  • Short access token expiry (1hr)
  • Auto-refresh 5 mins before expiry
  • Token expiry validation on mount
  • Prevent duplicate refresh requests
  • SameSite cookies for CSRF protection
  • Client-side token decode (signature verified server-side only)

Fixed Issues

  • Expired tokens not logging out: Added token expiry validation on mount
  • No auto-refresh: Scheduled refresh 5 mins before token expires
  • 401 errors not handled: Added global 401 interceptor with token refresh
  • Logged-out sessions persisting: Token validation clears expired sessions immediately
  • Logout not redirecting: Added automatic redirect to /login on all logout scenarios (manual, API 401, cross-tab)

Tech Stack

  • Next.js 14+ App Router
  • JWT via AuthContext
  • React Context API
  • Custom fetch wrapper for 401 handling