Auth & Security Checklist
Reference for authentication, authorization, and security hardening in backend systems. Read when implementing auth flows, reviewing security, or hardening API endpoints.
Table of Contents
- Authentication Patterns
- Authorization Checks
- Tenant Isolation
- Session and JWT Risks
- OAuth Handling
- API Key Handling
- Webhook Signature Verification
- CSRF Protection
- CORS Configuration
- Rate Limiting
- Secret Handling
- Safe Error Messages
Authentication Patterns
Session-based
- Store session ID in HTTP-only, Secure, SameSite cookie.
- Store session data server-side (Redis, database). Never in the cookie itself.
- Regenerate session ID after login to prevent session fixation.
- Set reasonable session TTL (e.g., 24 hours idle, 7 days absolute).
JWT-based
- Use short-lived access tokens (5–15 minutes).
- Use longer-lived refresh tokens (7–30 days) stored securely.
- Store refresh tokens in HTTP-only cookies or server-side (not localStorage).
- Include minimal claims: user ID, roles, tenant ID. No sensitive data.
- Validate
exp,iss,audclaims on every request. - Use asymmetric keys (RS256/ES256) for multi-service architectures.
API Keys
- Hash API keys before storing. Never store plaintext.
- Scope keys to specific permissions and resources.
- Support key rotation without downtime.
- Log key usage for audit trail.
Authorization Checks
- Check authorization on every request, including internal service calls.
- Verify resource ownership:
WHERE user_id = :currentUserId. - Implement RBAC (role-based) or ABAC (attribute-based) depending on complexity.
- Check permissions at the service layer, not just middleware — defense in depth.
- Deny by default. Explicitly grant access.
// Pseudocode
function getOrder(orderId, currentUser):
order = db.findOrder(orderId)
if order.tenantId != currentUser.tenantId:
throw ForbiddenError
if !currentUser.hasPermission("orders:read"):
throw ForbiddenError
return orderTenant Isolation
- Add
tenant_idto all tenant-scoped tables. - Filter every query by
tenant_id— never rely on application logic alone. - Use row-level security (RLS) in PostgreSQL when available.
- Cross-tenant data access should require explicit super-admin permission.
- Test with multiple tenants to verify isolation.
Session and JWT Risks
| Risk | Mitigation |
|---|---|
| Token theft | Short expiry, HTTP-only cookies, token rotation |
| Session fixation | Regenerate session ID on login |
| Replay attacks | One-time-use refresh tokens, token binding |
| Algorithm confusion | Pin expected algorithm server-side (never alg: none) |
| Logout bypass | Maintain token blocklist or use short-lived tokens |
OAuth Handling
- Validate
stateparameter to prevent CSRF on OAuth callback. - Exchange authorization code server-side, never client-side.
- Validate
id_tokensignature and claims. - Store tokens server-side. Only send session ID to client.
- Handle token refresh transparently. Retry failed requests with fresh tokens.
- Validate redirect URIs strictly — exact match, no wildcards.
API Key Handling
- Keys hashed with bcrypt or SHA-256 before storage
- Key prefix visible for identification (e.g.,
sk_live_...), rest hashed - Scoped to minimum required permissions
- Rotation supported without downtime (multiple active keys)
- Usage logged with timestamp, IP, endpoint
- Revocation takes effect immediately
Webhook Signature Verification
- Always verify signatures before processing webhook payloads.
- Use HMAC-SHA256 with a shared secret provided by the sender.
- Compare signatures using constant-time comparison to prevent timing attacks.
- Reject replayed webhooks by checking timestamp freshness (within 5 minutes).
- Return 200 quickly, process asynchronously.
// Pseudocode
function verifyWebhook(payload, signature, secret):
expected = hmacSha256(secret, payload)
if !constantTimeEqual(expected, signature):
throw InvalidSignatureError
if payload.timestamp < now() - 5min:
throw ReplayErrorCSRF Protection
- Required when using cookie-based sessions for state-changing requests.
- Use synchronizer token pattern or double-submit cookie.
- Set
SameSite=LaxorSameSite=Stricton session cookies. - Not needed for pure JWT/API-key authentication (no cookies).
CORS Configuration
- Never use
Access-Control-Allow-Origin: *on authenticated endpoints. - Allowlist specific origins. Validate against exact match or pattern.
- Restrict
Access-Control-Allow-Methodsto methods the endpoint supports. - Set
Access-Control-Max-Agefor preflight caching (e.g., 3600 seconds). - Do not reflect the
Originheader without validation.
Rate Limiting
| Endpoint type | Suggested limit |
|---|---|
| Login / auth | 5–10 per minute per IP |
| Password reset | 3 per hour per email |
| API (authenticated) | 100–1000 per minute per user |
| API (unauthenticated) | 20–60 per minute per IP |
| Webhooks | 100 per minute per source |
- Use sliding window or token bucket algorithm.
- Return
429withRetry-Afterheader. - Apply limits at reverse proxy (nginx, API gateway) when possible.
- Consider separate limits for read vs. write operations.
Secret Handling
- Store secrets in environment variables or secret manager (Vault, AWS Secrets Manager, GCP Secret Manager).
- Never commit secrets to version control.
- Never log secrets, tokens, passwords, or API keys.
- Rotate secrets on a schedule and immediately on suspected compromise.
- Use
.env.examplewith placeholder values. Add.envto.gitignore.
Safe Error Messages
- Return generic messages to clients: "Invalid credentials", "Resource not found".
- Never expose: stack traces, database errors, internal IPs, file paths, query details.
- Log detailed errors server-side with request ID for debugging.
- Different error detail levels for development vs. production environments.