jsonwebtoken (Node.js) / PyJWT (Python) for token verification; policy engines (OPA, Casbin, SpiceDB) for authorization at scale.alg header from the token itself [src2]| Component | Role | Technology Options | Scaling Strategy |
|---|---|---|---|
| Identity Provider (IdP) | Authenticates users, issues tokens | Auth0, Okta, Keycloak, AWS Cognito, custom | Horizontal behind LB; federate with OIDC |
| Password Storage | Stores hashed credentials | bcrypt, Argon2id, scrypt | Tune cost factor to ~250ms per hash |
| Token Service | Issues and refreshes access/refresh tokens | JWT (RS256/ES256), opaque tokens | Short-lived access (15min), long-lived refresh (7d) |
| API Gateway | Validates tokens, rate limits, routes | Kong, AWS API Gateway, Envoy, Nginx | Horizontal; cache JWKS with TTL |
| Session Store | Stores server-side sessions or refresh tokens | Redis, Memcached, DynamoDB | Cluster mode with replication |
| RBAC Engine | Maps users to roles to permissions | Casbin, OPA, custom middleware | Cache role-permission matrix; invalidate on change |
| ABAC Engine | Evaluates attribute-based policies | OPA (Rego), Cedar (AWS), XACML | Sidecar per service; precompile policies |
| ReBAC Engine | Checks relationship graphs for access | SpiceDB, Ory Keto, OpenFGA | Distributed graph with caching (Zanzibar model) |
| MFA Service | Second-factor verification | TOTP (Google Authenticator), WebAuthn/FIDO2, SMS | Stateless TOTP; WebAuthn for phishing resistance |
| Audit Log | Records auth events for compliance | ELK stack, CloudWatch, Datadog | Append-only, immutable, separate storage |
| Secret Management | Stores signing keys, API secrets | HashiCorp Vault, AWS Secrets Manager, GCP KMS | Auto-rotate keys; zero-trust access |
| Rate Limiter | Prevents brute-force and credential stuffing | Redis + sliding window, Cloudflare WAF | Per-IP and per-account; adaptive thresholds |
START: Choose Authorization Model
├── Do users have clearly defined roles (admin, editor, viewer)?
│ ├── YES → Are there fewer than 20 roles?
│ │ ├── YES → Use RBAC (simplest, fits 80% of apps)
│ │ └── NO → Role explosion risk — consider ABAC or hybrid
│ └── NO ↓
├── Do access decisions depend on attributes (time, location, department, resource owner)?
│ ├── YES → Are policies complex (3+ attributes per decision)?
│ │ ├── YES → Use ABAC with a policy engine (OPA, Cedar)
│ │ └── NO → RBAC + attribute filters (hybrid approach)
│ └── NO ↓
├── Do access decisions depend on relationships (user owns document, user is in org)?
│ ├── YES → Use ReBAC (SpiceDB, OpenFGA) — Google Zanzibar model
│ └── NO ↓
├── Scale > 100K concurrent users?
│ ├── YES → Use dedicated authz service (SpiceDB, OPA sidecar)
│ └── NO ↓
└── DEFAULT → Start with RBAC, evolve to ABAC/ReBAC when needed
START: Choose Token Strategy
├── Monolith architecture?
│ ├── YES → Server-side sessions (Redis) + CSRF protection
│ └── NO ↓
├── Microservices architecture?
│ ├── YES → JWT (RS256) signed by IdP, verified at gateway + service
│ └── NO ↓
├── Serverless / edge?
│ ├── YES → JWT (ES256) with short expiry (5-15min), no refresh at edge
│ └── NO ↓
└── DEFAULT → JWT (RS256) + refresh token rotation
Hash passwords with bcrypt (cost 12+) or Argon2id before storing. Never store plaintext or MD5/SHA hashes. [src1]
// Node.js — user registration with bcrypt
const bcrypt = require('bcrypt'); // [email protected]
const SALT_ROUNDS = 12; // ~250ms on modern hardware
async function registerUser(email, password) {
if (password.length < 8) throw new Error('Password too short');
const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
return db.query(
'INSERT INTO users (email, password_hash) VALUES ($1, $2) RETURNING id',
[email, hashedPassword]
);
}
Verify: await bcrypt.compare('testpass', stored_hash) → expected: true
Issue short-lived access tokens (15 min) and long-lived refresh tokens (7 days). Use asymmetric keys (RS256) for distributed verification. [src3]
// Node.js — JWT token issuance
const jwt = require('jsonwebtoken'); // [email protected]
const fs = require('fs');
const PRIVATE_KEY = fs.readFileSync('./keys/private.pem');
function issueTokens(user) {
const payload = { sub: user.id, email: user.email, roles: user.roles };
const accessToken = jwt.sign(payload, PRIVATE_KEY, {
algorithm: 'RS256', expiresIn: '15m', issuer: 'auth.example.com',
});
const refreshToken = jwt.sign(
{ sub: user.id, type: 'refresh' }, PRIVATE_KEY,
{ algorithm: 'RS256', expiresIn: '7d', jti: crypto.randomUUID() }
);
return { accessToken, refreshToken };
}
Verify: Decode token at jwt.io — payload should contain sub, roles, exp fields
Validate JWT on every request. Reject if expired, tampered, or using unexpected algorithm. [src1]
// Node.js Express — JWT authentication middleware
const jwt = require('jsonwebtoken');
const PUBLIC_KEY = fs.readFileSync('./keys/public.pem');
function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer '))
return res.status(401).json({ error: 'Missing bearer token' });
try {
req.user = jwt.verify(authHeader.slice(7), PUBLIC_KEY, {
algorithms: ['RS256'], issuer: 'auth.example.com',
});
next();
} catch (err) {
const msg = err.name === 'TokenExpiredError' ? 'Token expired' : 'Invalid token';
res.status(401).json({ error: msg });
}
}
Verify: Send request without token → expected: 401 Missing bearer token
Separate authorization from authentication. Check roles/permissions after identity is established. [src2]
// Node.js Express — RBAC authorization middleware
function authorize(...requiredRoles) {
return (req, res, next) => {
if (!req.user) return res.status(401).json({ error: 'Not authenticated' });
const hasRole = requiredRoles.some(r => req.user.roles?.includes(r));
if (!hasRole) return res.status(403).json({ error: 'Insufficient permissions' });
next();
};
}
// Usage: app.delete('/api/users/:id', authenticate, authorize('admin'), deleteUser);
Verify: Send request as viewer to admin-only route → expected: 403 Insufficient permissions
Implement refresh token rotation to limit compromise window. Invalidate old refresh tokens on use. [src3]
// Node.js — refresh token rotation
async function refreshAccessToken(refreshToken) {
const decoded = jwt.verify(refreshToken, PUBLIC_KEY, { algorithms: ['RS256'] });
const isRevoked = await redisClient.get(`revoked:${decoded.jti}`);
if (isRevoked) throw new Error('Refresh token revoked — possible theft');
await redisClient.set(`revoked:${decoded.jti}`, '1', { EX: 7 * 86400 });
const user = await db.query('SELECT * FROM users WHERE id = $1', [decoded.sub]);
return issueTokens(user);
}
Verify: Use same refresh token twice → second attempt should return error
Log all authentication events (login, logout, failed attempts, privilege changes) for compliance and incident response. [src2]
// Node.js — audit logging middleware
function auditLog(event) {
return (req, res, next) => {
auditStore.append({
timestamp: new Date().toISOString(), event,
userId: req.user?.sub || 'anonymous', ip: req.ip,
userAgent: req.headers['user-agent'],
resource: req.originalUrl, method: req.method,
});
next();
};
}
Verify: Check audit log after login attempt → should contain event entry with IP and timestamp
// Input: HTTP request with Authorization: Bearer <token>
// Output: req.user populated or 401/403 response
const jwt = require('jsonwebtoken'); // [email protected]
const PUBLIC_KEY = process.env.JWT_PUBLIC_KEY;
const authenticate = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'No token' });
try {
req.user = jwt.verify(token, PUBLIC_KEY, { algorithms: ['RS256'] });
next();
} catch (e) { res.status(401).json({ error: 'Invalid token' }); }
};
const authorize = (...roles) => (req, res, next) => {
if (!roles.some(r => req.user.roles?.includes(r)))
return res.status(403).json({ error: 'Forbidden' });
next();
};
app.delete('/users/:id', authenticate, authorize('admin'), handler);
# Input: Flask request with JWT in Authorization header
# Output: Decorated endpoint enforces role check or returns 403
from functools import wraps
from flask import request, jsonify, g
import jwt # PyJWT==2.x
PUBLIC_KEY = open("keys/public.pem").read()
def require_auth(f):
@wraps(f)
def decorated(*args, **kwargs):
token = request.headers.get("Authorization", "").removeprefix("Bearer ")
if not token:
return jsonify({"error": "Missing token"}), 401
try:
g.user = jwt.decode(token, PUBLIC_KEY, algorithms=["RS256"])
except jwt.ExpiredSignatureError:
return jsonify({"error": "Token expired"}), 401
except jwt.InvalidTokenError:
return jsonify({"error": "Invalid token"}), 401
return f(*args, **kwargs)
return decorated
def require_role(*roles):
def decorator(f):
@wraps(f)
@require_auth
def decorated(*args, **kwargs):
if not any(r in g.user.get("roles", []) for r in roles):
return jsonify({"error": "Forbidden"}), 403
return f(*args, **kwargs)
return decorated
return decorator
@app.route("/admin/users", methods=["DELETE"])
@require_role("admin")
def delete_user():
pass # Only admins reach here
// BAD — hardcoded secret, easily leaked via git
const token = jwt.sign(payload, 'my-super-secret-key-123', { algorithm: 'HS256' });
// GOOD — key loaded from environment, rotatable without code change
const PRIVATE_KEY = fs.readFileSync(process.env.JWT_PRIVATE_KEY_PATH);
const token = jwt.sign(payload, PRIVATE_KEY, { algorithm: 'RS256' });
// BAD — attacker can forge token with {"alg": "none"} header
const decoded = jwt.verify(token, secret);
// GOOD — rejects any token not signed with RS256
const decoded = jwt.verify(token, PUBLIC_KEY, { algorithms: ['RS256'] });
// BAD — frontend guard is trivially bypassed
if (user.role === 'admin') {
showDeleteButton(); // attacker calls DELETE API directly
}
// GOOD — server checks role regardless of frontend
app.delete('/api/users/:id',
authenticate, // who are you?
authorize('admin'), // are you allowed?
async (req, res) => { /* ... */ }
);
// BAD — JWT payloads are base64-encoded, NOT encrypted
const payload = { sub: id, email: e, password: pw, ssn: '123-45-6789' };
// GOOD — only non-sensitive identifiers and roles
const payload = { sub: userId, roles: ['editor'], iss: 'auth.example.com' };
// BAD — token valid forever, no way to revoke
const token = jwt.sign(payload, key); // no expiresIn
// GOOD — access token expires in 15 min, refresh token rotated on use
const accessToken = jwt.sign(payload, key, { expiresIn: '15m' });
const refreshToken = jwt.sign({ sub: userId, jti: uuid() }, key, { expiresIn: '7d' });
# Decode a JWT without verification (inspect claims)
echo "$TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq .
# Verify JWT signature with public key (Node.js one-liner)
node -e "const jwt=require('jsonwebtoken'); const key=require('fs').readFileSync('public.pem'); console.log(jwt.verify('$TOKEN', key, {algorithms:['RS256']}))"
# Check bcrypt hash cost factor
node -e "const h='$HASH'; console.log('Cost factor:', h.split('$')[2])"
# Test login endpoint rate limiting
for i in $(seq 1 10); do curl -s -o /dev/null -w "%{http_code}\n" -X POST https://api.example.com/login -d '{"email":"[email protected]","password":"wrong"}'; done
# List active sessions in Redis
redis-cli keys "session:*" | head -20
# Check JWKS endpoint availability
curl -s https://auth.example.com/.well-known/jwks.json | jq '.keys | length'
| Standard / Tool | Status | Key Changes | Notes |
|---|---|---|---|
| NIST SP 800-63B-4 (2025) | Current | Phishing-resistant MFA required for AAL3; password complexity rules removed | Replaces SP 800-63B |
| OAuth 2.1 (2025 draft) | Near-final | PKCE required for all flows; implicit flow removed | Supersedes OAuth 2.0 for new implementations |
| OAuth 2.0 (RFC 6749, 2012) | Active | — | Still valid; use 2.1 for new projects |
| JWT (RFC 7519, 2015) | Active | — | Pair with RFC 7517 (JWK) for key management |
| WebAuthn Level 3 (2024) | Current | Conditional UI, cross-device authentication | Preferred for phishing-resistant MFA |
| SpiceDB 1.x (2024) | Active | Zanzibar-inspired ReBAC | Use for large-scale relationship-based access |
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Building a new application with user accounts | Single-user CLI tools or scripts | OS-level file permissions |
| Multiple user roles with distinct permissions | All users have identical access | Simple API key authentication |
| Compliance requirements (SOC 2, HIPAA, GDPR) | Internal-only prototype with no real data | Skip auth entirely for local dev |
| Microservices need shared identity verification | Service-to-service only (no human users) | mTLS or service mesh identity |
| Fine-grained access (ABAC/ReBAC) on resources | Fewer than 3 roles in a simple CRUD app | Basic RBAC middleware is sufficient |
| Multi-tenant SaaS with per-org permissions | Single-tenant, single-org deployment | Simple role column in users table |