Authentication and authorization patterns — OAuth2, JWT, sessions, RBAC/ABAC. Use when implementing login flows, securing APIs, managing tokens, designing permission systems, or reviewing auth code.
Authentication proves who a caller is; authorization decides what they can do. Use industry-standard protocols (OAuth2, OIDC, JWT) and proven patterns (session cookies, RBAC, ABAC) rather than inventing your own.
Pick the flow based on the client's ability to keep secrets:
| Client type | Flow | Token storage |
|---|---|---|
| Server-side web app | Authorization Code | Server-side session |
| Single-page app (SPA) | Auth Code + PKCE | Memory (not localStorage) |
| Mobile / native app | Auth Code + PKCE | Secure device storage |
| Service-to-service | Client Credentials | Environment variable |
| CLI / limited-input device | Device Authorization | Display user code |
See references/oauth-flows.md for full flow diagrams and exchange
parameters.
exp, iss, and aud.RS256 or ES256 for public APIs over HS256.See references/jwt-reference.md for the structure, claims, and full
validation checklist.
For server-rendered apps using session cookies:
HttpOnly, Secure, and SameSite=Lax (or Strict) on the
cookie.RBAC assigns permissions to roles and roles to users. Simple and appropriate when access depends only on the user's job function.
ROLES = {
"viewer": ["read:projects", "read:reports"],
"editor": ["read:projects", "read:reports", "write:projects"],
"admin": ["read:projects", "read:reports", "write:projects",
"manage:users", "manage:settings"],
}
def has_permission(user, permission):
return any(permission in ROLES[role] for role in user.roles)
ABAC evaluates attributes of subject, resource, action, and context. Use it when access depends on ownership, department, time of day, or other context.
def can_access(subject, action, resource, context):
if action == "edit" and resource.owner_id == subject.id:
return True
if "admin" in subject.roles and resource.department == subject.department:
return True
if action == "deploy" and not context.is_maintenance_window:
return False
return False
Access tokens should be short-lived. Refresh tokens trade themselves for new access tokens at the auth server:
Client Auth Server API
| | |
|--- Request (access token) ----------------------->|
|<-- 401 Unauthorized -------------------------------|
|--- Refresh token -------->| |
|<-- New access + refresh --| |
|--- Retry (new access token) --------------------->|
|<-- 200 OK ----------------------------------------|
Refresh tokens must be stored securely (httpOnly cookie or secure device storage), single-use (rotated on every refresh), and revocable (server-side denylist or family tracking).
Agent-specific failure modes — provider-neutral pause-and-self-check items:
exp (expiry), iss (issuer), and aud (audience) claims, means any JWT — including expired, foreign, or forged ones — will be accepted. Always validate all four: signature, expiry, issuer, and audience. Explicitly specify the allowed algorithm list so an attacker cannot switch to alg: none.localStorage are accessible to any JavaScript running on the page, including injected scripts via XSS. Keep access tokens in memory only; store refresh tokens in httpOnly cookies that JavaScript cannot read."User does not exist" vs "Incorrect password" allows an attacker to enumerate valid usernames by observing which error they receive. Always return the same generic message: "Invalid email or password." regardless of which check failed.HS256 for tokens consumed by clients you don't control. HS256 uses a shared symmetric key — anyone who knows the key can forge tokens. For public APIs where tokens are validated by multiple parties, use RS256 or ES256 (asymmetric), so clients can validate with the public key without being able to forge new tokens.sk_live_abc123 (secret),
pk_live_xyz789 (public).// Express.js example
app.use(cors({
origin: ["https://app.example.com"], // Never use "*" with credentials
methods: ["GET", "POST", "PUT", "DELETE"],
allowedHeaders: ["Authorization", "Content-Type"],
credentials: true, // Allow cookies
maxAge: 86400, // Cache preflight 24h
}));
Never set Access-Control-Allow-Origin: * together with credentials.
alg: none is rejected.Always specify allowed algorithms explicitly to prevent algorithm
confusion attacks (an attacker switching RS256 to HS256 and signing
with the public key):
import jwt
from jwt import PyJWKClient
jwks_client = PyJWKClient("https://auth.example.com/.well-known/jwks.json")
def validate_token(token: str) -> dict:
signing_key = jwks_client.get_signing_key_from_jwt(token)
return jwt.decode(
token,
signing_key.key,
algorithms=["RS256"], # Explicit allowlist
issuer="https://auth.example.com",
audience="https://api.example.com",
options={
"require": ["exp", "iss", "aud", "sub"],
"verify_exp": True,
"verify_iss": True,
"verify_aud": True,
},
)
alg header.alg: none.The auth server issues a fresh refresh token on every use and invalidates the old one. If a previously-rotated refresh token is presented again, treat it as theft and invalidate the entire token family.
HS256 for tokens consumed by clients you don't control.