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, andaudclaims 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
stateparameter 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
HttpOnlyto prevent XSS-based session theft - Set
Secureto enforce HTTPS-only transmission - Set
SameSite=LaxorStrictto 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, andSameSiteattributes - 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.