40 KiB
40 KiB
API Authentication
Overview
API authentication specialist covering token patterns, OAuth2 flows, security hardening, compliance, monitoring, and production operations.
Core principle: Authentication proves identity; authorization controls access - implement defense-in-depth with short-lived tokens, secure storage, rotation, monitoring, and assume breach to minimize blast radius.
When to Use This Skill
Use when encountering:
- Authentication strategy: JWT vs sessions vs OAuth2 vs API keys
- OAuth2 flows: Authorization Code, PKCE, Client Credentials, token exchange
- Token security: Storage, rotation, revocation, theft detection
- Service-to-service: mTLS, service mesh, zero-trust
- Mobile auth: Secure storage, biometrics, certificate pinning
- Security hardening: Rate limiting, abuse prevention, anomaly detection
- Monitoring: Auth metrics, distributed tracing, audit logs
- Compliance: GDPR, PCI-DSS, SOC 2, audit trails
- Multi-tenancy: Tenant isolation, per-tenant policies
- Testing: Mock auth, development workflows
Do NOT use for:
- Application-specific business logic → Use domain skills
- Infrastructure security (firewalls, IDS) →
ordis-security-architect - Frontend auth UI →
lyra-ux-designer
Quick Reference - Authentication Patterns
| Pattern | Use Case | Security | Complexity | Revocation |
|---|---|---|---|---|
| JWT | Mobile apps, APIs | Medium | Low | Hard (requires blacklist) |
| Sessions | Web apps, admin panels | High | Medium | Easy (delete session) |
| OAuth2 | Third-party access, SSO | High | High | Medium (refresh rotation) |
| API Keys | Service-to-service, webhooks | Medium | Low | Easy (rotate keys) |
| mTLS | Service mesh, zero-trust | Very High | High | Medium (cert revocation) |
JWT vs Sessions Decision Matrix
| Factor | JWT | Server-Side Sessions | Winner |
|---|---|---|---|
| Mobile apps | Excellent (stateless) | Poor (sticky sessions needed) | JWT |
| Horizontal scaling | Excellent (no shared state) | Requires sticky sessions or Redis | JWT |
| Revocation | Poor (need blacklist or short TTL) | Excellent (delete session) | Sessions |
| Payload size | Large (sent every request) | Small (session ID only) | Sessions |
| Server memory | None (stateless) | High (session store) | JWT |
| XSS vulnerability | High (if stored in localStorage) | Low (httpOnly cookies) | Sessions |
| CSRF vulnerability | None (bearer token) | High (requires CSRF tokens) | JWT |
Production Recommendation: Hybrid Approach
Architecture:
- Short-lived JWTs (15 min) for API access
- Long-lived refresh tokens stored server-side (session-like)
- Refresh endpoint returns new JWT + rotates refresh token
Benefits:
- Stateless API access (JWT)
- Secure revocation (server-side refresh tokens)
- Mobile-friendly (no cookies required)
- Horizontal scaling (minimal session state)
OAuth2 Grant Types
Grant Type Selection Matrix
| Client Type | Grant Type | Security | Use Case |
|---|---|---|---|
| Web app (server-side) | Authorization Code + PKCE | High | User login with backend |
| SPA | Authorization Code + PKCE | Medium-High | React/Vue/Angular apps |
| Mobile app | Authorization Code + PKCE | High | iOS/Android apps |
| Service-to-service | Client Credentials | High | Background jobs, APIs |
| Device | Device Authorization Grant | Medium | Smart TV, IoT devices |
| Legacy | DEPRECATED | Don't use |
Authorization Code + PKCE (RFC 7636)
Why PKCE? Prevents authorization code interception attacks
// Step 1: Generate PKCE challenge
const codeVerifier = crypto.randomBytes(32).toString('base64url');
const codeChallenge = crypto
.createHash('sha256')
.update(codeVerifier)
.digest('base64url');
// Step 2: Redirect to authorization endpoint
const authUrl = new URL('https://auth.example.com/authorize');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', 'your_client_id');
authUrl.searchParams.set('redirect_uri', 'https://yourapp.com/callback');
authUrl.searchParams.set('scope', 'read write offline_access');
authUrl.searchParams.set('code_challenge', codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
authUrl.searchParams.set('state', generateStateToken()); // CSRF protection
// Step 3: Exchange code for token
const tokenResponse = await fetch('https://auth.example.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: receivedCode,
redirect_uri: 'https://yourapp.com/callback',
client_id: 'your_client_id',
code_verifier: codeVerifier // Proves you initiated the flow
})
});
// Response
{
"access_token": "eyJhbGc...",
"token_type": "Bearer",
"expires_in": 900,
"refresh_token": "zxcvbnm...",
"scope": "read write offline_access"
}
Client Credentials (Service-to-Service)
const tokenResponse = await fetch('https://auth.example.com/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Basic ${base64(client_id + ':' + client_secret)}`
},
body: new URLSearchParams({
grant_type: 'client_credentials',
scope: 'api.read api.write',
audience: 'https://api.example.com'
})
});
Token Storage Security
Storage Security Matrix
| Storage Location | XSS Risk | CSRF Risk | Accessible to JS | Production Use |
|---|---|---|---|---|
| localStorage | ❌ HIGH | ✅ None | Yes | NEVER for tokens |
| sessionStorage | ❌ HIGH | ✅ None | Yes | NEVER for tokens |
| Memory only | ✅ None | ✅ None | Yes (in-app) | ✅ Access tokens (SPA) |
| httpOnly cookie | ✅ None | ❌ HIGH | No | ✅ Refresh tokens (+SameSite) |
| Secure + httpOnly + SameSite=Strict | ✅ None | ✅ Low | No | ✅ BEST for web |
| iOS Keychain | ✅ None | ✅ N/A | No (secure enclave) | ✅ Mobile apps |
| Android Keystore | ✅ None | ✅ N/A | No (hardware-backed) | ✅ Mobile apps |
Web App Pattern (BFF - Backend For Frontend)
// Frontend - access token in memory only
class AuthService {
#accessToken = null; // Private field, lost on refresh
async callAPI(endpoint) {
if (!this.#accessToken || this.isExpired(this.#accessToken)) {
this.#accessToken = await this.refreshAccessToken();
}
return fetch(endpoint, {
headers: { 'Authorization': `Bearer ${this.#accessToken}` }
});
}
async refreshAccessToken() {
// Calls BFF, which reads httpOnly cookie
const response = await fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'include' // Send httpOnly cookie
});
const { access_token } = await response.json();
return access_token;
}
}
// Backend (BFF) - refresh endpoint
app.post('/api/auth/refresh', async (req, res) => {
const refreshToken = req.cookies.refresh_token; // httpOnly cookie
// Validate and rotate refresh token
const newTokens = await rotateRefreshToken(refreshToken);
// Set new httpOnly cookie
res.cookie('refresh_token', newTokens.refresh_token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
});
res.json({ access_token: newTokens.access_token, expires_in: 900 });
});
Mobile App Pattern
// iOS - Keychain storage
import Security
class TokenStorage {
func saveToken(_ token: String, forKey key: String) {
let data = token.data(using: .utf8)!
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
]
SecItemDelete(query as CFDictionary) // Delete old
SecItemAdd(query as CFDictionary, nil) // Add new
}
func getToken(forKey key: String) -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecReturnData as String: true
]
var result: AnyObject?
SecItemCopyMatching(query as CFDictionary, &result)
guard let data = result as? Data else { return nil }
return String(data: data, encoding: .utf8)
}
}
// Android - EncryptedSharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
class TokenStorage(context: Context) {
private val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
private val prefs = EncryptedSharedPreferences.create(
context,
"secure_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
fun saveToken(key: String, token: String) {
prefs.edit().putString(key, token).apply()
}
fun getToken(key: String): String? {
return prefs.getString(key, null)
}
}
Refresh Token Rotation
Pattern: Token Families with Replay Detection
// Database schema
CREATE TABLE refresh_tokens (
token_hash VARCHAR(64) PRIMARY KEY,
user_id UUID NOT NULL,
family_id UUID NOT NULL,
parent_token_hash VARCHAR(64),
device_id VARCHAR(255),
ip_address INET,
user_agent TEXT,
created_at TIMESTAMP NOT NULL,
expires_at TIMESTAMP NOT NULL,
revoked BOOLEAN DEFAULT false,
revoked_at TIMESTAMP,
revoked_reason TEXT,
INDEX idx_family (family_id),
INDEX idx_user (user_id),
INDEX idx_expires (expires_at)
);
// Refresh endpoint with rotation
async function refreshTokens(refreshToken, clientInfo) {
const tokenHash = sha256(refreshToken);
const dbToken = await db.query(
'SELECT * FROM refresh_tokens WHERE token_hash = $1',
[tokenHash]
);
// Case 1: Token not found or already revoked
if (!dbToken || dbToken.revoked) {
// Check if this token existed in history
const historical = await db.query(
'SELECT family_id FROM refresh_tokens WHERE token_hash = $1',
[tokenHash]
);
if (historical.length > 0) {
// REPLAY ATTACK DETECTED!
// Revoke entire token family
await db.query(
'UPDATE refresh_tokens SET revoked = true, revoked_at = NOW(), ' +
'revoked_reason = $1 WHERE family_id = $2',
['Replay attack detected', historical[0].family_id]
);
await auditLog.critical({
event: 'token_replay_attack',
user_id: historical[0].user_id,
family_id: historical[0].family_id,
ip: clientInfo.ip
});
throw new SecurityError('Token reuse detected - all sessions revoked');
}
throw new AuthError('Invalid refresh token');
}
// Case 2: Token expired
if (dbToken.expires_at < new Date()) {
throw new AuthError('Refresh token expired');
}
// Case 3: Valid token - rotate it
const newRefreshToken = crypto.randomBytes(32).toString('base64url');
const newAccessToken = generateJWT({
sub: dbToken.user_id,
scopes: ['read', 'write'],
exp: Math.floor(Date.now() / 1000) + 900 // 15 min
});
// Revoke current token
await db.query(
'UPDATE refresh_tokens SET revoked = true WHERE token_hash = $1',
[tokenHash]
);
// Create new token in same family
await db.query(
'INSERT INTO refresh_tokens ' +
'(token_hash, user_id, family_id, parent_token_hash, device_id, ' +
'ip_address, user_agent, created_at, expires_at) ' +
'VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW() + INTERVAL \'7 days\')',
[
sha256(newRefreshToken),
dbToken.user_id,
dbToken.family_id, // Same family
tokenHash, // Track lineage
clientInfo.device_id,
clientInfo.ip,
clientInfo.user_agent
]
);
return {
access_token: newAccessToken,
refresh_token: newRefreshToken,
expires_in: 900,
token_type: 'Bearer'
};
}
Advanced Refresh Patterns
Absolute expiry (max lifetime regardless of rotation):
// Add max_family_age to family tracking
CREATE TABLE token_families (
family_id UUID PRIMARY KEY,
user_id UUID NOT NULL,
created_at TIMESTAMP NOT NULL,
max_lifetime_hours INT DEFAULT 720, // 30 days max
INDEX idx_user (user_id)
);
// Check absolute expiry
const familyAge = Date.now() - family.created_at;
const maxAge = family.max_lifetime_hours * 60 * 60 * 1000;
if (familyAge > maxAge) {
throw new AuthError('Session expired - please re-authenticate');
}
Grace period for concurrent requests:
// Allow small window for race conditions
const ROTATION_GRACE_PERIOD_MS = 5000; // 5 seconds
if (dbToken.revoked && dbToken.revoked_at) {
const timeSinceRevocation = Date.now() - dbToken.revoked_at;
if (timeSinceRevocation < ROTATION_GRACE_PERIOD_MS) {
// Within grace period - might be concurrent refresh
// Return cached new tokens instead of replay alert
const newTokens = await getChildToken(tokenHash);
if (newTokens) return newTokens;
}
// Outside grace period - likely replay attack
await revokeTokenFamily(dbToken.family_id);
}
Rate Limiting & Abuse Prevention
Authentication Endpoint Rate Limits
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
// Login endpoint - strict limits
const loginLimiter = rateLimit({
store: new RedisStore({ client: redisClient }),
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // Max 5 attempts
message: 'Too many login attempts, please try again later',
keyGenerator: (req) => {
// Rate limit by IP + username combination
return `login:${req.ip}:${req.body.username}`;
},
handler: (req, res) => {
auditLog.warning({
event: 'rate_limit_exceeded',
endpoint: '/auth/login',
ip: req.ip,
username: req.body.username
});
res.status(429).json({
error: 'rate_limit_exceeded',
retry_after: res.getHeader('Retry-After')
});
}
});
app.post('/auth/login', loginLimiter, async (req, res) => {
// Login logic
});
// Refresh endpoint - moderate limits
const refreshLimiter = rateLimit({
store: new RedisStore({ client: redisClient }),
windowMs: 60 * 1000, // 1 minute
max: 10, // 10 refreshes per minute
keyGenerator: (req) => `refresh:${req.ip}`
});
app.post('/auth/refresh', refreshLimiter, async (req, res) => {
// Refresh logic
});
Account Lockout After Failed Attempts
async function attemptLogin(username, password, clientInfo) {
const lockoutKey = `lockout:${username}`;
const attemptsKey = `attempts:${username}`;
// Check if account is locked
const lockedUntil = await redis.get(lockoutKey);
if (lockedUntil && Date.now() < parseInt(lockedUntil)) {
throw new AuthError('Account temporarily locked due to failed login attempts');
}
// Verify credentials
const user = await db.findUser(username);
const valid = await bcrypt.compare(password, user.password_hash);
if (!valid) {
// Increment failed attempts
const attempts = await redis.incr(attemptsKey);
await redis.expire(attemptsKey, 15 * 60); // 15 min window
if (attempts >= 5) {
// Lock account for 30 minutes
const lockUntil = Date.now() + 30 * 60 * 1000;
await redis.set(lockoutKey, lockUntil.toString(), 'EX', 30 * 60);
await auditLog.warning({
event: 'account_locked',
user_id: user.id,
attempts,
ip: clientInfo.ip
});
throw new AuthError('Account locked due to too many failed attempts');
}
throw new AuthError('Invalid credentials');
}
// Success - clear attempts
await redis.del(attemptsKey);
// Check for anomalies
await detectAnomalies(user.id, clientInfo);
return generateTokens(user);
}
Anomaly Detection
async function detectAnomalies(userId, clientInfo) {
// Get user's login history
const recentLogins = await db.query(
'SELECT ip_address, country, city FROM login_history ' +
'WHERE user_id = $1 AND created_at > NOW() - INTERVAL \'30 days\' ' +
'ORDER BY created_at DESC LIMIT 100',
[userId]
);
// Check for new location
const knownLocations = new Set(recentLogins.map(l => `${l.country}:${l.city}`));
const currentLocation = `${clientInfo.country}:${clientInfo.city}`;
if (!knownLocations.has(currentLocation)) {
// New location - require additional verification
await sendSecurityAlert(userId, {
type: 'new_location',
location: currentLocation,
ip: clientInfo.ip
});
// Could require:
// - Email verification
// - 2FA challenge
// - Security question
// - Temporary session with limited access
}
// Check for impossible travel
if (recentLogins.length > 0) {
const lastLogin = recentLogins[0];
const timeDiff = Date.now() - lastLogin.created_at;
const distance = calculateDistance(
lastLogin.country,
clientInfo.country
);
// If 500+ km traveled in < 1 hour, flag as suspicious
if (distance > 500 && timeDiff < 60 * 60 * 1000) {
await auditLog.warning({
event: 'impossible_travel',
user_id: userId,
from: lastLogin.country,
to: clientInfo.country,
time_diff_minutes: timeDiff / 60000
});
// Require step-up authentication
return { require_2fa: true };
}
}
}
Monitoring & Observability
Key Metrics to Track
| Metric | Alert Threshold | Why It Matters |
|---|---|---|
| Login success rate | < 80% | Credentials issues, attacks |
| Token refresh failures | > 5% | Rotation bugs, clock skew |
| Rate limit hits | > 100/hour | Brute force attempts |
| Account lockouts | > 10/hour | Credential stuffing attack |
| Token replay attempts | > 0 | Security breach |
| Failed 2FA attempts | > 3/user/day | Account compromise |
| New device logins | Monitor trends | Unusual activity |
| p99 auth latency | > 500ms | Performance degradation |
Distributed Tracing for Auth Flows
const { trace, context } = require('@opentelemetry/api');
const tracer = trace.getTracer('auth-service');
async function handleLogin(req, res) {
return tracer.startActiveSpan('auth.login', async (span) => {
span.setAttribute('user.username', req.body.username);
span.setAttribute('client.ip', req.ip);
span.setAttribute('client.user_agent', req.headers['user-agent']);
try {
// Nested span for credential validation
const user = await tracer.startActiveSpan('auth.validate_credentials', async (validateSpan) => {
const result = await validateCredentials(req.body.username, req.body.password);
validateSpan.setAttribute('validation.success', !!result);
validateSpan.end();
return result;
});
if (!user) {
span.setAttribute('auth.result', 'invalid_credentials');
throw new AuthError('Invalid credentials');
}
// Nested span for token generation
const tokens = await tracer.startActiveSpan('auth.generate_tokens', async (tokenSpan) => {
const result = await generateTokens(user);
tokenSpan.setAttribute('tokens.access_expiry', result.expires_in);
tokenSpan.end();
return result;
});
span.setAttribute('auth.result', 'success');
span.setAttribute('user.id', user.id);
res.json(tokens);
} catch (error) {
span.recordException(error);
span.setAttribute('auth.result', 'error');
throw error;
} finally {
span.end();
}
});
}
// Trace shows:
// auth.login (500ms)
// ├── auth.validate_credentials (300ms) // DB query
// ├── auth.generate_tokens (50ms) // JWT signing
// └── auth.audit_log (150ms) // Logging
// Can identify bottlenecks:
// - Slow password hashing (increase bcrypt rounds?)
// - Slow DB queries (add indexes?)
// - Network latency to Redis
Audit Logging
class AuditLogger {
async log(event) {
const entry = {
timestamp: new Date().toISOString(),
event_type: event.type,
user_id: event.user_id,
ip_address: event.ip,
user_agent: event.user_agent,
resource: event.resource,
action: event.action,
result: event.result,
metadata: event.metadata,
trace_id: context.active().getValue('trace_id')
};
// Write to multiple destinations
await Promise.all([
// 1. Append-only audit table (compliance)
db.query('INSERT INTO audit_log (...) VALUES (...)', entry),
// 2. Time-series database (analytics)
influxdb.write('auth_events', entry),
// 3. SIEM (security monitoring)
siem.send(entry),
// 4. Compliance log (immutable, encrypted)
complianceLog.append(encrypt(entry))
]);
}
async critical(event) {
await this.log({ ...event, severity: 'critical' });
// Alert on critical events
await alerting.send({
title: `Critical Auth Event: ${event.event_type}`,
details: event,
severity: 'critical'
});
}
}
// Usage
await auditLog.log({
type: 'login_success',
user_id: user.id,
ip: req.ip,
user_agent: req.headers['user-agent'],
result: 'success'
});
await auditLog.critical({
type: 'token_replay_attack',
user_id: user.id,
family_id: token.family_id,
ip: req.ip
});
Multi-Tenancy Patterns
Tenant Isolation in Tokens
// JWT with tenant claim
const accessToken = jwt.sign({
sub: user.id,
tenant_id: user.tenant_id, // Tenant isolation
tenant_tier: tenant.tier, // For rate limiting
roles: user.roles, // ['admin', 'user']
scopes: ['read:orders', 'write:orders'],
iss: 'https://auth.example.com',
aud: 'https://api.example.com',
exp: Math.floor(Date.now() / 1000) + 900
}, privateKey, { algorithm: 'RS256' });
// Middleware to enforce tenant isolation
function tenantIsolation(req, res, next) {
const token = verifyJWT(req.headers.authorization);
// Extract tenant from token
req.tenant_id = token.tenant_id;
// Add tenant filter to all DB queries
req.dbFilter = { tenant_id: req.tenant_id };
next();
}
// All queries automatically filtered
app.get('/orders', tenantIsolation, async (req, res) => {
// Automatically filtered by tenant
const orders = await db.query(
'SELECT * FROM orders WHERE tenant_id = $1',
[req.tenant_id]
);
res.json(orders);
});
Per-Tenant Rate Limits
const getTenantRateLimit = (tier) => {
const limits = {
free: { windowMs: 60000, max: 100 }, // 100/min
pro: { windowMs: 60000, max: 1000 }, // 1000/min
enterprise: { windowMs: 60000, max: 10000 } // 10k/min
};
return limits[tier] || limits.free;
};
app.use(async (req, res, next) => {
const token = verifyJWT(req.headers.authorization);
const tenant = await getTenant(token.tenant_id);
const limit = getTenantRateLimit(tenant.tier);
// Apply tenant-specific rate limit
const limiter = rateLimit({
...limit,
keyGenerator: () => `api:${tenant.id}`
});
limiter(req, res, next);
});
Service-to-Service Authentication
Zero-Trust Architecture
Principles:
1. Never trust, always verify
2. Assume breach
3. Verify explicitly (identity + device + location)
4. Least privilege access
5. Micro-segmentation
Mutual TLS (mTLS) Pattern
# Kubernetes with cert-manager
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: service-a-cert
spec:
secretName: service-a-tls
issuerRef:
name: internal-ca
kind: ClusterIssuer
dnsNames:
- service-a.default.svc.cluster.local
usages:
- digital signature
- key encipherment
- client auth # Client authentication
- server auth # Server authentication
# Service configuration
apiVersion: v1
kind: Service
metadata:
name: service-b
annotations:
service.alpha.kubernetes.io/app-protocols: '{"https":"HTTPS"}'
spec:
ports:
- port: 443
protocol: TCP
targetPort: 8443
# Pod configuration
apiVersion: apps/v1
kind: Deployment
metadata:
name: service-a
spec:
template:
spec:
containers:
- name: app
volumeMounts:
- name: tls
mountPath: /etc/tls
readOnly: true
volumes:
- name: tls
secret:
secretName: service-a-tls
// Node.js client with mTLS
const https = require('https');
const fs = require('fs');
const options = {
hostname: 'service-b.default.svc.cluster.local',
port: 443,
path: '/api/orders',
method: 'GET',
// Client certificate
cert: fs.readFileSync('/etc/tls/tls.crt'),
key: fs.readFileSync('/etc/tls/tls.key'),
// CA certificate to verify server
ca: fs.readFileSync('/etc/tls/ca.crt'),
// Verify server identity
checkServerIdentity: (hostname, cert) => {
// Custom verification logic
if (cert.subject.CN !== 'service-b.default.svc.cluster.local') {
throw new Error('Server identity mismatch');
}
}
};
https.get(options, (res) => {
// Handle response
});
Service Mesh (Istio) Pattern
# Automatic mTLS for all services
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
namespace: default
spec:
mtls:
mode: STRICT # Require mTLS
# Authorization policy
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: service-b-policy
spec:
selector:
matchLabels:
app: service-b
rules:
- from:
- source:
principals: ["cluster.local/ns/default/sa/service-a"]
to:
- operation:
methods: ["GET", "POST"]
paths: ["/api/orders/*"]
- from:
- source:
principals: ["cluster.local/ns/default/sa/service-c"]
to:
- operation:
methods: ["GET"]
paths: ["/api/orders/*/status"]
# Request authentication (JWT validation)
apiVersion: security.istio.io/v1beta1
kind: RequestAuthentication
metadata:
name: jwt-auth
spec:
selector:
matchLabels:
app: service-b
jwtRules:
- issuer: "https://auth.example.com"
jwksUri: "https://auth.example.com/.well-known/jwks.json"
audiences:
- "service-b"
Mobile-Specific Patterns
Certificate Pinning
// iOS - Certificate pinning with URLSession
class CertificatePinner: NSObject, URLSessionDelegate {
let pinnedCertificates: [SecCertificate]
init(pinnedCertificates: [SecCertificate]) {
self.pinnedCertificates = pinnedCertificates
}
func urlSession(
_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
) {
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
let serverTrust = challenge.protectionSpace.serverTrust else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
// Get server certificate
guard let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
// Check if server cert matches any pinned cert
let serverCertData = SecCertificateCopyData(serverCertificate) as Data
for pinnedCert in pinnedCertificates {
let pinnedCertData = SecCertificateCopyData(pinnedCert) as Data
if serverCertData == pinnedCertData {
// Certificate matches - allow connection
let credential = URLCredential(trust: serverTrust)
completionHandler(.useCredential, credential)
return
}
}
// Certificate not pinned - reject connection
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
Biometric Authentication
// iOS - Biometric auth (Face ID / Touch ID)
import LocalAuthentication
class BiometricAuth {
func authenticate(reason: String, completion: @escaping (Bool, Error?) -> Void) {
let context = LAContext()
var error: NSError?
// Check if biometric auth is available
guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
completion(false, error)
return
}
// Attempt biometric authentication
context.evaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics,
localizedReason: reason
) { success, error in
DispatchQueue.main.async {
if success {
// Biometric auth successful - retrieve token from Keychain
let token = TokenStorage().getToken(forKey: "refresh_token")
completion(true, nil)
} else {
completion(false, error)
}
}
}
}
}
Compliance & Regulations
GDPR Considerations
// Right to be forgotten - token revocation
async function deleteUserData(userId) {
await db.transaction(async (tx) => {
// 1. Revoke all active tokens
await tx.query(
'UPDATE refresh_tokens SET revoked = true, ' +
'revoked_reason = $1 WHERE user_id = $2',
['GDPR deletion request', userId]
);
// 2. Anonymize audit logs (keep for compliance)
await tx.query(
'UPDATE audit_log SET user_id = NULL, ' +
'ip_address = NULL, user_agent = NULL WHERE user_id = $1',
[userId]
);
// 3. Delete user data
await tx.query('DELETE FROM users WHERE id = $1', [userId]);
});
}
// Data portability - export auth history
async function exportAuthData(userId) {
const data = {
login_history: await db.query(
'SELECT created_at, ip_address, user_agent, result ' +
'FROM login_history WHERE user_id = $1',
[userId]
),
active_sessions: await db.query(
'SELECT created_at, device_id, ip_address, expires_at ' +
'FROM refresh_tokens WHERE user_id = $1 AND revoked = false',
[userId]
)
};
return JSON.stringify(data, null, 2);
}
PCI-DSS for Payment Systems
// Requirements for authentication in payment systems
// 1. Strong access control (8.2)
const PASSWORD_REQUIREMENTS = {
minLength: 12,
requireUppercase: true,
requireLowercase: true,
requireNumbers: true,
requireSpecialChars: true,
preventReuse: 4, // Can't reuse last 4 passwords
maxAge: 90 * 24 * 60 * 60 * 1000 // 90 days
};
// 2. Multi-factor authentication (8.3)
async function loginWithMFA(username, password, mfaCode) {
const user = await validateCredentials(username, password);
if (!user) throw new AuthError('Invalid credentials');
// Require MFA for all administrative access
if (user.roles.includes('admin')) {
const validMFA = await validateTOTP(user.id, mfaCode);
if (!validMFA) throw new AuthError('Invalid MFA code');
}
return generateTokens(user);
}
// 3. Session timeout (8.1.8)
const SESSION_TIMEOUT = 15 * 60 * 1000; // 15 minutes idle
// 4. Audit logging (10.2)
await auditLog.log({
type: 'cardholder_data_access',
user_id: user.id,
resource: 'payment_methods',
action: 'read',
result: 'success',
timestamp: new Date().toISOString()
});
Testing Strategies
Mock Auth for Development
// Development-only bypass (NEVER in production)
if (process.env.NODE_ENV === 'development') {
app.use('/dev-auth/login-as/:userId', async (req, res) => {
if (process.env.ENABLE_DEV_AUTH !== 'true') {
return res.status(403).json({ error: 'Dev auth not enabled' });
}
const user = await db.findUser(req.params.userId);
const tokens = await generateTokens(user);
res.json(tokens);
});
}
// Environment check middleware
app.use((req, res, next) => {
if (req.path.startsWith('/dev-auth') && process.env.NODE_ENV !== 'development') {
return res.status(404).json({ error: 'Not found' });
}
next();
});
Integration Testing
const request = require('supertest');
const app = require('./app');
describe('OAuth2 Authorization Code Flow', () => {
let authCode, codeVerifier;
it('should initiate authorization', async () => {
codeVerifier = generatePKCEVerifier();
const codeChallenge = generatePKCEChallenge(codeVerifier);
const res = await request(app)
.get('/oauth/authorize')
.query({
response_type: 'code',
client_id: 'test_client',
redirect_uri: 'http://localhost:3000/callback',
scope: 'read write',
code_challenge: codeChallenge,
code_challenge_method: 'S256',
state: 'random_state'
});
expect(res.status).toBe(302);
expect(res.headers.location).toContain('code=');
// Extract code from redirect
const url = new URL(res.headers.location);
authCode = url.searchParams.get('code');
});
it('should exchange code for tokens', async () => {
const res = await request(app)
.post('/oauth/token')
.send({
grant_type: 'authorization_code',
code: authCode,
redirect_uri: 'http://localhost:3000/callback',
client_id: 'test_client',
code_verifier: codeVerifier
});
expect(res.status).toBe(200);
expect(res.body).toHaveProperty('access_token');
expect(res.body).toHaveProperty('refresh_token');
expect(res.body.token_type).toBe('Bearer');
expect(res.body.expires_in).toBe(900);
});
it('should detect PKCE verification failure', async () => {
const res = await request(app)
.post('/oauth/token')
.send({
grant_type: 'authorization_code',
code: authCode,
redirect_uri: 'http://localhost:3000/callback',
client_id: 'test_client',
code_verifier: 'wrong_verifier' // Wrong verifier
});
expect(res.status).toBe(400);
expect(res.body.error).toBe('invalid_grant');
});
});
describe('Refresh Token Rotation', () => {
let refreshToken1, refreshToken2;
it('should rotate refresh token on use', async () => {
// First refresh
const res1 = await request(app)
.post('/auth/refresh')
.send({ refresh_token: originalRefreshToken });
expect(res1.status).toBe(200);
refreshToken1 = res1.body.refresh_token;
// Second refresh with new token
const res2 = await request(app)
.post('/auth/refresh')
.send({ refresh_token: refreshToken1 });
expect(res2.status).toBe(200);
refreshToken2 = res2.body.refresh_token;
expect(refreshToken1).not.toBe(refreshToken2);
});
it('should detect refresh token replay', async () => {
// Try to reuse first refresh token (already rotated)
const res = await request(app)
.post('/auth/refresh')
.send({ refresh_token: refreshToken1 });
expect(res.status).toBe(401);
expect(res.body.error).toContain('replay');
// Entire family should be revoked
const familyCheck = await request(app)
.post('/auth/refresh')
.send({ refresh_token: refreshToken2 });
expect(familyCheck.status).toBe(401); // Also revoked
});
});
Token Validation Patterns
JWT Validation with Caching
const jwt = require('jsonwebtoken');
const { NodeCache } = require('node-cache');
const publicKeyCache = new NodeCache({ stdTTL: 3600 }); // 1 hour
async function validateJWT(token) {
// Decode without verification to get header
const decoded = jwt.decode(token, { complete: true });
if (!decoded) throw new AuthError('Invalid token format');
const keyId = decoded.header.kid;
// Try cache first
let publicKey = publicKeyCache.get(keyId);
if (!publicKey) {
// Fetch from JWKS endpoint
const jwks = await fetch('https://auth.example.com/.well-known/jwks.json');
const keys = await jwks.json();
const key = keys.keys.find(k => k.kid === keyId);
if (!key) throw new AuthError('Public key not found');
publicKey = jwkToPem(key);
publicKeyCache.set(keyId, publicKey);
}
// Verify signature and claims
try {
const payload = jwt.verify(token, publicKey, {
algorithms: ['RS256'],
issuer: 'https://auth.example.com',
audience: 'https://api.example.com'
});
// Additional validation
if (!payload.sub) throw new AuthError('Missing subject claim');
if (!payload.scopes || !Array.isArray(payload.scopes)) {
throw new AuthError('Missing or invalid scopes');
}
return payload;
} catch (error) {
if (error.name === 'TokenExpiredError') {
throw new AuthError('Token expired');
}
throw new AuthError('Token validation failed');
}
}
Key Rotation Without Downtime
// Support multiple signing keys simultaneously
const CURRENT_KEY_ID = 'key-2024-11';
const PREVIOUS_KEY_ID = 'key-2024-10';
const signingKeys = new Map([
[CURRENT_KEY_ID, fs.readFileSync('/keys/current-private.pem')],
[PREVIOUS_KEY_ID, fs.readFileSync('/keys/previous-private.pem')]
]);
// Sign with current key
function generateJWT(payload) {
return jwt.sign(payload, signingKeys.get(CURRENT_KEY_ID), {
algorithm: 'RS256',
keyid: CURRENT_KEY_ID,
expiresIn: '15m'
});
}
// Validate with either key (grace period)
function validateJWT(token) {
const decoded = jwt.decode(token, { complete: true });
const keyId = decoded.header.kid;
if (!signingKeys.has(keyId)) {
throw new AuthError('Unknown signing key');
}
return jwt.verify(token, signingKeys.get(keyId), {
algorithms: ['RS256']
});
}
// Key rotation process:
// 1. Generate new key pair → key-2024-12
// 2. Add to signingKeys map (validation now accepts 3 keys)
// 3. Update CURRENT_KEY_ID to key-2024-12 (new tokens use new key)
// 4. Wait for old tokens to expire (15 min)
// 5. Remove key-2024-10 from signingKeys map
Anti-Patterns
| Anti-Pattern | Why Bad | Fix |
|---|---|---|
| Long-lived JWTs | Can't revoke, security risk | Max 15-60 min, use refresh tokens |
| Tokens in localStorage | XSS vulnerability | httpOnly cookies or memory-only |
| No refresh rotation | Stolen token = permanent access | Rotate on every use, detect replay |
| Password Grant | App handles credentials, no MFA | Authorization Code + PKCE |
| Shared secrets across services | One breach = all compromised | Per-service secrets, rotate regularly |
| No rate limiting | Brute force attacks | Rate limit login, refresh, sensitive endpoints |
| Ignoring anomalies | Account takeover undetected | Monitor location, device, behavior |
| No audit logging | Can't investigate breaches | Log all auth events, immutable storage |
| Weak password requirements | Easy to crack | 12+ chars, complexity, no common passwords |
| No MFA for admins | Privileged account compromise | Require MFA for elevated access |
Cross-References
Related skills:
- Security architecture →
ordis-security-architect(threat modeling, defense-in-depth) - FastAPI implementation →
fastapi-development(FastAPI auth middleware) - REST API design →
rest-api-design(Bearer tokens, auth headers) - GraphQL auth →
graphql-api-design(context-based auth, directives) - Microservices →
microservices-architecture(service mesh, mTLS)
Further Reading
- OAuth 2.1: Latest OAuth spec (consolidates best practices)
- RFC 7636: PKCE specification
- RFC 8693: Token exchange for delegation
- OWASP Auth Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
- JWT Best Practices: https://datatracker.ietf.org/doc/html/rfc8725
- Zero Trust Architecture: NIST SP 800-207