Security Design Patterns
Security design patterns are reusable solutions to common security problems. Using proven patterns reduces the risk of introducing vulnerabilities through custom implementations.
Request Path Security Controls
Scroll sideways to see the whole diagram
Authentication Patterns
Token-Based Authentication (JWT)
Stateless authentication using signed tokens. Server validates token signature without database lookup.
✓ Scalable • ✓ Stateless • ⚠ Token revocation complexity
- • Use short expiration times (15-60 minutes)
- • Implement refresh token rotation
- • Store tokens securely (HttpOnly cookies preferred)
- • Validate all claims (exp, iat, iss, aud)
Session-Based Authentication
Server maintains session state. Session ID stored in cookie references server-side session data.
✓ Easy revocation • ✓ Server control • ⚠ State management
- • Use cryptographically random session IDs (128+ bits)
- • Regenerate session ID after authentication
- • Set Secure, HttpOnly, SameSite flags on cookies
- • Implement session timeout and absolute timeout
OAuth 2.0 / OpenID Connect
Delegated authentication using identity providers. Separates authentication from authorization.
✓ SSO support • ✓ Third-party IdP • ⚠ Implementation complexity
- • Use Authorization Code flow with PKCE (not Implicit)
- • Validate state parameter to prevent CSRF
- • Verify ID token signature and claims
- • Use allowed redirect URI whitelist
Multi-Factor Authentication (MFA)
Requires multiple authentication factors: something you know, have, or are.
✓ Strong security • ✓ Phishing resistant • ⚠ User friction
- • Prefer hardware keys (FIDO2/WebAuthn) over SMS
- • TOTP apps are better than SMS (SIM swap attacks)
- • Implement backup codes securely
- • Consider risk-based/adaptive MFA
Authorization Models
RBAC (Role-Based Access Control)
Users assigned roles; roles have permissions.
User → Role → Permission Admin → [read, write, delete] Editor → [read, write] Viewer → [read]
Best for: Static permission structures, enterprise apps
ABAC (Attribute-Based Access Control)
Access based on attributes of user, resource, action, environment.
if user.dept == resource.dept AND user.clearance >= resource.level AND time.hour in [9..17] then ALLOW
Best for: Dynamic, context-aware access control
ReBAC (Relationship-Based Access Control)
Access based on relationships between entities in a graph.
User --[owner]--> Document User --[member]--> Team --[owns]--> Folder Check: can User view Document?
Best for: Social apps, file sharing, nested permissions
Authorization Best Practices
Practical: JWT Validation Middleware
"""Secure JWT validation middleware with proper error handling."""
import os
import jwt
from functools import wraps
from flask import request, jsonify, g
# RS256 is asymmetric: verify with the IdP's PUBLIC key, never a shared secret.
# Load the PEM from your secret manager and rotate it in step with the IdP.
JWT_PUBLIC_KEY = os.environ["JWT_PUBLIC_KEY"]
JWT_ALGORITHM = "RS256"
JWT_ISSUER = "https://auth.example.com"
JWT_AUDIENCE = "https://api.example.com"
# Prefer the IdP's JWKS so key rotation is automatic — resolve by the token's kid claim:
# jwks_client = jwt.PyJWKClient("https://auth.example.com/.well-known/jwks.json")
# signing_key = jwks_client.get_signing_key_from_jwt(token).key
def require_auth(required_scopes=None):
"""Decorator that validates JWT and checks scopes."""
def decorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
token = _extract_bearer_token(request)
if not token:
return jsonify({"error": "missing_token"}), 401
try:
payload = jwt.decode(
token,
JWT_PUBLIC_KEY,
algorithms=[JWT_ALGORITHM],
issuer=JWT_ISSUER,
audience=JWT_AUDIENCE,
options={
"require": ["exp", "iat", "iss", "aud", "sub"],
"verify_exp": True,
"verify_iat": True,
}
)
except jwt.ExpiredSignatureError:
return jsonify({"error": "token_expired"}), 401
except jwt.InvalidTokenError:
return jsonify({"error": "invalid_token"}), 401
# Check required scopes
if required_scopes:
token_scopes = set(payload.get("scope", "").split())
if not token_scopes.issuperset(required_scopes):
return jsonify({"error": "insufficient_scope"}), 403
g.current_user = payload["sub"]
g.token_scopes = payload.get("scope", "").split()
return f(*args, **kwargs)
return wrapper
return decorator
def _extract_bearer_token(req):
auth_header = req.headers.get("Authorization", "")
if auth_header.startswith("Bearer "):
return auth_header[7:]
return None
# Usage:
@app.route("/api/v1/users/<user_id>", methods=["GET"])
@require_auth(required_scopes={"read:users"})
def get_user(user_id):
# g.current_user is set by middleware
if g.current_user != user_id and "admin" not in g.token_scopes:
return jsonify({"error": "forbidden"}), 403
return jsonify(get_user_data(user_id))Practical: OPA Rego Authorization Policy
package api.authz
import rego.v1
# Default deny
default allow := false
# Admins can do anything
allow if {
input.user.role == "admin"
}
# Users can read their own data
allow if {
input.method == "GET"
input.path = ["api", "v1", "users", user_id]
input.user.id == user_id
}
# Editors can write to resources in their department
allow if {
input.method in {"PUT", "POST", "PATCH"}
input.user.role == "editor"
input.resource.department == input.user.department
}
# Time-based access: only during business hours
allow if {
input.user.role == "contractor"
hour := time.clock(time.now_ns())[0]
hour >= 9
hour < 17
}Secrets Management Pattern
Never hardcode secrets. Use a secrets manager with short-lived, auto-rotating credentials. Static values are read from a versioned store such as HashiCorp Vault KV v2.
"""Retrieve secrets from HashiCorp Vault with auto-renewal."""
import hvac
import os
import psycopg2
class VaultSecrets:
def __init__(self):
self.client = hvac.Client(
url=os.environ["VAULT_ADDR"],
token=os.environ["VAULT_TOKEN"], # Use AppRole in production
)
def get_database_creds(self, role="app-readonly"):
"""Get dynamic, short-lived database credentials."""
response = self.client.secrets.database.generate_credentials(
name=role,
mount_point="database"
)
return {
"username": response["data"]["username"],
"password": response["data"]["password"],
"ttl": response["lease_duration"], # Auto-expires
"lease_id": response["lease_id"],
}
def get_api_key(self, path="api-keys/stripe"):
"""Read a static secret from KV v2. The path is relative to the KV mount
(which defaults to "secret"), so hvac resolves this to secret/data/..."""
response = self.client.secrets.kv.v2.read_secret_version(
path=path
)
return response["data"]["data"]["api_key"]
# Usage — credentials auto-rotate, no hardcoded secrets
vault = VaultSecrets()
db_creds = vault.get_database_creds()
conn = psycopg2.connect(
host="db.internal",
user=db_creds["username"],
password=db_creds["password"], # Short-lived: TTL is set by the Vault role
)Auth & Secrets Decision Matrix
| Pattern | When to Use | Tools |
|---|---|---|
| JWT + OIDC | Stateless APIs, microservices, SPAs | Auth0, Keycloak, Okta |
| Session-based | Server-rendered apps, need instant revocation | Redis, PostgreSQL, Memcached |
| RBAC | Clear role hierarchy, few permission levels | Built-in, Casbin, Spring Security |
| ABAC / Policy | Dynamic rules, multi-tenant, complex logic | OPA/Rego, Cedar, Casbin |
| Secrets Manager | Any app with credentials, API keys, certs | Vault, Azure Key Vault |
Input Validation Patterns
Allowlist Validation
Define what IS allowed rather than what is NOT allowed. Reject everything not explicitly permitted.
^[a-zA-Z0-9_-]20$ // Username pattern
Schema Validation
Validate structure, types, and constraints using schemas (JSON Schema, Zod, Yup).
{ "type": "object",
"properties": {
"email": { "type": "string", "format": "email" },
"age": { "type": "integer", "minimum": 0, "maximum": 150 }
},
"required": ["email"] }
Canonicalization
Convert input to standard form before validation to prevent bypass via encoding tricks.
- • Decode URL encoding before validation
- • Normalize Unicode (NFC form)
- • Resolve path traversal (../) before checking
Data Protection Patterns
Encryption at Rest
- • AES-256 for symmetric encryption
- • Use envelope encryption (DEK + KEK)
- • Rotate keys periodically
- • Use HSM or KMS for key storage
Encryption in Transit
- • TLS 1.2+ required (prefer 1.3)
- • Strong cipher suites only
- • Certificate pinning for mobile
- • mTLS for service-to-service
Tokenization
- • Replace sensitive data with tokens
- • Token vault maps tokens to real data
- • Reduces PCI DSS scope
- • Format-preserving for legacy systems
Data Masking
- • Show only last 4 digits of SSN/CC
- • Redact in logs and error messages
- • Dynamic masking based on user role
- • Static masking for non-prod environments
Anti-Patterns to Avoid
Security by Obscurity
Hiding implementation details instead of proper security controls. Secret algorithms get discovered.
Client-Side Security
Relying on JavaScript validation or UI hiding for security. Attackers bypass the client entirely.
Hardcoded Secrets
Embedding API keys, passwords, or tokens in source code. They end up in version control and logs.
Overly Permissive Defaults
Default configurations that allow everything. Production systems inherit insecure dev settings.
Framework Alignment
ISO 27002:2022: A.8.2 (Privileged Access), A.8.3 (Information Access Restriction), A.8.5 (Secure Authentication), A.8.24 (Use of Cryptography)
CIS Controls v8.1: 5 (Account Management), 6 (Access Control Management), 3 (Data Protection)
OWASP ASVS 5.0: OWASP Application Security Verification Standard — V6 (Authentication), V7 (Session Management), V8 (Authorization), V11 (Cryptography)
Related: Security Frameworks →
Security Design Pattern Labs
Apply the authentication, authorization, validation, and secrets patterns to a service you own.
Evidence to Capture
A pattern finding is provable when you can point at the decision, the enforcement point, and the policy that runs there.
Pattern decision: The chosen pattern (session or token, RBAC or ABAC or ReBAC, central or embedded policy) and the constraint from the decision matrix that drove it. A pattern with no recorded constraint is a preference.
Enforcement point: Where the control actually runs: the middleware line, the gateway route, or the sidecar filter, plus the policy document and its revision.
Token validation config: The middleware that pins the algorithm, checks issuer and audience, and enforces expiry and revocation. The config is the evidence, not the presence of a JWT library.
Secrets reference: The vault URI or secret identifier and the identity permitted to read it. The secret value itself must never appear in a review artefact.
Data protection mapping: Which fields are encrypted or tokenised, under which key, and the data classification that requires it.
Likely False Positives
Looks like a missing pattern, usually a pattern placed one layer out.
- Authorization in the gateway: Route-level policy at the ingress or API gateway is a legitimate enforcement point. Read the route configuration before reporting an unguarded handler.
- UI shows what the API refuses: A dashboard rendering admin actions to every user is a presentation defect only if the backend accepts them. Send the request and read the response code.
- Credential in the workload manifest: A reference resolved by workload identity at runtime is not an embedded secret. Check whether the value is a reference or a literal.
- Long-lived token: A long expiry is acceptable when the token is sender-constrained by mTLS or DPoP and revocable. The binding is the control.
- Algorithm confusion concern: A library that pins the accepted algorithms at construction time is not vulnerable to alg substitution. Read the constructor, not the import.
Review Checklist
Pattern fit
- ☐ Match the chosen pattern to the decision matrix row and confirm the stated constraint is real for this system.
- ☐ Check the pattern is applied to every instance of the concern, not one of several call paths.
- ☐ Confirm the anti-pattern list was used as a review input, and that each rejected pattern has a reason.
- ☐ Look for two patterns that compose badly, such as cached authorization decisions under a revocable policy.
Enforcement point
- ☐ Confirm one chokepoint enforces authorization for every route, and list the routes that bypass it.
- ☐ Read the policy document and confirm deny-by-default: the effect of no matching rule is refusal.
- ☐ Check the policy input contains the tenant and subject, not only the resource path.
- ☐ Verify the validation middleware rejects on failure rather than continuing with an anonymous principal.
Secrets and data
- ☐ Confirm the workload reads secrets by reference and that the reading identity is not shared across services.
- ☐ Check key rotation is scheduled and that the rotation path was exercised.
- ☐ Confirm field-level protection covers the classified fields listed in the data inventory.
- ☐ Check validation runs server-side on the parsed value, not on the raw string.
Remediation
Fix the pattern, then the instance
Structural
- • Move enforcement to a single chokepoint in front of the resource and delete per-handler checks, so a new route inherits the control.
- • Make the policy deny by default and require an explicit allow rule per resource and action.
- • Replace embedded credentials with a workload identity and a secret reference resolved at start-up.
Compensating
- • Where a pattern cannot be replaced, shorten the credential lifetime and add revocation plus anomaly alerting on its use.
- • For a route that must bypass the chokepoint, record the exception with an owner and a review date.
CWE References: CWE-863