1382 lines
40 KiB
Markdown
1382 lines
40 KiB
Markdown
|
|
# 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** | ~~Password Grant~~ | DEPRECATED | Don't use |
|
|
|
|
### Authorization Code + PKCE (RFC 7636)
|
|
|
|
**Why PKCE?** Prevents authorization code interception attacks
|
|
|
|
```javascript
|
|
// 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)
|
|
|
|
```javascript
|
|
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)
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```swift
|
|
// 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)
|
|
}
|
|
}
|
|
```
|
|
|
|
```kotlin
|
|
// 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
|
|
|
|
```javascript
|
|
// 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):
|
|
|
|
```javascript
|
|
// 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**:
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
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
|
|
|
|
```yaml
|
|
# 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
|
|
```
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```yaml
|
|
# 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
|
|
|
|
```swift
|
|
// 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
|
|
|
|
```swift
|
|
// 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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
// 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
|