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.
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
Policy-Based (OPA/Cedar)
Externalized policy engine makes authorization decisions.
permit( principal in Role::"editor", action == Action::"edit", resource in Folder::"docs" );
Best for: Microservices, consistent cross-service authz
Authorization Best Practices
Practical: JWT Validation Middleware
"""Secure JWT validation middleware with proper error handling."""
import jwt
from functools import wraps
from flask import request, jsonify, g
from datetime import datetime, timezone
# NEVER hardcode — load from environment / vault
JWT_SECRET = os.environ["JWT_SECRET"]
JWT_ALGORITHM = "RS256" # Use RS256 (asymmetric) in production
JWT_ISSUER = "https://auth.example.com"
JWT_AUDIENCE = "https://api.example.com"
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_SECRET,
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))"""Secure JWT validation middleware with proper error handling."""
import jwt
from functools import wraps
from flask import request, jsonify, g
from datetime import datetime, timezone
# NEVER hardcode — load from environment / vault
JWT_SECRET = os.environ["JWT_SECRET"]
JWT_ALGORITHM = "RS256" # Use RS256 (asymmetric) in production
JWT_ISSUER = "https://auth.example.com"
JWT_AUDIENCE = "https://api.example.com"
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_SECRET,
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
}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.
"""Retrieve secrets from HashiCorp Vault with auto-renewal."""
import hvac
import os
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="secret/data/api-keys/stripe"):
"""Read a static secret from KV v2."""
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"], # Expires in 1 hour
)"""Retrieve secrets from HashiCorp Vault with auto-renewal."""
import hvac
import os
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="secret/data/api-keys/stripe"):
"""Read a static secret from KV v2."""
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"], # Expires in 1 hour
)Pattern 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, AWS SSM, 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: V2 (Authentication), V3 (Session Management), V4 (Access Control), V6 (Cryptography)
Related: Security Frameworks →