Standards

Authentication Security: JWT OAuth 2.0 Session Management and MFA

Comprehensive guide to securing authentication with JWT best practices OAuth 2.0 PKCE and multi-factor authentication

Authentication Security Fundamentals

Authentication security is the foundation of application security, answering the fundamental question: who is this user? Once answered, authorization controls what that user can do. A failure in authentication bypasses all downstream security controls—if an attacker can impersonate any user, they inherit that user's permissions.

Modern authentication systems combine multiple components: credential verification (passwords, API keys, certificates), session management (cookies, tokens, JWTs), and increasingly multi-factor authentication (TOTP, WebAuthn, SMS). Each component introduces distinct vulnerability classes:

  • Credential storage: Plaintext passwords, weak hashing (MD5, SHA-1), missing salts enable credential theft and offline cracking
  • Session management: Session fixation, hijacking, and missing cookie security attributes allow session theft
  • Token-based auth: JWT vulnerabilities (alg:none, weak secrets) and improper validation enable token forgery
  • OAuth flows: Missing PKCE, redirect URI validation failures, and state parameter omission enable authorization code interception

Classified under OWASP A07:2025 – Identification and Authentication Failures and CWE-287 (Improper Authentication), authentication vulnerabilities consistently rank among the most exploited weakness classes. The 2023 Verizon DBIR reported that 74% of breaches involved the human element, with stolen credentials being the leading attack vector.

JWT Vulnerabilities and Best Practices

JSON Web Tokens (JWTs) have become the dominant authentication mechanism for stateless APIs and single-page applications. A JWT contains a header (algorithm), payload (claims), and signature. The signature cryptographically binds the header and payload, preventing tampering—when properly implemented.

Critical JWT Vulnerabilities

1. Algorithm Confusion (alg: none)

The JWT header specifies the signing algorithm. Some libraries accept "alg": "none", which disables signature verification entirely. An attacker modifies the header to use "none" and removes the signature, and the token is accepted as valid.

Vulnerable Code (Node.js/JavaScript):

// VULNERABLE: Accepts any algorithm including "none"
const jwt = require('jsonwebtoken');
const token = req.headers.authorization.split(' ')[1];
const decoded = jwt.decode(token); // No verification!
req.user = decoded;

Secure Code (Node.js/JavaScript):

// SECURE: Enforces algorithm and validates signature
const jwt = require('jsonwebtoken');
const token = req.headers.authorization.split(' ')[1];
const decoded = jwt.verify(token, process.env.JWT_SECRET, {
  algorithms: ['HS256'], // Explicit algorithm allowlist
  issuer: 'https://api.example.com',
  audience: 'https://example.com'
});
req.user = decoded;

2. Weak Signing Secrets

JWTs signed with weak or hardcoded secrets can be brute-forced. Once the secret is recovered, attackers can mint arbitrary tokens with any claims.

Vulnerable Code (Python/Flask):

# VULNERABLE: Hardcoded weak secret
import jwt

token = jwt.encode(
    {'user_id': user.id, 'role': 'user'},
    'secret123',  # Weak secret that can be brute-forced
    algorithm='HS256'
)

Secure Code (Python/Flask):

# SECURE: Strong secret from environment, with expiration
import jwt
import os
from datetime import datetime, timedelta

token = jwt.encode({
    'user_id': user.id,
    'role': 'user',
    'exp': datetime.utcnow() + timedelta(hours=1),
    'iat': datetime.utcnow(),
    'iss': 'https://api.example.com'
}, os.environ['JWT_SECRET'], algorithm='HS256')

3. Missing Expiration Validation

JWTs without expiration (exp claim) or applications that do not validate expiration create tokens that never expire, extending the window for token theft.

Vulnerable Code (Java/Spring):

// VULNERABLE: No expiration check
String token = Jwts.builder()
    .setSubject(user.getEmail())
    .claim("role", user.getRole())
    .signWith(SignatureAlgorithm.HS256, "hardcoded-secret")
    .compact(); // Missing setExpiration()

Secure Code (Java/Spring):

// SECURE: Enforced expiration
String secret = System.getenv("JWT_SECRET");
Date now = new Date();
Date expiry = new Date(now.getTime() + 3600000); // 1 hour

String token = Jwts.builder()
    .setSubject(user.getEmail())
    .claim("role", user.getRole())
    .setIssuedAt(now)
    .setExpiration(expiry)
    .setIssuer("https://api.example.com")
    .signWith(SignatureAlgorithm.HS256, secret)
    .compact();

Vulnerable Code (Go):

// VULNERABLE: No expiration validation
token, _ := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
    return []byte("hardcoded-secret"), nil
})
// Missing expiration check

Secure Code (Go):

// SECURE: Validates expiration and algorithm
import (
    "github.com/golang-jwt/jwt/v5"
    "os"
    "time"
)

token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
    // Validate algorithm
    if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
        return nil, fmt.Errorf("unexpected signing method")
    }
    return []byte(os.Getenv("JWT_SECRET")), nil
})

if err != nil || !token.Valid {
    return errors.New("invalid token")
}

// Validate expiration
claims := token.Claims.(jwt.MapClaims)
if exp, ok := claims["exp"].(float64); ok {
    if time.Now().Unix() > int64(exp) {
        return errors.New("token expired")
    }
}

Vulnerable Code (TypeScript/Express):

// VULNERABLE: Accepts any algorithm and no expiration check
import jwt from 'jsonwebtoken';

const token = jwt.sign(
  { userId: user.id, role: user.role },
  'weak-secret' // Hardcoded weak secret
);
// No expiration set

Secure Code (TypeScript/Express):

// SECURE: Strong secret, explicit algorithm, expiration
import jwt from 'jsonwebtoken';

const token = jwt.sign(
  {
    userId: user.id,
    role: user.role,
    iss: 'https://api.example.com',
    aud: 'https://example.com'
  },
  process.env.JWT_SECRET!,
  {
    algorithm: 'HS256',
    expiresIn: '1h',
    issuer: 'https://api.example.com'
  }
);

JWT Best Practices

  • Use strong secrets (256+ bits of entropy) stored in environment variables
  • Set short expiration times (15 minutes for access tokens, longer for refresh tokens)
  • Validate exp, iat, iss, and aud claims on every verification
  • Use algorithm allowlists (algorithms: ['HS256']) to prevent algorithm confusion
  • Never store sensitive data (passwords, SSNs) in JWT payloads—they are base64-encoded, not encrypted
  • Consider asymmetric algorithms (RS256) for public verification scenarios

OAuth 2.0 Security Flows

OAuth 2.0 is an authorization framework that enables third-party applications to obtain limited access to a user's resources without exposing credentials. While OAuth 2.0 solves credential exposure, it introduces new attack vectors when implemented incorrectly.

Authorization Code Flow with PKCE

The Authorization Code Flow is the most secure OAuth flow for public clients (mobile apps, SPAs). PKCE (Proof Key for Code Exchange) prevents authorization code interception attacks.

Vulnerable Flow (Without PKCE - Python):

# VULNERABLE: No PKCE, authorization code can be intercepted
import requests

# Step 1: Redirect user to authorization URL
auth_url = f"https://auth.example.com/authorize?client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&response_type=code"

# Step 2: Exchange code for token (vulnerable to interception)
def callback(code):
    response = requests.post('https://auth.example.com/token', data={
        'grant_type': 'authorization_code',
        'code': code,
        'client_id': CLIENT_ID,
        'client_secret': CLIENT_SECRET,  # Should not be in public clients!
        'redirect_uri': REDIRECT_URI
    })
    return response.json()['access_token']

Secure Flow (With PKCE - Python):

# SECURE: PKCE protects against code interception
import hashlib
import base64
import secrets
import requests

# Generate PKCE verifier and challenge
code_verifier = base64.urlsafe_b64encode(secrets.token_bytes(32)).decode('utf-8').rstrip('=')
code_challenge = base64.urlsafe_b64encode(
    hashlib.sha256(code_verifier.encode('utf-8')).digest()
).decode('utf-8').rstrip('=')

# Step 1: Authorization request with code_challenge
auth_url = (
    f"https://auth.example.com/authorize?"
    f"client_id={CLIENT_ID}&"
    f"redirect_uri={REDIRECT_URI}&"
    f"response_type=code&"
    f"code_challenge={code_challenge}&"
    f"code_challenge_method=S256"
)

# Step 2: Token exchange with code_verifier
def callback(code):
    response = requests.post('https://auth.example.com/token', data={
        'grant_type': 'authorization_code',
        'code': code,
        'client_id': CLIENT_ID,
        'redirect_uri': REDIRECT_URI,
        'code_verifier': code_verifier  # Proves possession
    })
    return response.json()['access_token']

Critical OAuth Security Considerations

1. State Parameter for CSRF Protection

Vulnerable (JavaScript):

// VULNERABLE: No state parameter - CSRF attack possible
window.location.href = `https://auth.example.com/authorize?client_id=${clientId}&redirect_uri=${redirectUri}`;

Secure (JavaScript):

// SECURE: State parameter prevents CSRF
const state = crypto.randomUUID();
sessionStorage.setItem('oauth_state', state);

window.location.href = `https://auth.example.com/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&state=${state}`;

// In callback:
const returnedState = new URLSearchParams(window.location.search).get('state');
if (returnedState !== sessionStorage.getItem('oauth_state')) {
  throw new Error('CSRF detected: state mismatch');
}

2. Redirect URI Validation

Open redirects in OAuth callbacks enable authorization code theft.

Vulnerable (Java/Spring):

// VULNERABLE: No redirect URI validation
@GetMapping("/oauth/callback")
public String callback(@RequestParam String code, @RequestParam String redirect_uri) {
    // Attacker can set redirect_uri to their domain
    return "redirect:" + redirect_uri + "?code=" + code;
}

Secure (Java/Spring):

// SECURE: Strict redirect URI allowlist
private static final Set ALLOWED_REDIRECTS = Set.of(
    "https://app.example.com/callback",
    "https://mobile.example.com/callback"
);

@GetMapping("/oauth/callback")
public String callback(@RequestParam String code, @RequestParam String redirect_uri) {
    if (!ALLOWED_REDIRECTS.contains(redirect_uri)) {
        throw new SecurityException("Invalid redirect URI");
    }
    return "redirect:" + redirect_uri + "?code=" + code;
}

OAuth 2.0 Best Practices

  • Always use PKCE for public clients (mobile apps, SPAs)
  • Validate redirect URIs against an exact match allowlist
  • Use the state parameter to prevent CSRF attacks
  • Set short authorization code lifetimes (1-10 minutes)
  • Use refresh tokens with rotation for long-lived access
  • Never expose client secrets in public clients (mobile, JavaScript)

Session Management Security

Session management handles user state across HTTP requests. Poor session handling enables session fixation, hijacking, and CSRF attacks.

Session Fixation

Session fixation occurs when an application accepts a session ID set by an attacker. The attacker creates a session ID, tricks the victim into using it, then hijacks the session after the victim authenticates.

Vulnerable (Node.js/Express):

// VULNERABLE: Session ID not regenerated after login
app.post('/login', (req, res) => {
  const user = authenticate(req.body.username, req.body.password);
  if (user) {
    req.session.userId = user.id; // Same session ID before/after login!
    res.send('Logged in');
  }
});

Secure (Node.js/Express):

// SECURE: Regenerate session ID after authentication
app.post('/login', (req, res) => {
  const user = authenticate(req.body.username, req.body.password);
  if (user) {
    req.session.regenerate((err) => {
      if (err) return res.status(500).send('Session error');
      req.session.userId = user.id;
      res.send('Logged in');
    });
  }
});

Secure Cookie Configuration

Session cookies must be configured with security attributes to prevent theft and CSRF.

Vulnerable (Python/Flask):

# VULNERABLE: Missing security attributes
from flask import Flask, session

app = Flask(__name__)
app.secret_key = 'hardcoded-secret'  # Also a problem!

@app.route('/login', methods=['POST'])
def login():
    session['user_id'] = user.id  # Cookie has no security flags
    return 'Logged in'

Secure (Python/Flask):

# SECURE: All security attributes configured
from flask import Flask, session
import os

app = Flask(__name__)
app.secret_key = os.environ['SESSION_SECRET']
app.config.update(
    SESSION_COOKIE_SECURE=True,      # HTTPS only
    SESSION_COOKIE_HTTPONLY=True,    # Not accessible via JavaScript
    SESSION_COOKIE_SAMESITE='Lax',   # CSRF protection
    PERMANENT_SESSION_LIFETIME=3600  # 1 hour timeout
)

@app.route('/login', methods=['POST'])
def login():
    session.permanent = True
    session['user_id'] = user.id
    return 'Logged in'

Vulnerable (Go):

// VULNERABLE: No secure cookie flags
http.SetCookie(w, &http.Cookie{
    Name:  "session",
    Value: sessionID,
    Path:  "/",
})

Secure (Go):

// SECURE: All security flags set
http.SetCookie(w, &http.Cookie{
    Name:     "session",
    Value:    sessionID,
    Path:     "/",
    HttpOnly: true,           // Prevents XSS access
    Secure:   true,           // HTTPS only
    SameSite: http.SameSiteLaxMode, // CSRF protection
    MaxAge:   3600,           // 1 hour
})

Session Hijacking Prevention

Vulnerable (TypeScript):

// VULNERABLE: Session ID in URL (easily stolen from logs/referrer)
app.get('/dashboard', (req, res) => {
  const sessionId = req.query.sid; // Never put session IDs in URLs!
  const session = sessions.get(sessionId);
  res.render('dashboard', { user: session.user });
});

Secure (TypeScript):

// SECURE: Session ID in HttpOnly cookie with validation
import session from 'express-session';

app.use(session({
  secret: process.env.SESSION_SECRET!,
  name: 'sid',
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: true,
    httpOnly: true,
    sameSite: 'strict',
    maxAge: 3600000 // 1 hour
  }
}));

app.get('/dashboard', (req, res) => {
  if (!req.session.userId) {
    return res.redirect('/login');
  }
  res.render('dashboard', { user: req.session.user });
});

Session Management Best Practices

  • Regenerate session IDs after authentication and privilege changes
  • Set HttpOnly to prevent XSS-based session theft
  • Set Secure to enforce HTTPS-only transmission
  • Set SameSite=Lax or Strict to prevent CSRF
  • Implement absolute session timeouts (e.g., 8 hours max)
  • Implement idle timeouts (e.g., 30 minutes of inactivity)
  • Never expose session IDs in URLs or logs

Multi-Factor Authentication Implementation

Multi-factor authentication (MFA) requires users to provide two or more verification factors: something they know (password), something they have (phone, hardware token), or something they are (biometric). MFA dramatically reduces account takeover risk.

TOTP (Time-based One-Time Password)

TOTP generates 6-digit codes that change every 30 seconds, used by Google Authenticator and Authy.

Implementation (Node.js):

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

// Generate secret for user
const secret = speakeasy.generateSecret({
  name: 'YourApp',
  issuer: 'YourCompany',
  length: 32
});

// Generate QR code for user to scan
const qrCodeUrl = await QRCode.toDataURL(secret.otpauth_url);

// Store secret.base32 in database
await db.users.update(userId, { mfaSecret: secret.base32 });

// Verification
function verifyTOTP(userId: string, token: string): boolean {
  const user = db.users.find(userId);
  return speakeasy.totp.verify({
    secret: user.mfaSecret,
    encoding: 'base32',
    token: token,
    window: 2 // Allow 1 period before/after for clock skew
  });
}

WebAuthn (Hardware Keys and Biometrics)

WebAuthn enables passwordless authentication using hardware security keys (YubiKey) or platform authenticators (Touch ID, Windows Hello).

Implementation (Python/Flask):

from webauthn import generate_registration_options, verify_registration_response
from webauthn.helpers import bytes_to_base64url
import secrets

# Registration - Generate challenge
@app.route('/webauthn/register/options', methods=['POST'])
def webauthn_register_options():
    user = get_current_user()
    challenge = secrets.token_bytes(32)

    # Store challenge in session
    session['webauthn_challenge'] = bytes_to_base64url(challenge)

    options = generate_registration_options(
        rp_id='example.com',
        rp_name='Your App',
        user_id=str(user.id).encode(),
        user_name=user.email,
        user_display_name=user.full_name,
        challenge=challenge,
        authenticator_selection={'userVerification': 'required'}
    )
    return jsonify(options)

# Verification
@app.route('/webauthn/register/verify', methods=['POST'])
def webauthn_register_verify():
    credential = request.json
    stored_challenge = session.get('webauthn_challenge')

    verification = verify_registration_response(
        credential=credential,
        expected_challenge=stored_challenge,
        expected_origin='https://example.com',
        expected_rp_id='example.com'
    )

    # Store credential in database
    db.credentials.create({
        'user_id': current_user.id,
        'credential_id': verification.credential_id,
        'public_key': verification.credential_public_key
    })

    return jsonify({'verified': True})

Backup Codes

Always provide backup codes in case users lose access to their MFA device.

Implementation (Go):

import (
    "crypto/rand"
    "encoding/hex"
    "golang.org/x/crypto/bcrypt"
)

// Generate backup codes
func GenerateBackupCodes(count int) ([]string, error) {
    codes := make([]string, count)
    for i := 0; i < count; i++ {
        bytes := make([]byte, 4)
        if _, err := rand.Read(bytes); err != nil {
            return nil, err
        }
        codes[i] = hex.EncodeToString(bytes)
    }
    return codes, nil
}

// Hash and store backup codes
func StoreBackupCodes(userID string, codes []string) error {
    for _, code := range codes {
        hashed, err := bcrypt.GenerateFromPassword([]byte(code), 12)
        if err != nil {
            return err
        }
        db.BackupCodes.Create(userID, string(hashed))
    }
    return nil
}

// Verify backup code (one-time use)
func VerifyBackupCode(userID string, code string) (bool, error) {
    storedCodes := db.BackupCodes.FindByUser(userID)

    for _, storedCode := range storedCodes {
        if bcrypt.CompareHashAndPassword([]byte(storedCode.Hash), []byte(code)) == nil {
            // Mark as used (delete or flag)
            db.BackupCodes.Delete(storedCode.ID)
            return true, nil
        }
    }
    return false, nil
}

MFA Best Practices

  • Support multiple MFA methods (TOTP, WebAuthn, SMS as last resort)
  • Never use SMS as the only MFA option (SIM swapping attacks)
  • Provide backup codes and store them hashed
  • Enforce MFA for privileged accounts (admin, financial operations)
  • Implement MFA recovery flows with identity verification
  • Allow users to register multiple authenticators (primary + backup hardware key)

How CodeSlick Detects Authentication Issues

CodeSlick identifies authentication and session management vulnerabilities across JavaScript, TypeScript, Python, Java, and Go:

  • Hardcoded secrets: 38 detection patterns for JWT secrets, API keys, passwords, and tokens embedded in source code (CWE-798)
  • Weak cryptography: MD5, SHA-1, DES usage for password hashing or token generation (CWE-327)
  • JWT vulnerabilities: Missing signature verification, weak algorithms, hardcoded secrets, missing expiration validation
  • Insecure session cookies: Missing HttpOnly, Secure, and SameSite attributes
  • Missing session regeneration: Session IDs not regenerated after authentication
  • OAuth misconfigurations: Missing state parameters, open redirect vulnerabilities in redirect URIs

All findings include CWE classification, CVSS scoring (3.1-9.8 range), and OWASP A07:2025 mapping. CodeSlick's AI-powered fix engine generates framework-specific secure implementations for Node.js, Flask, Django, Spring Boot, and Go.

Run CodeSlick on every pull request via the GitHub App, on every commit via the CLI pre-commit hook, or on-demand at codeslick.dev/analyze.

Scan your authentication code for JWT vulnerabilities, weak session management, and hardcoded secrets in under 3 seconds.

Frequently Asked Questions

Related Guides