Security
Threat model
This SDK handles authentication for web applications. The primary threats are:
- Token theft/replay: Magic-link tokens intercepted in transit or stolen from storage
- Session hijacking: Session cookies stolen via XSS, MITM, or cookie theft
- Brute force: Automated attempts to guess magic-link tokens
- Email enumeration: Attackers determining which emails have accounts
- CSRF: State-changing auth endpoints called by malicious sites
- Token reuse: Magic-link tokens consumed more than once
- Audit trail poisoning: Tokens or sensitive data leaking into logs
Security requirements (v1)
Token security
- Magic-link tokens MUST be hashed (SHA-256) before storage. Raw tokens are never persisted.
- Tokens MUST have a configurable TTL (default: 15 minutes)
- Tokens MUST be single-use. D1 consumption uses one ordered batch and requires a successful delete.
- Creating a token invalidates every older outstanding token for the same email.
- A failed email send deletes the newly issued token before returning the generic login response.
- Token generation MUST use
crypto.getRandomValues()with sufficient entropy (minimum 32 bytes, hex-encoded) - Email GETs MUST NOT consume auth tokens; consumption requires an explicit same-origin POST.
- Expired tokens MUST be cleaned up on a schedule (D1: scheduled Worker, MySQL: cron/event)
Session security
- Session cookies MUST be
HttpOnly,Secure,SameSite=Lax, path/ - Cookie name MUST be configurable per host app (default:
jp_session) - Session IDs MUST be cryptographically random (minimum 32 bytes)
- Only SHA-256 session hashes are persisted; raw bearer session IDs remain in the cookie only.
- Sessions MUST have a configurable TTL (default: 7 days)
- Session rotation: on every
completeLogin, issue a new session ID (never reuse) - Session revocation:
logoutMUST delete the session from storage AND clear the cookie - Provide a
revokeAllSessions(userId)method for emergency use
Rate limiting
-
requestLogin(magic-link send): rate limit per email (max 5 per 15 minutes) and per IP (max 20 per 15 minutes) -
completeLogin(token verification): rate limit per IP (max 10 per 15 minutes) - The built-in D1 adapter atomically counts and decides each attempt; concurrent requests cannot all pass a separate check before incrementing.
- Rate limit responses do not leak whether the email exists.
- Third-party storage adapters should implement optional
consumeRateLimit; the compatibilitycheckRateLimit/incrementRateLimitfallback remains non-atomic.
CSRF protection
- All state-changing auth endpoints (
requestLogin,completeLogin,logout) MUST require POST and verify origin - Use
Originheader check against a configurable allowed-origins list - For the subscribe widget: the form submission endpoint MUST also be origin-checked
Email enumeration resistance
-
requestLoginMUST return the same response regardless of whether the email exists or is authorized - Host authorization MUST run before email, before session creation, and on every session resolution.
Redirect safety
-
redirectToMUST be a same-origin application path, never an absolute URL. - Magic-link builders receive only normalized paths.
- Logout and successful verification responses return only normalized paths.
- The subscribe widget intentionally has a known enumeration characteristic (different email content for known vs unknown users). This is documented as a deliberate tradeoff. Mitigation: identical UI response, identical subject line, identical email template structure.
Logging hygiene
- Auth adapter logs never receive raw tokens, token prefixes, session IDs, session prefixes, email addresses, or magic-link URLs.
- Auth adapter warnings are structured JSON containing only action, safe error code, and error category; arbitrary exception messages are not logged.
- Add a host-owned audit sink for timestamp, request metadata, and result without putting credentials or personal data in generic Worker logs.
Email delivery observability
- Provider message IDs and non-secret delivery states are stored in the host-owned D1.
- Cloudflare lifecycle events are schema-validated before storage.
- Shared-domain events update only message IDs already correlated by the host; unknown recipients, subjects, and message bodies are never copied into its D1.
- Queue logs exclude recipient addresses, subjects, SMTP details, and message IDs.
- Repeated provider events are idempotent by provider event ID.
Dependency management
- Renovate or Dependabot enabled on the repo
-
pnpm auditruns in CI on every PR - No runtime dependencies in
auth-core(contracts only) - Minimize dependencies in provider packages — prefer Web Crypto API and platform primitives
Deployment exposure
- Production Worker configs disable
workers.devand Preview URLs; private host apps should additionally use Worker-attached Cloudflare Access.
Security non-goals (v1)
Things we're explicitly not building yet:
- MFA / TOTP / WebAuthn (defer to Firebase or BetterAuth adapters)
- Account lockout (rate limiting is sufficient for magic-link flows)
- IP allowlisting
- WAF-level protections (handled by Cloudflare at the edge)
- Penetration testing (manual review is sufficient at our current scale)
Security review process
Use .ai/prompts/security-review.md to run a security audit against any version of the codebase. The prompt is designed for a coding agent (Codex or Claude Code) to systematically check each item above.
Incident response
If a vulnerability is discovered:
- Determine blast radius (which host apps are affected)
- If token/session compromise: rotate all secrets, revoke all sessions via
revokeAllSessions - Fix in
auth-coreor relevant package - Bump version, deploy to all affected host apps
- Document in
SECURITY-CHANGELOG.md(create when first needed)