Home/Blog/Article
Backend & Security

Securing Modern Web Apps: OAuth 2.0, JWT, and Zero Trust Architecture

Comprehensive guide to implementing modern authentication and authorization. Learn OAuth 2.0 flows, JWT best practices, and Zero Trust security principles for web applications.

Oct 9, 2025
14 min read

About the Author

A
Alex Thompson
Security Architect

Alex is a certified security professional with expertise in application security, penetration testing, and secure architecture design. He regularly speaks at security conferences.

Need Expert Help?

Let's discuss how we can help bring your project to life with our web development expertise.


Introduction


Modern web applications face sophisticated security threats requiring robust authentication and authorization systems. This comprehensive guide covers OAuth 2.0, JWT implementation, and Zero Trust architecture principles to build secure web applications.


OAuth 2.0 Fundamentals


OAuth 2.0 is an industry-standard authorization framework that enables secure delegated access without exposing user credentials.


Core OAuth 2.0 Flows


Authorization Code Flow (Most Secure)


// Step 1: Redirect user to authorization server
function initiateAuth() {
  const params = new URLSearchParams({
    client_id: process.env.OAUTH_CLIENT_ID!,
    redirect_uri: 'https://yourapp.com/callback',
    response_type: 'code',
    scope: 'openid profile email',
    state: generateRandomState(), // CSRF protection
    code_challenge: generateCodeChallenge(), // PKCE
    code_challenge_method: 'S256'
  });

  window.location.href = `https://auth.provider.com/authorize?${params}`;
}

// Step 2: Exchange code for tokens
async function handleCallback(code: string) {
  const response = await fetch('https://auth.provider.com/token', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      code,
      redirect_uri: 'https://yourapp.com/callback',
      client_id: process.env.OAUTH_CLIENT_ID!,
      client_secret: process.env.OAUTH_CLIENT_SECRET!,
      code_verifier: getStoredCodeVerifier(), // PKCE
    }),
  });

  const { access_token, refresh_token, id_token } = await response.json();

  // Store tokens securely
  await storeTokens({ access_token, refresh_token, id_token });
}

##JWT (JSON Web Tokens)


Anatomy of a JWT


// JWT structure: header.payload.signature

interface JWTHeader {
  alg: 'HS256' | 'RS256' | 'ES256';
  typ: 'JWT';
  kid?: string; // Key ID
}

interface JWTPayload {
  // Registered claims
  iss: string; // Issuer
  sub: string; // Subject (user ID)
  aud: string | string[]; // Audience
  exp: number; // Expiration time (Unix timestamp)
  nbf?: number; // Not before
  iat: number; // Issued at
  jti?: string; // JWT ID (unique identifier)

  // Custom claims
  email?: string;
  roles?: string[];
  permissions?: string[];
}

// Creating a JWT (Server-side)
import jwt from 'jsonwebtoken';

function createAccessToken(userId: string): string {
  const payload: JWTPayload = {
    iss: 'https://api.yourapp.com',
    sub: userId,
    aud: 'https://yourapp.com',
    exp: Math.floor(Date.now() / 1000) + (15 * 60), // 15 minutes
    iat: Math.floor(Date.now() / 1000),
    email: user.email,
    roles: user.roles,
  };

  return jwt.sign(payload, process.env.JWT_PRIVATE_KEY!, {
    algorithm: 'RS256',
  });
}

// Verifying a JWT (Server-side)
function verifyAccessToken(token: string): JWTPayload {
  try {
    return jwt.verify(token, process.env.JWT_PUBLIC_KEY!, {
      algorithms: ['RS256'],
      issuer: 'https://api.yourapp.com',
      audience: 'https://yourapp.com',
    }) as JWTPayload;
  } catch (error) {
    throw new UnauthorizedError('Invalid token');
  }
}

JWT Best Practices


// ✅ Use short expiration times
const ACCESS_TOKEN_EXPIRY = 15 * 60; // 15 minutes
const REFRESH_TOKEN_EXPIRY = 7 * 24 * 60 * 60; // 7 days

// ✅ Implement token refresh
async function refreshAccessToken(refreshToken: string) {
  const payload = verifyRefreshToken(refreshToken);

  // Check if refresh token is revoked
  if (await isTokenRevoked(refreshToken)) {
    throw new UnauthorizedError('Token revoked');
  }

  return createAccessToken(payload.sub);
}

// ✅ Store tokens securely
// Server-side: Use httpOnly cookies
res.cookie('access_token', accessToken, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict',
  maxAge: 15 * 60 * 1000,
});

// Client-side: Never in localStorage, use memory or httpOnly cookies
class TokenManager {
  private accessToken: string | null = null;

  setToken(token: string) {
    this.accessToken = token; // Memory only, lost on refresh
  }

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

  clearToken() {
    this.accessToken = null;
  }
}

Zero Trust Architecture


Zero Trust operates on the principle: "Never trust, always verify."


Core Principles


  • **Verify explicitly**: Always authenticate and authorize
  • **Use least privilege access**: Minimal permissions
  • **Assume breach**: Design for compromise

  • Implementation


    // Middleware for Zero Trust
    export async function zeroTrustMiddleware(
      req: Request,
      res: Response,
      next: NextFunction
    ) {
      try {
        // 1. Verify identity
        const token = extractToken(req);
        const payload = await verifyToken(token);
    
        // 2. Verify device/context
        await verifyDeviceFingerprint(req);
        await verifyGeolocation(req, payload.sub);
    
        // 3. Check for suspicious activity
        await checkRateLimits(req, payload.sub);
        await detectAnomalies(req, payload.sub);
    
        // 4. Verify access level
        const hasAccess = await checkPermissions(
          payload.sub,
          req.method,
          req.path
        );
    
        if (!hasAccess) {
          throw new ForbiddenError('Insufficient permissions');
        }
    
        // 5. Log access attempt
        await auditLog({
          userId: payload.sub,
          action: `${req.method} ${req.path}`,
          ipAddress: req.ip,
          userAgent: req.get('user-agent'),
          timestamp: new Date(),
        });
    
        req.user = payload;
        next();
      } catch (error) {
        next(error);
      }
    }

    Role-Based Access Control (RBAC)


    // Define roles and permissions
    enum Role {
      ADMIN = 'admin',
      EDITOR = 'editor',
      VIEWER = 'viewer',
    }
    
    enum Permission {
      READ_POSTS = 'read:posts',
      CREATE_POSTS = 'create:posts',
      UPDATE_POSTS = 'update:posts',
      DELETE_POSTS = 'delete:posts',
      MANAGE_USERS = 'manage:users',
    }
    
    const rolePermissions: Record<Role, Permission[]> = {
      [Role.ADMIN]: [
        Permission.READ_POSTS,
        Permission.CREATE_POSTS,
        Permission.UPDATE_POSTS,
        Permission.DELETE_POSTS,
        Permission.MANAGE_USERS,
      ],
      [Role.EDITOR]: [
        Permission.READ_POSTS,
        Permission.CREATE_POSTS,
        Permission.UPDATE_POSTS,
      ],
      [Role.VIEWER]: [Permission.READ_POSTS],
    };
    
    // Check permissions
    function requirePermission(permission: Permission) {
      return (req: Request, res: Response, next: NextFunction) => {
        const userRoles = req.user.roles;
    
        const hasPermission = userRoles.some((role: Role) =>
          rolePermissions[role]?.includes(permission)
        );
    
        if (!hasPermission) {
          throw new ForbiddenError(`Requires permission: ${permission}`);
        }
    
        next();
      };
    }
    
    // Usage
    app.delete('/api/posts/:id',
      zeroTrustMiddleware,
      requirePermission(Permission.DELETE_POSTS),
      deletePost
    );

    Security Best Practices


    1. Secure Password Handling


    import bcrypt from 'bcrypt';
    
    // Hash password
    async function hashPassword(password: string): Promise<string> {
      const SALT_ROUNDS = 12;
      return bcrypt.hash(password, SALT_ROUNDS);
    }
    
    // Verify password
    async function verifyPassword(
      password: string,
      hash: string
    ): Promise<boolean> {
      return bcrypt.compare(password, hash);
    }
    
    // Password strength validation
    function validatePassword(password: string): boolean {
      const minLength = 12;
      const hasUpperCase = /[A-Z]/.test(password);
      const hasLowerCase = /[a-z]/.test(password);
      const hasNumbers = /\d/.test(password);
      const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password);
    
      return (
        password.length >= minLength &&
        hasUpperCase &&
        hasLowerCase &&
        hasNumbers &&
        hasSpecialChar
      );
    }

    2. CSRF Protection


    import { randomBytes } from 'crypto';
    
    // Generate CSRF token
    function generateCSRFToken(): string {
      return randomBytes(32).toString('hex');
    }
    
    // CSRF middleware
    function csrfProtection(req: Request, res: Response, next: NextFunction) {
      if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
        return next();
      }
    
      const token = req.headers['x-csrf-token'] || req.body._csrf;
      const sessionToken = req.session.csrfToken;
    
      if (!token || token !== sessionToken) {
        throw new ForbiddenError('Invalid CSRF token');
      }
    
      next();
    }
    
    // Set CSRF token
    app.use((req, res, next) => {
      if (!req.session.csrfToken) {
        req.session.csrfToken = generateCSRFToken();
      }
      res.locals.csrfToken = req.session.csrfToken;
      next();
    });

    3. Rate Limiting


    import rateLimit from 'express-rate-limit';
    
    // Basic rate limiting
    const apiLimiter = rateLimit({
      windowMs: 15 * 60 * 1000, // 15 minutes
      max: 100, // Limit each IP to 100 requests per window
      message: 'Too many requests, please try again later',
      standardHeaders: true,
      legacyHeaders: false,
    });
    
    // Stricter limits for authentication endpoints
    const authLimiter = rateLimit({
      windowMs: 15 * 60 * 1000,
      max: 5,
      skipSuccessfulRequests: true, // Don't count successful auth attempts
    });
    
    app.use('/api/', apiLimiter);
    app.use('/api/auth/', authLimiter);
    
    // Advanced: Per-user rate limiting
    import Redis from 'ioredis';
    const redis = new Redis();
    
    async function checkUserRateLimit(userId: string, limit: number): Promise<boolean> {
      const key = `rate_limit:${userId}`;
      const current = await redis.incr(key);
    
      if (current === 1) {
        await redis.expire(key, 3600); // 1 hour window
      }
    
      return current <= limit;
    }

    4. SQL Injection Prevention


    // ❌ NEVER do this
    function getUser(userId: string) {
      return db.query(`SELECT * FROM users WHERE id = ${userId}`);
    }
    
    // ✅ Always use parameterized queries
    function getUser(userId: string) {
      return db.query('SELECT * FROM users WHERE id = ?', [userId]);
    }
    
    // ✅ With Prisma ORM
    async function getUser(userId: string) {
      return prisma.user.findUnique({
        where: { id: userId }
      });
    }

    5. XSS Prevention


    import DOMPurify from 'isomorphic-dompurify';
    
    // Sanitize user input
    function sanitizeHTML(dirty: string): string {
      return DOMPurify.sanitize(dirty, {
        ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'a'],
        ALLOWED_ATTR: ['href', 'target', 'rel'],
      });
    }
    
    // Content Security Policy
    app.use((req, res, next) => {
      res.setHeader(
        'Content-Security-Policy',
        "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;"
      );
      next();
    });

    Multi-Factor Authentication (MFA)


    import speakeasy from 'speakeasy';
    import QRCode from 'qrcode';
    
    // Generate MFA secret
    async function setupMFA(userId: string) {
      const secret = speakeasy.generateSecret({
        name: `YourApp (${userId})`,
        length: 32,
      });
    
      // Store secret in database
      await db.user.update({
        where: { id: userId },
        data: { mfaSecret: secret.base32 }
      });
    
      // Generate QR code
      const qrCode = await QRCode.toDataURL(secret.otpauth_url!);
    
      return {
        secret: secret.base32,
        qrCode,
      };
    }
    
    // Verify MFA token
    async function verifyMFA(userId: string, token: string): Promise<boolean> {
      const user = await db.user.findUnique({
        where: { id: userId },
        select: { mfaSecret: true }
      });
    
      if (!user?.mfaSecret) {
        throw new Error('MFA not set up');
      }
    
      return speakeasy.totp.verify({
        secret: user.mfaSecret,
        encoding: 'base32',
        token,
        window: 1, // Allow 1 step before/after
      });
    }

    Conclusion


    Implementing robust authentication and authorization is crucial for modern web applications. Combine OAuth 2.0 for delegated authorization, JWTs for stateless authentication, and Zero Trust principles for comprehensive security.


    Key Takeaways:

  • Use OAuth 2.0 Authorization Code Flow with PKCE
  • Implement short-lived JWTs with refresh tokens
  • Apply Zero Trust: verify every request
  • Use RBAC for fine-grained access control
  • Implement MFA for sensitive operations
  • Always sanitize input and use parameterized queries

  • Resources


  • [OAuth 2.0 RFC](https://tools.ietf.org/html/rfc6749)
  • [JWT.io](https://jwt.io/)
  • [OWASP Top Ten](https://owasp.org/www-project-top-ten/)
  • [Zero Trust Architecture](https://www.nist.gov/publications/zero-trust-architecture)

  • Stay Updated

    Subscribe to Our Newsletter

    Get the latest articles, insights, and updates delivered directly to your inbox. Join our community of developers and tech enthusiasts.

    We respect your privacy. Unsubscribe at any time.