Section 03

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

Always enforce authorization on the server side. Never rely solely on UI hiding. Check permissions at the resource level, not just the endpoint level.

Practical: JWT Validation Middleware

auth-middleware.py
python
"""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

authz-policy.rego
rego
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.

secrets-vault.py
python
"""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

NIST CSF 2.0: PR.AA (Identity Management, Authentication, Access Control)
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 →