Best Practices
Production identity - token security, session strategy, migration, lock-in, observability, compliance
Best Practices
Identity is the kind of system where bugs become security incidents and migrations take quarters. The patterns below keep both costs down.
Token Storage (the Most Common Mistake)
| Where | Safe for | Why |
|---|---|---|
| HttpOnly secure cookie | All token types in SSR apps | JS can't read it; XSS doesn't directly leak |
| In-memory (variable, React state) | SPAs | Vanishes on reload; refresh via httpOnly cookie |
| localStorage | Never for tokens that grant API access | XSS = total account takeover |
| sessionStorage | Slightly better than localStorage; still bad | Same XSS issue |
| Secure Keychain / Keystore | Mobile native | OS-protected storage |
The pattern for SPAs:
- Refresh token in httpOnly secure cookie (your backend issued).
- Access token in memory.
- On page load, hit
/auth/refreshwhich uses the cookie; gets new access token; stores in memory. - Access token expires every 5-15 min; refresh transparently.
Auth0's @auth0/auth0-react and Clerk's @clerk/clerk-react both do this pattern automatically with their hosted backend.
Session Strategy
Pick deliberately:
| Strategy | When |
|---|---|
| Stateless JWT only | API-first; no need to revoke instantly; happy with token expiry |
| Stateless + revocation list | Need "log out everywhere" / "kick this session" |
| Server sessions (cookie + Redis) | SSR apps; finer control; easier revocation |
| Hybrid (short JWT + server session lookup) | Best of both; more code |
For B2B with security expectations: server-side sessions with explicit revocation are common. For B2C scale: stateless JWT + rotation + short expiry is enough.
Avoid Lock-In
You probably won't migrate IdPs, but you might. Reduce coupling:
- Use OIDC SDKs, not vendor-specific ones where you can. The Auth0 SDK works against any OIDC provider.
- Don't depend on vendor-specific claims. Use the standard
sub,email,name,org_idetc. - Store your own user record keyed by IdP
sub— never just rely on Auth0's user object as your "users table." - Avoid vendor rules / actions for business logic. Webhook into your own service for custom logic.
Your own users table looks like:
CREATE TABLE users (
id UUID PRIMARY KEY,
external_id VARCHAR(255) UNIQUE NOT NULL, -- IdP's sub
external_provider VARCHAR(50) NOT NULL, -- 'auth0' | 'clerk' | 'workos'
email VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
...
);Migrating IdPs becomes "match new IdP's sub to the old external_id for each user; cut over."
Email is Identity (Carefully)
Most flows assume email uniqueness. Be deliberate:
emailshould beUNIQUEandNOT NULLin your user table.- "Sign in with Google" returns an email — match to existing user if it exists (after verification).
- Don't let
email_verified=falseaccounts log in via password — they could squat someone else's email. - Treat email change as a sensitive operation: re-auth, verify new email, log it.
Migration Paths
You will eventually need to migrate users between systems. Plans:
| Scenario | Approach |
|---|---|
| Switching IdP providers | "Lazy migrate" — keep both alive; users re-auth into new provider on next login; flag complete after N days |
| Adding SSO retroactively | Map email domain → org; auto-link existing users |
| Splitting one tenant into two | Backfill org membership; users may need to choose |
| Merging accounts | Hard; usually manual via support |
The lazy-migration pattern works because IdPs can import password hashes (bcrypt, scrypt, Argon2) — you can move passwords without forcing reset.
Observability
| Signal | Why |
|---|---|
| Failed login rate per IP / user | Credential stuffing; brute force |
| Successful logins from new countries / devices | Account compromise detection |
| MFA challenges that failed | Phishing in progress? |
| Password resets per minute | Coordinated attack |
| Refresh token reuse events | Stolen token alarm |
| Active session count | Capacity / usage |
Most IdPs ship these to their dashboard. Pipe critical events (token reuse, lockouts, MFA failures) to your SIEM via webhooks — see ELK.
For B2B, organization-level audit logs are table stakes — customers want "show me everything users in our org did." Your IdP should provide them; if not, build them.
Rate Limiting
Auth endpoints are the highest-value attack surface in your app. Rate-limit aggressively:
/loginand/password-reset— strict per-IP and per-account limits./signup— captcha or per-IP rate limit./mfa/verify— per-account limit (3 attempts then lockout).- IdP-issued tokens — built-in rate limits per audience.
Most hosted IdPs do this for you. If self-hosting Keycloak, set the brute-force detection thoroughly.
Compliance Touchpoints
| Standard | Relevance |
|---|---|
| SOC 2 | Customer expectations for B2B; your IdP provides controls + audit logs |
| ISO 27001 | Similar; some enterprise checklists |
| GDPR | Right to access / delete; user export and account deletion |
| HIPAA | If you handle PHI; specific IdP plans required (Auth0 HIPAA, Cognito BAA) |
| CCPA | California-specific; export and delete |
| PCI DSS | If you store cards (don't); IdP shouldn't see cards |
Hosted IdPs include compliance controls. If self-hosting, you own all of it.
Cost Management
| IdP | Pricing axis |
|---|---|
| Auth0 | Monthly active users (MAU) + features tier |
| Clerk | MAU |
| WorkOS | Per organization-connected (SSO) + free dev tier |
| Keycloak (self-host) | Compute + ops |
| AWS Cognito | MAU; cheapest by far for many workloads |
Watch the paid feature cliffs — Auth0's enterprise SSO, custom domain, MFA, etc. are tier-gated. Plan ahead.
Common Pitfalls
| Pitfall | Symptom | Fix |
|---|---|---|
| Access token in localStorage | XSS → account takeover | Memory or httpOnly cookie |
| Long-lived access tokens | Stolen token persists | 5-15 min expiry; rotate refresh |
Trusting email_verified blindly | Account squatting | Verify before trusting email |
Authorization: Bearer on public pages | Token in browser history / logs | Cookies for browser, headers for API |
| No CSRF protection on session cookies | Cross-site token abuse | SameSite=Lax/Strict; CSRF tokens for POST |
| Hand-rolling OAuth | RFC bugs | Use SDK |
Not validating JWT aud | Token meant for service A used on B | Always validate aud and iss |
Confusing id_token and access_token | Auth bugs | id_token = who; access_token = what they can do |
| Hard-coding IdP user IDs in URLs | URL-based account leakage | Use your own opaque IDs |
| No "log out everywhere" feature | Stolen device = persistent access | Revocation list / session count limit |
Multi-Tenant Account Linking
A user signs up with Google in Org A. Later, Org B invites them via email. They click → Org B's IdP redirects → user logs in.
The flow:
- User accepts invitation (email → invite URL with one-time token).
- Invite URL goes to login.
- After login, link IdP
subto invitation. - User now has membership in Org B.
Auth0 Organizations, WorkOS, Clerk Organizations have this built in. Don't roll your own.
Account Deletion (GDPR Right to Erasure)
Building it from day one:
User clicks "Delete account"
→ 30-day soft-delete (recoverable)
→ after 30 days: hard delete from your DB + IdP + analytics + logs + backups
→ email confirmation at each stageHard but mandatory. Build it for your first GDPR-relevant user — retrofitting is painful.
Checklist
Production identity checklist
- Using a hosted IdP or established self-host (Keycloak / Authentik)
- Standard OIDC / OAuth; no hand-rolled protocols
- Access tokens in memory / httpOnly cookies — never localStorage
- Refresh tokens rotating with reuse detection
- JWTs validated with
aud,iss,exp, signature - Your own users table keyed by IdP
sub - Multi-tenant: org-scoped tokens or org context in JWT
- RBAC permissions in tokens; permission check middleware
- MFA enforced for sensitive actions
- Passkey support for new accounts
- SSO (SAML / OIDC) for enterprise customers (or WorkOS / similar)
- SCIM for enterprise user provisioning
- Rate limits on
/login,/signup,/password-reset,/mfa/verify - Audit log of auth events shipped off-platform
- Impersonation tooling with audit trail
- Account deletion flow (GDPR-compliant)
- Session revocation ("log out everywhere") works
- User export ("download my data") works
- On-call has runbook for "user reports account taken over"