Back to Blog

Authentication & Session Architecture: From JWTs to Zero-Trust

Authentication Architecture Overview

Authentication is the cornerstone of application security. The architecture you choose affects security posture, user experience, scalability, and operational complexity.

┌─────────────────────────────────────────────────────────────────────────────┐
│                    Authentication Methods Comparison                         │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│              Session-Based         Token-Based (JWT)      Token + Refresh   │
│  ┌────────────────────────────────────────────────────────────────────────┐│
│  │ Storage    │ Server (Redis/DB) │ Client only        │ Client + Server  ││
│  │ Stateless  │ No                │ Yes                │ Partial          ││
│  │ Revocation │ Immediate         │ Difficult          │ Via refresh      ││
│  │ Scalability│ Needs shared store│ Excellent          │ Good             ││
│  │ Security   │ CSRF vulnerable   │ XSS vulnerable     │ Balanced         ││
│  │ Size       │ Small cookie      │ Large (payload)    │ Medium           ││
│  │ Mobile     │ Cookie issues     │ Works well         │ Works well       ││
│  └────────────────────────────────────────────────────────────────────────┘│
│                                                                             │
│  Modern Recommendation: Short-lived JWT + Refresh Token Rotation            │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

JWT Architecture Deep Dive

JWT Structure and Validation

┌─────────────────────────────────────────────────────────────────────────────┐
│                    JWT Token Structure                                       │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.                    ← Header (Base64)│
│  eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4iLCJpYXQiOjE1MTYyMzkwMjJ9. │
│  SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c           ← Signature       │
│                                                                             │
│  Header (decoded):                                                          │
│  {                                                                          │
│    "alg": "RS256",    // Algorithm: RSA with SHA-256                       │
│    "typ": "JWT",      // Token type                                        │
│    "kid": "key-1"     // Key ID for key rotation                           │
│  }                                                                          │
│                                                                             │
│  Payload (decoded):                                                         │
│  {                                                                          │
│    "sub": "user-123",           // Subject (user ID)                       │
│    "iss": "https://auth.app",   // Issuer                                  │
│    "aud": "https://api.app",    // Audience                                │
│    "exp": 1699999999,           // Expiration (Unix timestamp)             │
│    "iat": 1699996399,           // Issued at                               │
│    "jti": "unique-token-id",    // JWT ID (for revocation)                 │
│    "roles": ["user", "admin"],  // Custom claims                           │
│    "permissions": ["read", "write"]                                        │
│  }                                                                          │
│                                                                             │
│  Signature = RS256(                                                         │
│    base64UrlEncode(header) + "." + base64UrlEncode(payload),               │
│    privateKey                                                               │
│  )                                                                          │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

Token Service Implementation

// ============================================================
// JWT Token Service
// ============================================================

import jwt, { JwtPayload, SignOptions, VerifyOptions } from 'jsonwebtoken';
import crypto from 'crypto';

interface TokenPayload {
  sub: string;
  email: string;
  roles: string[];
  permissions: string[];
}

interface TokenPair {
  accessToken: string;
  refreshToken: string;
  expiresIn: number;
}

interface RefreshTokenData {
  userId: string;
  tokenFamily: string;
  createdAt: Date;
  expiresAt: Date;
  revoked: boolean;
}

class TokenService {
  private readonly accessTokenSecret: string;
  private readonly refreshTokenSecret: string;
  private readonly accessTokenExpiry = '15m';
  private readonly refreshTokenExpiry = '7d';

  constructor(
    private readonly tokenStore: TokenStore,
    config: { accessSecret: string; refreshSecret: string }
  ) {
    this.accessTokenSecret = config.accessSecret;
    this.refreshTokenSecret = config.refreshSecret;
  }

  async generateTokenPair(user: User): Promise<TokenPair> {
    const tokenFamily = crypto.randomUUID();

    const accessToken = this.generateAccessToken(user);
    const refreshToken = await this.generateRefreshToken(user.id, tokenFamily);

    return {
      accessToken,
      refreshToken,
      expiresIn: 900, // 15 minutes in seconds
    };
  }

  private generateAccessToken(user: User): string {
    const payload: TokenPayload = {
      sub: user.id,
      email: user.email,
      roles: user.roles,
      permissions: this.derivePermissions(user.roles),
    };

    const options: SignOptions = {
      algorithm: 'RS256',
      expiresIn: this.accessTokenExpiry,
      issuer: 'https://auth.example.com',
      audience: 'https://api.example.com',
      jwtid: crypto.randomUUID(),
    };

    return jwt.sign(payload, this.accessTokenSecret, options);
  }

  private async generateRefreshToken(
    userId: string,
    tokenFamily: string
  ): Promise<string> {
    const tokenId = crypto.randomUUID();
    const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);

    // Store refresh token metadata
    await this.tokenStore.saveRefreshToken({
      id: tokenId,
      userId,
      tokenFamily,
      createdAt: new Date(),
      expiresAt,
      revoked: false,
    });

    const payload = {
      sub: userId,
      family: tokenFamily,
      jti: tokenId,
    };

    return jwt.sign(payload, this.refreshTokenSecret, {
      expiresIn: this.refreshTokenExpiry,
    });
  }

  async verifyAccessToken(token: string): Promise<TokenPayload> {
    const options: VerifyOptions = {
      algorithms: ['RS256'],
      issuer: 'https://auth.example.com',
      audience: 'https://api.example.com',
    };

    try {
      const payload = jwt.verify(token, this.accessTokenSecret, options);
      return payload as TokenPayload;
    } catch (error) {
      if (error instanceof jwt.TokenExpiredError) {
        throw new AuthError('TOKEN_EXPIRED', 'Access token has expired');
      }
      if (error instanceof jwt.JsonWebTokenError) {
        throw new AuthError('INVALID_TOKEN', 'Invalid access token');
      }
      throw error;
    }
  }

  async refreshTokens(refreshToken: string): Promise<TokenPair> {
    let payload: JwtPayload;

    try {
      payload = jwt.verify(refreshToken, this.refreshTokenSecret) as JwtPayload;
    } catch {
      throw new AuthError('INVALID_REFRESH_TOKEN', 'Invalid refresh token');
    }

    const storedToken = await this.tokenStore.getRefreshToken(payload.jti!);

    if (!storedToken) {
      throw new AuthError('TOKEN_NOT_FOUND', 'Refresh token not found');
    }

    if (storedToken.revoked) {
      // Potential token theft - revoke entire family
      await this.tokenStore.revokeTokenFamily(storedToken.tokenFamily);
      throw new AuthError('TOKEN_REUSE', 'Refresh token has been revoked');
    }

    if (storedToken.expiresAt < new Date()) {
      throw new AuthError('TOKEN_EXPIRED', 'Refresh token has expired');
    }

    // Revoke old refresh token (rotation)
    await this.tokenStore.revokeRefreshToken(payload.jti!);

    // Generate new token pair with same family
    const user = await this.userService.findById(storedToken.userId);
    const accessToken = this.generateAccessToken(user);
    const newRefreshToken = await this.generateRefreshToken(
      user.id,
      storedToken.tokenFamily
    );

    return {
      accessToken,
      refreshToken: newRefreshToken,
      expiresIn: 900,
    };
  }

  async revokeAllUserTokens(userId: string): Promise<void> {
    await this.tokenStore.revokeAllUserTokens(userId);
  }

  private derivePermissions(roles: string[]): string[] {
    const permissionMap: Record<string, string[]> = {
      admin: ['read', 'write', 'delete', 'manage_users'],
      editor: ['read', 'write'],
      viewer: ['read'],
    };

    const permissions = new Set<string>();
    roles.forEach((role) => {
      permissionMap[role]?.forEach((p) => permissions.add(p));
    });

    return Array.from(permissions);
  }
}

class AuthError extends Error {
  constructor(
    public code: string,
    message: string
  ) {
    super(message);
    this.name = 'AuthError';
  }
}

Frontend Authentication Implementation

Auth Context and Provider

// ============================================================
// React Auth Context
// ============================================================

interface User {
  id: string;
  email: string;
  name: string;
  roles: string[];
  permissions: string[];
}

interface AuthState {
  user: User | null;
  isAuthenticated: boolean;
  isLoading: boolean;
}

interface AuthContextType extends AuthState {
  login: (email: string, password: string) => Promise<void>;
  logout: () => Promise<void>;
  refreshSession: () => Promise<void>;
  hasPermission: (permission: string) => boolean;
  hasRole: (role: string) => boolean;
}

const AuthContext = createContext<AuthContextType | null>(null);

// Token storage with security considerations
class TokenStorage {
  private accessToken: string | null = null;

  // Access token in memory only (XSS mitigation)
  setAccessToken(token: string): void {
    this.accessToken = token;
  }

  getAccessToken(): string | null {
    return this.accessToken;
  }

  clearAccessToken(): void {
    this.accessToken = null;
  }

  // Refresh token in httpOnly cookie (set by server)
  // No client-side access needed
}

const tokenStorage = new TokenStorage();

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [state, setState] = useState<AuthState>({
    user: null,
    isAuthenticated: false,
    isLoading: true,
  });

  // Initialize auth state on mount
  useEffect(() => {
    initializeAuth();
  }, []);

  // Set up token refresh interval
  useEffect(() => {
    if (!state.isAuthenticated) return;

    // Refresh token 1 minute before expiry
    const refreshInterval = setInterval(() => {
      refreshSession();
    }, 14 * 60 * 1000); // 14 minutes

    return () => clearInterval(refreshInterval);
  }, [state.isAuthenticated]);

  // Handle tab visibility for session sync
  useEffect(() => {
    const handleVisibilityChange = () => {
      if (document.visibilityState === 'visible') {
        // Re-validate session when tab becomes visible
        validateSession();
      }
    };

    document.addEventListener('visibilitychange', handleVisibilityChange);
    return () => {
      document.removeEventListener('visibilitychange', handleVisibilityChange);
    };
  }, []);

  async function initializeAuth(): Promise<void> {
    try {
      // Try to refresh session using httpOnly refresh token cookie
      const response = await fetch('/api/auth/refresh', {
        method: 'POST',
        credentials: 'include', // Include cookies
      });

      if (response.ok) {
        const data = await response.json();
        tokenStorage.setAccessToken(data.accessToken);
        setState({
          user: data.user,
          isAuthenticated: true,
          isLoading: false,
        });
      } else {
        setState({
          user: null,
          isAuthenticated: false,
          isLoading: false,
        });
      }
    } catch {
      setState({
        user: null,
        isAuthenticated: false,
        isLoading: false,
      });
    }
  }

  async function login(email: string, password: string): Promise<void> {
    const response = await fetch('/api/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      credentials: 'include',
      body: JSON.stringify({ email, password }),
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.message);
    }

    const data = await response.json();
    tokenStorage.setAccessToken(data.accessToken);
    setState({
      user: data.user,
      isAuthenticated: true,
      isLoading: false,
    });
  }

  async function logout(): Promise<void> {
    try {
      await fetch('/api/auth/logout', {
        method: 'POST',
        credentials: 'include',
      });
    } finally {
      tokenStorage.clearAccessToken();
      setState({
        user: null,
        isAuthenticated: false,
        isLoading: false,
      });
    }
  }

  async function refreshSession(): Promise<void> {
    try {
      const response = await fetch('/api/auth/refresh', {
        method: 'POST',
        credentials: 'include',
      });

      if (response.ok) {
        const data = await response.json();
        tokenStorage.setAccessToken(data.accessToken);
      } else {
        // Refresh failed - log out user
        await logout();
      }
    } catch {
      await logout();
    }
  }

  async function validateSession(): Promise<void> {
    const token = tokenStorage.getAccessToken();
    if (!token) {
      await refreshSession();
      return;
    }

    // Check if token is about to expire
    const payload = JSON.parse(atob(token.split('.')[1]));
    const expiresAt = payload.exp * 1000;
    const now = Date.now();

    if (expiresAt - now < 60000) {
      // Less than 1 minute to expiry
      await refreshSession();
    }
  }

  function hasPermission(permission: string): boolean {
    return state.user?.permissions.includes(permission) ?? false;
  }

  function hasRole(role: string): boolean {
    return state.user?.roles.includes(role) ?? false;
  }

  return (
    <AuthContext.Provider
      value={{
        ...state,
        login,
        logout,
        refreshSession,
        hasPermission,
        hasRole,
      }}
    >
      {children}
    </AuthContext.Provider>
  );
}

export function useAuth(): AuthContextType {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return context;
}

// ============================================================
// Authenticated Fetch Wrapper
// ============================================================

export async function authFetch(
  url: string,
  options: RequestInit = {}
): Promise<Response> {
  const token = tokenStorage.getAccessToken();

  const response = await fetch(url, {
    ...options,
    headers: {
      ...options.headers,
      Authorization: token ? `Bearer ${token}` : '',
    },
    credentials: 'include',
  });

  // Handle token expiration
  if (response.status === 401) {
    // Try to refresh token
    const refreshResponse = await fetch('/api/auth/refresh', {
      method: 'POST',
      credentials: 'include',
    });

    if (refreshResponse.ok) {
      const data = await refreshResponse.json();
      tokenStorage.setAccessToken(data.accessToken);

      // Retry original request
      return fetch(url, {
        ...options,
        headers: {
          ...options.headers,
          Authorization: `Bearer ${data.accessToken}`,
        },
        credentials: 'include',
      });
    }

    // Refresh failed - redirect to login
    window.location.href = '/login';
  }

  return response;
}

Protected Routes

// ============================================================
// Route Protection Components
// ============================================================

interface ProtectedRouteProps {
  children: React.ReactNode;
  requiredPermissions?: string[];
  requiredRoles?: string[];
  fallback?: React.ReactNode;
}

export function ProtectedRoute({
  children,
  requiredPermissions = [],
  requiredRoles = [],
  fallback = <Navigate to="/login" />,
}: ProtectedRouteProps) {
  const { isAuthenticated, isLoading, hasPermission, hasRole } = useAuth();

  if (isLoading) {
    return <LoadingScreen />;
  }

  if (!isAuthenticated) {
    return fallback;
  }

  // Check permissions
  const hasRequiredPermissions = requiredPermissions.every(hasPermission);
  const hasRequiredRoles =
    requiredRoles.length === 0 || requiredRoles.some(hasRole);

  if (!hasRequiredPermissions || !hasRequiredRoles) {
    return <ForbiddenPage />;
  }

  return <>{children}</>;
}

// Usage
function App() {
  return (
    <AuthProvider>
      <Router>
        <Routes>
          <Route path="/login" element={<LoginPage />} />
          <Route path="/register" element={<RegisterPage />} />

          <Route
            path="/dashboard"
            element={
              <ProtectedRoute>
                <DashboardPage />
              </ProtectedRoute>
            }
          />

          <Route
            path="/admin"
            element={
              <ProtectedRoute requiredRoles={['admin']}>
                <AdminPage />
              </ProtectedRoute>
            }
          />

          <Route
            path="/settings"
            element={
              <ProtectedRoute requiredPermissions={['manage_settings']}>
                <SettingsPage />
              </ProtectedRoute>
            }
          />
        </Routes>
      </Router>
    </AuthProvider>
  );
}

// Permission-based UI rendering
export function PermissionGate({
  permission,
  children,
  fallback = null,
}: {
  permission: string;
  children: React.ReactNode;
  fallback?: React.ReactNode;
}) {
  const { hasPermission } = useAuth();

  if (!hasPermission(permission)) {
    return <>{fallback}</>;
  }

  return <>{children}</>;
}

// Usage
function ArticleActions({ article }: { article: Article }) {
  return (
    <div>
      <button>View</button>

      <PermissionGate permission="write">
        <button>Edit</button>
      </PermissionGate>

      <PermissionGate permission="delete">
        <button>Delete</button>
      </PermissionGate>
    </div>
  );
}

OAuth 2.0 / OpenID Connect Integration

┌─────────────────────────────────────────────────────────────────────────────┐
│                    OAuth 2.0 Authorization Code Flow with PKCE              │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  User        Frontend        Backend         Auth Server    Resource Server │
│   │             │               │                 │               │         │
│   │ 1. Click    │               │                 │               │         │
│   │  "Login"    │               │                 │               │         │
│   │────────────►│               │                 │               │         │
│   │             │               │                 │               │         │
│   │             │ 2. Generate code_verifier,      │               │         │
│   │             │    code_challenge = SHA256(verifier)            │         │
│   │             │               │                 │               │         │
│   │             │ 3. Redirect to Auth Server      │               │         │
│   │◄────────────│───────────────────────────────►│               │         │
│   │             │    /authorize?                  │               │         │
│   │             │      response_type=code&        │               │         │
│   │             │      client_id=xxx&             │               │         │
│   │             │      redirect_uri=xxx&          │               │         │
│   │             │      code_challenge=xxx&        │               │         │
│   │             │      code_challenge_method=S256&│               │         │
│   │             │      scope=openid profile       │               │         │
│   │             │               │                 │               │         │
│   │ 4. Login at Auth Server     │                 │               │         │
│   │────────────────────────────────────────────►│               │         │
│   │             │               │                 │               │         │
│   │ 5. Redirect back with code  │                 │               │         │
│   │◄───────────────────────────────────────────│               │         │
│   │  /callback?code=xxx         │                 │               │         │
│   │────────────►│               │                 │               │         │
│   │             │               │                 │               │         │
│   │             │ 6. Send code + code_verifier    │               │         │
│   │             │───────────────►                 │               │         │
│   │             │               │ 7. POST /token  │               │         │
│   │             │               │────────────────►│               │         │
│   │             │               │   code=xxx&     │               │         │
│   │             │               │   code_verifier=xxx             │         │
│   │             │               │                 │               │         │
│   │             │               │ 8. Verify PKCE, │               │         │
│   │             │               │    return tokens│               │         │
│   │             │               │◄────────────────│               │         │
│   │             │               │   {access_token,│               │         │
│   │             │               │    id_token,    │               │         │
│   │             │               │    refresh_token}               │         │
│   │             │ 9. Set session│                 │               │         │
│   │             │◄──────────────│                 │               │         │
│   │             │               │                 │               │         │
│   │ 10. Authenticated           │                 │               │         │
│   │◄────────────│               │                 │               │         │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

OAuth Client Implementation

// ============================================================
// OAuth 2.0 + PKCE Client
// ============================================================

interface OAuthConfig {
  clientId: string;
  authorizationEndpoint: string;
  tokenEndpoint: string;
  redirectUri: string;
  scopes: string[];
}

class OAuthClient {
  private codeVerifier: string | null = null;

  constructor(private config: OAuthConfig) {}

  async initiateLogin(): Promise<void> {
    // Generate PKCE code verifier and challenge
    this.codeVerifier = this.generateCodeVerifier();
    const codeChallenge = await this.generateCodeChallenge(this.codeVerifier);

    // Store verifier for token exchange
    sessionStorage.setItem('oauth_code_verifier', this.codeVerifier);

    // Generate state for CSRF protection
    const state = crypto.randomUUID();
    sessionStorage.setItem('oauth_state', state);

    // Build authorization URL
    const params = new URLSearchParams({
      response_type: 'code',
      client_id: this.config.clientId,
      redirect_uri: this.config.redirectUri,
      scope: this.config.scopes.join(' '),
      state,
      code_challenge: codeChallenge,
      code_challenge_method: 'S256',
    });

    // Redirect to authorization server
    window.location.href = `${this.config.authorizationEndpoint}?${params}`;
  }

  async handleCallback(code: string, state: string): Promise<TokenResponse> {
    // Verify state
    const storedState = sessionStorage.getItem('oauth_state');
    if (state !== storedState) {
      throw new Error('Invalid state parameter');
    }

    // Get code verifier
    const codeVerifier = sessionStorage.getItem('oauth_code_verifier');
    if (!codeVerifier) {
      throw new Error('Missing code verifier');
    }

    // Exchange code for tokens
    const response = await fetch(this.config.tokenEndpoint, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: new URLSearchParams({
        grant_type: 'authorization_code',
        client_id: this.config.clientId,
        code,
        redirect_uri: this.config.redirectUri,
        code_verifier: codeVerifier,
      }),
    });

    if (!response.ok) {
      throw new Error('Token exchange failed');
    }

    // Clean up
    sessionStorage.removeItem('oauth_code_verifier');
    sessionStorage.removeItem('oauth_state');

    return response.json();
  }

  private generateCodeVerifier(): string {
    const array = new Uint8Array(32);
    crypto.getRandomValues(array);
    return this.base64UrlEncode(array);
  }

  private async generateCodeChallenge(verifier: string): Promise<string> {
    const encoder = new TextEncoder();
    const data = encoder.encode(verifier);
    const hash = await crypto.subtle.digest('SHA-256', data);
    return this.base64UrlEncode(new Uint8Array(hash));
  }

  private base64UrlEncode(array: Uint8Array): string {
    return btoa(String.fromCharCode(...array))
      .replace(/\+/g, '-')
      .replace(/\//g, '_')
      .replace(/=+$/, '');
  }
}

interface TokenResponse {
  access_token: string;
  token_type: string;
  expires_in: number;
  refresh_token?: string;
  id_token?: string;
}

// Social Login Buttons
function SocialLoginButtons() {
  const googleClient = new OAuthClient({
    clientId: process.env.GOOGLE_CLIENT_ID!,
    authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
    tokenEndpoint: 'https://oauth2.googleapis.com/token',
    redirectUri: `${window.location.origin}/auth/callback/google`,
    scopes: ['openid', 'email', 'profile'],
  });

  const githubClient = new OAuthClient({
    clientId: process.env.GITHUB_CLIENT_ID!,
    authorizationEndpoint: 'https://github.com/login/oauth/authorize',
    tokenEndpoint: 'https://github.com/login/oauth/access_token',
    redirectUri: `${window.location.origin}/auth/callback/github`,
    scopes: ['read:user', 'user:email'],
  });

  return (
    <div className="social-login">
      <button onClick={() => googleClient.initiateLogin()}>
        Continue with Google
      </button>
      <button onClick={() => githubClient.initiateLogin()}>
        Continue with GitHub
      </button>
    </div>
  );
}

Session Security Best Practices

// ============================================================
// Security Headers and Cookie Configuration
// ============================================================

// Server-side cookie settings
const COOKIE_OPTIONS = {
  httpOnly: true,           // Prevent JavaScript access
  secure: true,             // HTTPS only
  sameSite: 'strict' as const, // CSRF protection
  path: '/',
  maxAge: 7 * 24 * 60 * 60, // 7 days
  domain: '.example.com',   // Allow subdomains
};

// Express middleware for security headers
function securityHeaders(req: Request, res: Response, next: NextFunction) {
  // Prevent clickjacking
  res.setHeader('X-Frame-Options', 'DENY');

  // Prevent MIME sniffing
  res.setHeader('X-Content-Type-Options', 'nosniff');

  // XSS protection
  res.setHeader('X-XSS-Protection', '1; mode=block');

  // Content Security Policy
  res.setHeader(
    'Content-Security-Policy',
    "default-src 'self'; " +
    "script-src 'self' 'unsafe-inline'; " +
    "style-src 'self' 'unsafe-inline'; " +
    "img-src 'self' data: https:; " +
    "connect-src 'self' https://api.example.com; " +
    "frame-ancestors 'none';"
  );

  // HSTS
  res.setHeader(
    'Strict-Transport-Security',
    'max-age=31536000; includeSubDomains; preload'
  );

  next();
}

// ============================================================
// CSRF Protection
// ============================================================

import csrf from 'csurf';
import cookieParser from 'cookie-parser';

// CSRF middleware
const csrfProtection = csrf({
  cookie: {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
  },
});

// Provide CSRF token to frontend
app.get('/api/csrf-token', csrfProtection, (req, res) => {
  res.json({ csrfToken: req.csrfToken() });
});

// Frontend: Include CSRF token in requests
async function fetchWithCSRF(url: string, options: RequestInit = {}) {
  // Get CSRF token
  const tokenResponse = await fetch('/api/csrf-token', {
    credentials: 'include',
  });
  const { csrfToken } = await tokenResponse.json();

  return fetch(url, {
    ...options,
    headers: {
      ...options.headers,
      'X-CSRF-Token': csrfToken,
    },
    credentials: 'include',
  });
}

// ============================================================
// Rate Limiting for Auth Endpoints
// ============================================================

import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';

const authLimiter = rateLimit({
  store: new RedisStore({
    client: redisClient,
    prefix: 'rate_limit:auth:',
  }),
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 5, // 5 attempts per window
  message: {
    error: 'Too many login attempts. Please try again later.',
  },
  keyGenerator: (req) => {
    // Rate limit by IP + email combination
    return `${req.ip}:${req.body.email}`;
  },
  skip: (req) => {
    // Skip rate limiting for successful requests
    return false;
  },
});

app.post('/api/auth/login', authLimiter, loginHandler);

Multi-Factor Authentication (MFA)

// ============================================================
// TOTP-based MFA
// ============================================================

import speakeasy from 'speakeasy';
import QRCode from 'qrcode';

class MFAService {
  async setupMFA(user: User): Promise<{ secret: string; qrCode: string }> {
    const secret = speakeasy.generateSecret({
      name: `MyApp:${user.email}`,
      issuer: 'MyApp',
    });

    // Store secret (encrypted) in database
    await this.userService.updateMFASecret(user.id, secret.base32);

    // Generate QR code for authenticator apps
    const qrCode = await QRCode.toDataURL(secret.otpauth_url!);

    return {
      secret: secret.base32,
      qrCode,
    };
  }

  async verifyMFA(user: User, token: string): Promise<boolean> {
    const secret = await this.userService.getMFASecret(user.id);

    if (!secret) {
      throw new Error('MFA not configured');
    }

    return speakeasy.totp.verify({
      secret,
      encoding: 'base32',
      token,
      window: 1, // Allow 1 step tolerance
    });
  }

  async generateBackupCodes(user: User): Promise<string[]> {
    const codes: string[] = [];

    for (let i = 0; i < 10; i++) {
      const code = crypto.randomBytes(4).toString('hex').toUpperCase();
      codes.push(code);
    }

    // Hash and store backup codes
    const hashedCodes = await Promise.all(
      codes.map((code) => bcrypt.hash(code, 10))
    );
    await this.userService.setBackupCodes(user.id, hashedCodes);

    return codes; // Return unhashed codes to user (only time they'll see them)
  }

  async verifyBackupCode(user: User, code: string): Promise<boolean> {
    const hashedCodes = await this.userService.getBackupCodes(user.id);

    for (let i = 0; i < hashedCodes.length; i++) {
      if (await bcrypt.compare(code, hashedCodes[i])) {
        // Remove used backup code
        hashedCodes.splice(i, 1);
        await this.userService.setBackupCodes(user.id, hashedCodes);
        return true;
      }
    }

    return false;
  }
}

// Login flow with MFA
async function loginWithMFA(req: Request, res: Response) {
  const { email, password, mfaToken } = req.body;

  // Step 1: Verify credentials
  const user = await authService.verifyCredentials(email, password);

  if (!user) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  // Step 2: Check if MFA is enabled
  if (user.mfaEnabled) {
    if (!mfaToken) {
      // Return partial authentication - needs MFA
      const partialToken = jwt.sign(
        { sub: user.id, mfaPending: true },
        process.env.JWT_SECRET!,
        { expiresIn: '5m' }
      );

      return res.json({
        requiresMFA: true,
        partialToken,
      });
    }

    // Verify MFA token
    const mfaValid = await mfaService.verifyMFA(user, mfaToken);

    if (!mfaValid) {
      return res.status(401).json({ error: 'Invalid MFA token' });
    }
  }

  // Step 3: Issue full tokens
  const tokens = await tokenService.generateTokenPair(user);

  res.cookie('refreshToken', tokens.refreshToken, COOKIE_OPTIONS);
  res.json({
    accessToken: tokens.accessToken,
    user: sanitizeUser(user),
  });
}

Key Takeaways

  1. Never store access tokens in localStorage: Use memory for access tokens, httpOnly cookies for refresh tokens

  2. Implement token rotation: Refresh tokens should be single-use to detect theft

  3. Short-lived access tokens: 15 minutes max; refresh silently in the background

  4. Always use PKCE for OAuth: Even for confidential clients; it's the modern standard

  5. Defense in depth: Combine multiple security measures (CSRF, rate limiting, MFA)

  6. Secure cookie settings: httpOnly, secure, sameSite=strict

  7. Handle token expiration gracefully: Silent refresh, queue failed requests, retry after refresh

  8. Implement proper logout: Revoke tokens server-side, clear all client storage

  9. Monitor auth events: Log login attempts, failures, token refreshes for security auditing

  10. Plan for session sync: Handle multiple tabs, visibility changes, network reconnection

What did you think?

© 2026 Vidhya Sagar Thakur. All rights reserved.