Skip to content

Authentication

MechanismStateBest ForTrade-off
Session cookieServerTraditional web appsScalability vs simplicity
JWTClientAPIs, microservicesRevocation vs statelessness
API keyServerService-to-serviceSimplicity vs granularity
OAuth2DelegatedThird-party accessComplexity vs security
SSO (OIDC)FederatedEnterprise appsSetup cost vs unified UX
PasskeysDevicePassword replacementAdoption vs phishing resistance
1. User submits credentials (POST /login)
2. Server validates, creates session in store (Redis, DB)
3. Server sets Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Lax
4. Browser sends cookie on every subsequent request
5. Server looks up session_id in store → retrieves user context
6. On logout, server deletes session from store
AttributePurposeWhen to Set
HttpOnlyBlock JavaScript accessAlways for session cookies
SecureHTTPS transport onlyAlways in production
SameSite=LaxBlock cross-origin POSTDefault for most cookies
SameSite=StrictBlock all cross-siteAuth-sensitive cookies
DomainScope to domainSharing across subdomains
Max-AgeLifetime in secondsSet reasonable limits
PathScope to URL pathWhen isolating cookie access
StoreLatencyHorizontal ScaleSession Sharing
In-memory (default)FastestNoneNo
Redis~1msYesYes
Database~10msLimitedYes
Signed cookieN/AInfiniteYes (stateless)

Redis is the standard choice for multi-server deployments. Signed cookies work for small payloads but leak data if not encrypted.

Header.Payload.Signature
│ │ │
│ │ └─ HMAC-SHA256(base64(header) + "." + base64(payload), secret)
│ │
│ └─ {"sub":"user123","iss":"auth.example.com","exp":1735689600,"aud":"api"}
└─ {"alg":"RS256","typ":"JWT"}

All three parts are Base64URL-encoded. The signature covers header + payload — tamper with either and verification fails.

AlgorithmTypeKeyUse Case
HS256SymmetricShared secretSingle service
RS256AsymmetricRSA key pairMultiple consumers
ES256AsymmetricECDSA key pairModern, compact tokens
EdDSAAsymmetricEd25519 pairFastest verification

Use asymmetric algorithms when multiple services verify tokens — publish the public key at a JWKS endpoint, keep the private key on the auth server.

Every service that accepts a JWT must verify:

  1. Signature — using the correct public key or shared secret
  2. exp — token has not expired
  3. iss — issuer matches your auth server
  4. aud — audience includes your service
  5. nbf — not-before time has passed (if present)
  6. alg — matches expected algorithm (reject none)
Client Auth Server Resource Server
│ │ │
│── POST /token ──────────>│ │
│ (credentials) │ │
│<── access_token (15min) ─│ │
│ refresh_token (7d) │ │
│ │ │
│── GET /api ──────────────┼─────────────────────────>│
│ Authorization: Bearer │ │
│<── 200 ──────────────────┼──────────────────────────│
│ │ │
│ ... access token expires ... │
│ │ │
│── POST /token ──────────>│ │
│ (refresh_token) │ │
│<── new access_token ─────│ │
│ new refresh_token │ (rotate on each use) │

Short-lived access tokens limit exposure. Refresh tokens enable revocation without checking a deny list on every request.

GrantFlowUse CaseNotes
Authorization CodeBrowser redirectWeb apps + backendStandard choice
Authorization Code + PKCERedirect + code verifierSPAs, mobile, CLIsRequired for public clients
Client CredentialsDirect token requestService-to-serviceNo user context
Device CodeOut-of-band user authCLIs, smart TVsUser authenticates on other device
Implicit(Deprecated)Use Authorization Code + PKCE
Resource Owner Password(Deprecated)Exposes credentials to client
User Client App Auth Server Resource Server
│ │ │ │
│─ click ─────>│ │ │
│ │── redirect ───────>│ │
│ │ /authorize? │ │
│ │ response_type= │ │
│ │ code& │ │
│ │ client_id=X& │ │
│ │ redirect_uri=Y& │ │
│ │ scope=read& │ │
│ │ state=random123 │ │
│ │ │ │
│<──── login form ─────────────────│ │
│──── credentials ─────────────────>│ │
│ │ │ │
│<──── redirect to Y?code=ABC ─────│ │
│──────────────>│ │ │
│ │── POST /token ────>│ │
│ │ code=ABC& │ │
│ │ client_secret=Z │ │
│ │<── access_token ───│ │
│ │ │ │
│ │── GET /api ────────┼────────────────────>│
│ │ Bearer token │ │
│ │<── data ───────────┼─────────────────────│

PKCE (Proof Key for Code Exchange) prevents authorization code interception for public clients (SPAs, mobile apps) that cannot keep a client_secret.

1. Client generates random code_verifier (43-128 chars)
2. Client computes code_challenge = BASE64URL(SHA256(code_verifier))
3. Client sends code_challenge in /authorize request
4. Auth server stores code_challenge with the authorization code
5. Client sends code_verifier in /token request
6. Auth server hashes code_verifier, compares with stored challenge

If an attacker intercepts the authorization code, they lack the code_verifier and cannot exchange it for a token.

Scopes limit what a token can do. Define them by resource and action:

read:users # Read user profiles
write:orders # Create/update orders
admin:billing # Full billing access

Request the minimum scopes needed. Users see the scope list on the consent screen.

ConcernBest Practice
PlacementHeader (Authorization: Bearer or X-API-Key) — never in URL
StorageHash server-side; show full key only once at creation
RotationSupport multiple active keys; deprecate old ones
ScopingLimit by endpoint, IP allowlist, rate tier
PrefixAdd a recognizable prefix (sk_live_, pk_test_)
LoggingLog key prefix/ID, never the full key
DimensionAPI KeyOAuth2 Token
RepresentsApplication identityUser + app identity
ExpiryLong-livedShort-lived
RevocationDelete from storeDeny list or expiry
ScopeStatic per keyDynamic per grant
When to useServer-to-serverUser-delegated access
SAML 2.0OpenID Connect (OIDC)
ProtocolXML-basedJSON/REST (over OAuth2)
Token formatXML assertionJWT (id_token)
TransportBrowser POST / redirectBrowser redirect
Best forEnterprise, legacyModern web and mobile
Built onCustom specOAuth2
DiscoveryMetadata XML.well-known/openid-configuration
Identity dataAttributes in assertionClaims in id_token
1. App redirects to IdP: /authorize?scope=openid profile email
2. User authenticates at IdP (Google, Okta, Azure AD)
3. IdP redirects back with authorization code
4. App exchanges code for id_token + access_token
5. App reads user identity from id_token claims (sub, email, name)

OIDC adds an identity layer on top of OAuth2. The id_token tells you who the user is; the access_token lets you call APIs on their behalf.

FactorTypeStrengthUX Friction
TOTP (authenticator app)Something you haveGoodLow
SMS OTPSomething you haveWeakLow
WebAuthn / PasskeysSomething you haveExcellentLow
Email magic linkSomething you haveModerateMedium
Hardware key (YubiKey)Something you haveExcellentMedium

SMS is vulnerable to SIM-swap attacks. Prefer TOTP or WebAuthn. Passkeys (discoverable WebAuthn credentials) replace passwords entirely — the browser prompts for biometrics or a device PIN.

1. Server generates shared secret (base32-encoded, 160+ bits)
2. User scans QR code into authenticator app
3. At login, app computes: TOTP = HMAC-SHA1(secret, floor(time / 30))
4. Server computes same value, allows ±1 window for clock skew
5. Each code is valid for 30 seconds
Anti-patternProblemFix
JWT as session replacementCannot revoke without deny listShort expiry + refresh tokens + deny list
Secrets in localStorageXSS exfiltrates themUse HttpOnly cookies
Rolling your own authTiming attacks, subtle logic bugsUse established libraries (Passport, NextAuth)
Long-lived tokensExtended exposure windowShort access (15min), rotating refresh (7d)
API keys in URLsLogged in server logs, browser historySend in headers
No rate limiting on /loginBrute forceRate limit + lockout + CAPTCHA
Accepting alg: none in JWTAttacker strips signatureAllowlist signing algorithms
Shared secrets across servicesOne compromised service leaks allAsymmetric keys + JWKS endpoint
  • Security Lesson Plan — Progressive authentication and authorization exercises
  • Cryptography — Hashing, certificates, TLS
  • HTTP — Headers, status codes, cookies in curl
  • API Design — REST conventions, versioning, error handling
  • System Design — Rate limiting, load balancing, session affinity