Steven's Knowledge

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)

WhereSafe forWhy
HttpOnly secure cookieAll token types in SSR appsJS can't read it; XSS doesn't directly leak
In-memory (variable, React state)SPAsVanishes on reload; refresh via httpOnly cookie
localStorageNever for tokens that grant API accessXSS = total account takeover
sessionStorageSlightly better than localStorage; still badSame XSS issue
Secure Keychain / KeystoreMobile nativeOS-protected storage

The pattern for SPAs:

  1. Refresh token in httpOnly secure cookie (your backend issued).
  2. Access token in memory.
  3. On page load, hit /auth/refresh which uses the cookie; gets new access token; stores in memory.
  4. 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:

StrategyWhen
Stateless JWT onlyAPI-first; no need to revoke instantly; happy with token expiry
Stateless + revocation listNeed "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_id etc.
  • 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:

  • email should be UNIQUE and NOT NULL in your user table.
  • "Sign in with Google" returns an email — match to existing user if it exists (after verification).
  • Don't let email_verified=false accounts 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:

ScenarioApproach
Switching IdP providers"Lazy migrate" — keep both alive; users re-auth into new provider on next login; flag complete after N days
Adding SSO retroactivelyMap email domain → org; auto-link existing users
Splitting one tenant into twoBackfill org membership; users may need to choose
Merging accountsHard; 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

SignalWhy
Failed login rate per IP / userCredential stuffing; brute force
Successful logins from new countries / devicesAccount compromise detection
MFA challenges that failedPhishing in progress?
Password resets per minuteCoordinated attack
Refresh token reuse eventsStolen token alarm
Active session countCapacity / 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:

  • /login and /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

StandardRelevance
SOC 2Customer expectations for B2B; your IdP provides controls + audit logs
ISO 27001Similar; some enterprise checklists
GDPRRight to access / delete; user export and account deletion
HIPAAIf you handle PHI; specific IdP plans required (Auth0 HIPAA, Cognito BAA)
CCPACalifornia-specific; export and delete
PCI DSSIf 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

IdPPricing axis
Auth0Monthly active users (MAU) + features tier
ClerkMAU
WorkOSPer organization-connected (SSO) + free dev tier
Keycloak (self-host)Compute + ops
AWS CognitoMAU; 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

PitfallSymptomFix
Access token in localStorageXSS → account takeoverMemory or httpOnly cookie
Long-lived access tokensStolen token persists5-15 min expiry; rotate refresh
Trusting email_verified blindlyAccount squattingVerify before trusting email
Authorization: Bearer on public pagesToken in browser history / logsCookies for browser, headers for API
No CSRF protection on session cookiesCross-site token abuseSameSite=Lax/Strict; CSRF tokens for POST
Hand-rolling OAuthRFC bugsUse SDK
Not validating JWT audToken meant for service A used on BAlways validate aud and iss
Confusing id_token and access_tokenAuth bugsid_token = who; access_token = what they can do
Hard-coding IdP user IDs in URLsURL-based account leakageUse your own opaque IDs
No "log out everywhere" featureStolen device = persistent accessRevocation 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:

  1. User accepts invitation (email → invite URL with one-time token).
  2. Invite URL goes to login.
  3. After login, link IdP sub to invitation.
  4. 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 stage

Hard 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"

On this page