Authentication in Practice - Sessions, JWT, OAuth, mTLS, Passkeys
- Authentication (authN) answers "who are you"; authorization (authZ) answers "what may you do". A failed authN is a 401, a failed authZ is a 403 (HTTP status codes).
- A session is server-side state with a random id as the handle; a JWT is signed client-held state that any server with the key can check without a lookup. The price of stateless is that you cannot revoke before
expwithout bringing a store back. - OAuth 2.0 is delegated authorization, a valet key for an API (RFC 6749). OpenID Connect adds the login layer: an ID token says who the user is, an access token says what the app may call.
- For browsers and mobile apps use the authorization code flow with PKCE (RFC 7636, RFC 9700). Implicit and password grants are out.
- Machines prove identity with mTLS or short-lived signed tokens; people should use passkeys (WebAuthn), because the signature is bound to the site's origin and a phishing page cannot reuse it.
Mental model
A hotel. At the front desk you show your ID once (authentication) and get a key card. The card does not say who you are in words; it opens your room and the gym but not the room next door (authorization). It works until checkout (token expiry). Staff can also hand a valet key to the parking attendant: it starts the car but does not open the trunk or the glovebox. That is OAuth: you let an app act on part of your account without giving it your password.
Where it breaks: a hotel can deactivate a lost key card in seconds because the lock checks a central list. A self-contained signed token has no central list to check, so a lost one keeps working until it expires. And a hotel clerk sees your face; a website only ever sees whatever secret you present, so whoever holds the secret is you.
A session id is a pointer into a server-side map: sessions[sid] -> user. A JWT is a signed struct passed by value: the receiver verifies the checksum instead of dereferencing anything. OAuth is a capability with a limited scope, like handing a function a read-only file descriptor instead of the whole process's permissions. mTLS is each service holding an identity document that a trusted authority signed, checked at connection time.
Where it breaks: a pointer can be invalidated by deleting the map entry; a by-value token cannot be recalled, so any real revocation design (deny-list, short expiry plus a refresh lookup) reintroduces the map. And unlike a file descriptor, a bearer token is not bound to the process that received it: whoever copies the string can use it.
How it works
Authentication, authorization and the 401/403 split
Authentication establishes an identity: a user, a service, a device. Authorization decides whether that identity may perform a specific action on a specific resource. They fail differently. RFC 9110 uses 401 for "no valid credentials" (and requires a WWW-Authenticate header naming the scheme) and 403 for "I understood who you are, and the answer is no". The mistake to avoid is answering 403 when the real problem is a missing or expired token, because clients key their re-login logic off the 401.
Password login, step by step
- The browser posts the username and password over TLS (TLS). The password is protected in transit only; the server must see it once, in the clear, to check it.
- The server looks up the stored password hash and recomputes it. Passwords are never stored or compared as plaintext. The OWASP Password Storage Cheat Sheet recommends Argon2id (its minimum configuration is 19 MiB of memory, 2 iterations, 1 degree of parallelism), with scrypt, bcrypt and PBKDF2 as fallbacks. A slow, memory-hard hash is the point: it makes a stolen database expensive to crack. Salting, pepper and the hash algorithms are explained on Symmetric crypto and hashing.
- On a match the server creates a credential and hands it back. There are two families, shown next.
Session cookie versus bearer token
| Session cookie | Bearer token | |
|---|---|---|
| What the server returns | Set-Cookie: __Host-sid=... | A token in the response body |
| Who attaches it later | The browser, automatically | Your code, in Authorization: Bearer ... (RFC 6750) |
| What it is | Opaque id; the facts live in a server store | Opaque id or a self-contained JWT |
| Main browser risk | CSRF, because the cookie is sent for you | XSS, because script must hold the token |
The cookie mechanics (flags, SameSite, CSRF defences) and the JWT anatomy (header, payload, signature, base64url) are owned by Cookies, sessions, tokens and CORS. This page assumes them and covers what sits around them.
What a JWT check actually does
A JWT is signed, not encrypted: the payload is readable by anyone, and the signature only proves the first two parts were not changed (RFC 7519). The verifier runs a fixed list of checks, in this order, and rejects on the first failure (RFC 8725 section 3 is the checklist).
Worked example: a real HS256 token
This token was generated with Node's crypto module, HS256 and the throwaway secret demo-secret. It is 203 characters: 36 (header) + 1 + 122 (payload) + 1 + 43 (signature).
The header decodes to {"alg":"HS256","typ":"JWT"} and the payload to:
exp is 1893456000, which is 2030-01-01 00:00:00 UTC. The signature is HMAC-SHA256("demo-secret", header + "." + payload), 32 bytes, base64url-encoded without padding to 43 characters. The verifier recomputes it, compares, then checks iss, aud and exp against its own configuration. The Hands-on section reproduces every step in a shell.
OAuth 2.0 authorization code flow with PKCE
OAuth answers a delegation problem: a photo-printing site wants to read your photos at another service without ever holding your password there. Four roles (RFC 6749 section 1.1): the resource owner (you), the client (the app), the authorization server (issues tokens) and the resource server (the API). The browser carries the user between the app and the authorization server.
What each step carries:
| Step | Parameters and what they are for |
|---|---|
| 1-2 | response_type=code, client_id, redirect_uri, scope, state, code_challenge, code_challenge_method=S256. state is an unguessable value the app checks on return so a forged response is rejected; code_challenge is the PKCE commitment. |
| 3 | The authorization server authenticates the user (a password, MFA or a passkey) and shows the consent screen. The app never sees the credentials. |
| 4-5 | The browser is redirected to the exact registered redirect_uri with code and state in the query string. The code is single-use and short-lived (RFC 6749 section 4.1.2 recommends at most 10 minutes). |
| 6 | The app posts grant_type=authorization_code, code, redirect_uri and the **code_verifier**, plus its client credentials if it is a confidential client. The server hashes the verifier and compares it to the stored challenge. |
| 7 | The server returns an access token, usually a refresh token, and (with OIDC) an ID token. |
| 8-9 | The app calls the API with Authorization: Bearer <access_token>; the resource server validates the token and answers. |
PKCE (RFC 7636) closes the hole in step 4: on a phone or in a browser, another app or script can sometimes read the redirect and steal the code. The app invents a random verifier (43 to 128 characters), sends only code_challenge = BASE64URL(SHA256(verifier)) in step 1, and reveals the verifier only in step 6. The RFC's own test vector (appendix B):
A thief who steals the code in step 4 does not have the verifier, so step 6 fails for them.
Deep dive
HS256, RS256 and ES256
The alg header chooses how the signature is made. The choice decides who can verify and who can forge (JWA, RFC 7518).
| Algorithm | Primitive | Key | Signature size | Who can verify |
|---|---|---|---|---|
| HS256 | HMAC-SHA256 | One shared secret | 32 bytes, 43 characters | Only parties holding the secret, and each of them can also forge |
| RS256 | RSASSA-PKCS1-v1_5 with SHA-256 | RSA key pair (2048-bit modulus) | 256 bytes, 342 characters | Anyone with the public key; only the issuer can sign |
| ES256 | ECDSA on P-256 with SHA-256 | EC key pair | 64 bytes, 86 characters | Anyone with the public key; only the issuer can sign |
The sizes are derived: base64url turns every 3 bytes into 4 characters without padding, so 32 bytes is 43 characters, 256 bytes is 342 and 64 bytes is 86. With RS256 or ES256 the issuer publishes its public keys as a JWK Set (JWKS, RFC 7517) and tags each token with a kid so verifiers pick the right key; OIDC providers advertise the JWKS location in their discovery document. That is why public-key signatures dominate once more than one service verifies tokens: nobody but the issuer holds signing power. The signature theory is on Key exchange and public-key cryptography.
Attacks on JWT validation
alg: none. The spec defines "unsecured JWTs" whose signature part is empty (RFC 7519 section 6). A library that trusts the header will accept a token whosealgisnone, so an attacker can edit the payload freely. Reject it.- Algorithm confusion. A server expects RS256 and holds the RSA public key. An attacker sets
alg: HS256and signs with that public key as if it were an HMAC secret. A verifier that picks the algorithm from the header then verifies it successfully. The public key is public, so the attacker holds it. - The fix for both is the same: the verifier pins the algorithm (and the key type) in its own configuration and never lets the token choose (RFC 8725 section 3.1).
- Claims are not optional. A valid signature on a token meant for another API is still the wrong token: check
iss,audandexp(andnbfif present) every time (RFC 8725 sections 3.8 to 3.9). Allow a small clock-skew tolerance, seconds not minutes, because issuer and verifier clocks differ.
Revocation, short lifetimes and refresh tokens
A signed token cannot be recalled, so keep access tokens short-lived (minutes) and pair them with a refresh token that the authorization server looks up in a store. Revoking means deleting the refresh token; the damage window is then at most the remaining access-token lifetime. For public clients, RFC 9700 requires that refresh tokens be either sender-constrained or rotated on every use, so a stolen one is detected when both the thief and the user try to use it. If instant revocation matters, use opaque access tokens checked by introspection (RFC 7662) and revoke them with RFC 7009; you trade a network call per request for immediacy.
Where a single-page app keeps its tokens
The storage trade-off (XSS reads localStorage, CSRF rides a cookie) is laid out in the cookies page. The IETF's current guidance for browser apps goes one step further: a small backend for frontend (BFF) performs the OAuth flow, keeps the tokens server-side and gives the browser only an HttpOnly session cookie (OAuth 2.0 for Browser-Based Applications, an Internet-Draft). The browser never holds an access token at all.
Redirect URI validation and state
The authorization server must compare the redirect_uri against pre-registered values by exact string match (RFC 9700 section 2.1). A prefix or wildcard match lets an attacker point the code at a page they control. state (or PKCE) binds the response to the request that started it, defeating forged callbacks.
OAuth versus OpenID Connect
OAuth says the app may call this API. It does not say who the user is, and treating an access token as proof of login is a classic mistake. OpenID Connect (OIDC Core 1.0) adds the missing layer on top of the same code flow:
| ID token | Access token | |
|---|---|---|
| Meant for | The client app (its aud is the client id) | The resource server (API) |
| Says | Who authenticated and when: iss, sub, aud, exp, iat, and a nonce echoing the request | What the holder may do, usually via scope |
| Format | Always a JWT | A JWT or an opaque string; the client should not parse it |
| Client should | Validate signature, iss, aud, nonce, expiry, then log the user in | Send it to the API; never inspect it to learn the user |
OIDC also defines the openid scope that requests an ID token, a UserInfo endpoint for extra profile claims, and a discovery document at /.well-known/openid-configuration that lists the endpoints and the JWKS location. SAML 2.0 (an OASIS standard from 2005) solves the same single sign-on problem with XML assertions and is still common in enterprise SSO.
OAuth grants
| Grant | Use for | Status |
|---|---|---|
| Authorization code + PKCE | Users logging in through browsers, mobile and desktop apps | The default; required for public clients |
| Client credentials (RFC 6749 section 4.4) | Service to service, no user involved | Current; protect the secret or use mTLS client auth |
| Device authorization (RFC 8628) | TVs and CLIs with no browser: the user enters a short code on another device | Current |
| Implicit (token in the URL fragment) | Was for single-page apps | RFC 9700 says clients SHOULD NOT use it; the OAuth 2.1 draft drops it |
| Resource owner password credentials | Was for trusted first-party apps | RFC 9700 says it MUST NOT be used; the OAuth 2.1 draft drops it |
Credential types compared
| Credential | State lives | Revocation | Size | Best use |
|---|---|---|---|---|
| Server session id | Server store | Delete the entry; immediate | Tens of bytes | First-party browser apps |
| Self-contained JWT | In the token | Not before exp without a store | Grows with claims | Short-lived access tokens verified by many services |
| Opaque access token | Authorization server | Revoke at the server; resource servers introspect | Tens of bytes | When immediate revocation matters |
| API key | Provider's database | Delete or rotate the key | A fixed string | Identifying a calling project; not strong user authentication |
| mTLS client certificate | Issuing CA and the certificate's validity | Short certificate lifetimes or a revocation check | A certificate chain per connection | Service to service, device identity |
An API key identifies the caller, and it is long-lived and often copy-pasted into config, logs and URLs. RFC 6750 section 2.3 discourages sending a bearer credential in the URL query string because URLs are logged by servers, proxies and browsers. Send keys in a header, scope them narrowly and rotate them.
Service-to-service identity: mTLS and signed tokens
Plain HTTPS authenticates only the server. Mutual TLS makes the client present a certificate too. The handshake difference is small, and the full handshake lives on TLS:
The server validates the client certificate against a CA it trusts and reads an identity from it, usually the Subject Alternative Name; service meshes use a SPIFFE ID there (SPIFFE concepts). Certificate chains, issuance and expiry belong to certificates and PKI. OAuth can reuse the same machinery: RFC 8705 lets a client authenticate to the token endpoint with a certificate and binds an access token to the certificate, so a stolen token is useless without the private key. DPoP (RFC 9449) gives the same sender-constraining at the application layer without needing client certificates.
The trade-off between mTLS and a signed token for services: mTLS proves identity per connection and needs certificate rotation machinery; a signed token proves identity per request and survives a proxy that terminates TLS, which is why gateways often convert one into the other (proxies and gateways). For the client-credentials grant, the secret in step 6 should be replaced by mTLS or a signed assertion where possible.
Multi-factor authentication
- TOTP (RFC 6238) is HOTP (RFC 4226) with the counter replaced by time:
counter = floor(unix_time / 30), thenHMAC-SHA1(secret, counter)is dynamically truncated to a number and reduced to 6 or 8 digits. RFC 6238 appendix B gives a test vector: the ASCII secret12345678901234567890at time 59 s gives counter 1 and the 8-digit code94287082, so a 6-digit app would show287082. The server accepts the current window and often one neighbour to absorb clock drift. - SMS codes are weaker: they can be redirected by SIM swapping and read by malware. NIST SP 800-63B classifies SMS and voice delivery as a restricted authenticator (NIST SP 800-63B).
- TOTP and push approvals are phishable: a fake site can relay the code to the real one within the 30-second window. The only widely deployed factor that resists that is the next one.
Passkeys and WebAuthn
WebAuthn (W3C Web Authentication) replaces the shared secret with a public-key pair per site. At registration the authenticator (a phone, a security key, the OS) creates a key pair and the site stores only the public key. At login the site sends a random challenge; the authenticator signs it together with data that includes the site's relying-party ID and the origin the browser reports. The server verifies the signature with the stored public key.
Two properties make this phishing-resistant. First, the browser fills in the origin, not the user: a look-alike site on a different origin gets a different relying-party ID, so the authenticator does not even offer that credential, and a signature made for the real site cannot be replayed elsewhere. Second, no secret is transmitted, so a database leak at the site yields only public keys. Passkeys (FIDO Alliance) are WebAuthn credentials that can be synced across a user's devices by the platform, so losing one phone does not lock the user out; the price is that account recovery and the sync provider become part of your threat model.
Login and password-reset design
- Return the same message and similar timing whether the account exists or not, to prevent account enumeration (OWASP Authentication Cheat Sheet).
- Throttle by account and by source, with care that a lockout does not let an attacker lock other people out. The mechanics are on rate limiting and load shedding.
- Reset links carry a long random, single-use, short-lived token that is stored hashed, and the page answers identically for unknown addresses (OWASP Forgot Password Cheat Sheet).
- Rotate the session id at login (to stop session fixation) and invalidate other sessions after a password change (OWASP Session Management Cheat Sheet).
Textbook vs real world
| The textbook says | Production does | Why |
|---|---|---|
| JWTs are stateless, so they scale | Short access-token lifetimes plus a server-side refresh-token store, sometimes a deny-list | Stateless verification is cheap, but revoking a stolen or fired-user token needs state again |
| OAuth is how you log in | OpenID Connect for login, OAuth for API access | An access token says what an app may call, not who the user is; the ID token carries identity |
| HTTPS is enough to protect an API key | Keys in headers, scoped, rotated and scanned for in repos and logs | TLS protects the wire; the key still leaks through logs, URLs, config files and client code |
| Add a one-time code over SMS and you have two factors | Authenticator apps, security keys and passkeys; SMS as a fallback at most | SMS codes fall to SIM swaps and phishing relays; passkeys are bound to the origin |
| Store the token in localStorage for a single-page app | A backend-for-frontend holding tokens behind an HttpOnly session cookie | Any XSS can read localStorage; the BFF keeps the token out of script's reach |
In production
- Google documents its sign-in as OpenID Connect: a discovery document, signed ID tokens and a published key set (Google: OpenID Connect).
- GitHub offers fine-grained personal access tokens limited to chosen repositories and permissions, with an expiry (GitHub docs: managing personal access tokens).
- Stripe issues restricted API keys that grant only selected resource permissions, so a leaked key reaches less (Stripe docs: API keys).
- AWS signs every API request with Signature Version 4: a key derived from the secret key via HMAC signs the request, and the secret key itself never travels (AWS: Signature Version 4).
- Kubernetes issues pods bound service-account tokens that are time-limited and audience-bound (Kubernetes: service accounts).
- A breach, and what it teaches. In October 2023 Okta disclosed that an attacker had accessed its customer-support case system, and warned that HAR files customers upload for troubleshooting can contain session tokens that an attacker can use to impersonate users (Okta: tracking unauthorized access to Okta's support system). Cloudflare described how the attacker used a session token hijacked from a support ticket created by one of its employees to reach its Okta instance (Cloudflare blog). The lesson is the bearer-token rule: a session token pasted into a ticket or log is the account.
History and evolution
- 1994 Cookies appear in Netscape, giving stateless HTTP a way to remember a login (history on the cookies page).
- 1999 HTTP Basic and Digest authentication are standardised in RFC 2617 (now RFC 7617 and RFC 7616). Basic sends the password on every request, so it is only safe over TLS.
- 2007 OAuth 1.0 is written by a community because sites needed delegated API access without handing out passwords. It becomes RFC 5849 in 2010; its request-signing scheme was hard to implement correctly.
- 2012 RFC 6749 and RFC 6750 replace signing with bearer tokens carried over TLS, which is simpler and moves the risk to token theft.
- 2014 OpenID Connect Core 1.0 is finalised because OAuth alone did not say who the user was.
- 2015 RFC 7519 standardises the JWT. RFC 7636 adds PKCE after authorization-code interception attacks on mobile apps.
- 2019 WebAuthn Level 1 becomes a W3C Recommendation; Level 2 follows in 2021 (W3C Web Authentication).
- 2020 RFC 8705 (mTLS-bound tokens) and RFC 8725 (JWT best practices, written after alg-confusion and
nonebugs) are published. - 2022 Apple, Google and Microsoft announce support for synced passkeys (Apple newsroom: joint announcement, May 2022).
- 2023 RFC 9449 standardises DPoP, sender-constraining tokens without client certificates.
- 2025 RFC 9700, the OAuth 2.0 security best current practice, tells implementers to retire the implicit and password grants and to require exact redirect-URI matching.
- Now Passkeys are available on the major platforms; track adoption through the FIDO Alliance. OAuth 2.1 is still an Internet-Draft that consolidates these practices (draft-ietf-oauth-v2-1).
- Next Passkeys as the default login, and sender-constrained tokens (DPoP, mTLS) so that a stolen token alone is not enough.
Hands-on
Real runs on macOS with OpenSSL 4.0.3 (Homebrew) and curl 8.7.1, abbreviated. The system /usr/bin/openssl on macOS is LibreSSL and some options differ. Never paste a production token into an online decoder; decode locally.
Decode a JWT payload. base64url uses - and _ and drops the = padding, so restore both before base64 -d:
Without the padding fix, base64 -d can drop the end of the JSON. Anyone can do this, which is the point: the payload is not secret.
Recompute the HS256 signature and compare it with the third part of the token:
It matches the token's third part exactly. Change one character of the payload and it will not.
See the credential headers curl sends. These ran against a local test server:
YWRhOnNlY3JldA== is just base64("ada:secret"): Basic is the password in the open, so only use it over TLS.
mTLS with openssl s_server and s_client. The server demands a client certificate (-Verify 1) and trusts one CA file; the test certificates are self-signed with a SPIFFE-style URI in the client's SAN:
With the client certificate, the server log shows the identity it extracted. Without it:
The first two lines are the server verifying the client certificate presented by the first connection; the error is the second connection, which sent none. In TLS 1.3 the client may finish its side of the handshake before the server's rejection arrives, so watch the server's log, not just the client's Verify return code.
Compute a TOTP code by hand with the RFC 6238 test secret, using Node:
That is the value RFC 6238 appendix B lists for time 59.
Gotchas
Interview questions
Show answer
A session stores the facts on the server and gives the client an opaque id, so revocation is one delete and the cookie is tiny, but every request needs a store lookup and a multi-server deployment needs a shared store. A JWT carries the facts in a signed token, so any service with the key verifies it without a lookup, but you cannot revoke it before exp, it grows with its claims, and stale claims persist. I default to sessions for a first-party web app and use short-lived JWTs between services.
Follow-ups:
- How do you revoke a JWT? Keep it short-lived and revoke the refresh token, or keep a deny-list; both are state.
- How big can a JWT get? It rides in every request, so large claim sets cost bandwidth and can hit header and cookie size limits (cookies page).
- Which scales better? Verification scales either way; the session store is the part you must make shared and fast.
Show answer
Parse it, take the algorithm from my own configuration (never from the header), pick the key by kid from the issuer's JWKS, verify the signature, then check iss, aud, exp and nbf against what I expect, allowing a few seconds of clock skew. Any failure is a 401. The checklist is RFC 8725.
Follow-ups:
- What is alg=none? An unsecured JWT with an empty signature; a verifier trusting the header would accept forged tokens.
- What is algorithm confusion? Signing an HS256 token with an RS256 public key; pinning the algorithm stops it.
- What if the issuer rotates keys? Verifiers re-fetch the JWKS when they see an unknown
kid, with caching and a rate limit on the refresh.
Show answer
OAuth 2.0 delegates access to an API. OpenID Connect adds authentication on top of the same flow by returning an ID token, a JWT that says who the user is and is meant for the client app. The access token is meant for the resource server and says what the holder may call; the client treats it as opaque and never uses it to find out who the user is.
Follow-ups:
- What is the
noncefor? It binds the ID token to the original request, so a captured token cannot be replayed into another login. - Where do I get more user data? From the UserInfo endpoint, using the access token.
Show answer
Not in localStorage: any XSS can read it. The strongest design is a backend for frontend that runs the OAuth flow and keeps the tokens server-side, giving the browser only an HttpOnly, Secure, SameSite session cookie. Then XSS can still make requests while the page is open but cannot steal a token, and the cookie is protected from CSRF with SameSite and a CSRF token (cookies page).
Follow-ups:
- Is an
HttpOnlycookie immune to XSS? No: injected script can still send requests that carry it. It only prevents theft. - What does CSRF change about the choice? Cookies are attached automatically, so they need
SameSiteand a token; anAuthorizationheader is not attached by the browser.
Show answer
Give each workload a short-lived identity and verify it on every call. With mTLS, a mesh or a platform issues each workload a certificate whose SAN carries its identity (for example a SPIFFE ID), and both sides authenticate at connection time. With tokens, a service obtains an access token via the client-credentials grant, ideally authenticated by a certificate or a signed assertion, and the callee validates it. Either way I keep lifetimes short, automate rotation, and still do authorization per call, because identity is not permission.
Follow-ups:
- mTLS or signed tokens? mTLS authenticates the connection and needs certificate rotation; tokens authenticate each request and survive TLS-terminating proxies. Many systems use both.
- How do I rotate? Short certificate lifetimes plus an automated issuer, so rotation is routine, not an incident.
- What is SPIFFE? A standard for workload identities (SPIFFE IDs) carried in X.509 or JWT documents (SPIFFE concepts).
Show answer
Accept the password over TLS, check it against an Argon2id hash, and respond identically for unknown accounts to prevent enumeration, with rate limits per account and per source. Then require a second factor: a passkey or a TOTP code, with SMS only as a fallback. On success, issue a new session id (preventing fixation) in an HttpOnly, Secure, SameSite cookie. Password reset sends a long, random, single-use, short-lived token that I store hashed, answers the same for unknown addresses, and ends all other sessions on completion.
Follow-ups:
- How do you stop credential stuffing? Per-source and per-account limits, breached-password checks and MFA; see rate limiting.
- What about account recovery for passkeys? It becomes the weakest link, so it needs a strong, audited path.
- Why is the reset token stored hashed? So a database leak does not hand out working reset links.
Show answer
A passkey is a public-key pair created per site. At login the authenticator signs a server challenge together with the site's relying-party ID and the origin as the browser reports it. A phishing site has a different origin, so the authenticator does not offer the credential, and a signature made for the real site is not valid for any other. Also nothing secret is sent, so a server breach leaks only public keys.
Follow-ups:
- What if the phishing site proxies the real one? The origin the browser reports is still the proxy's, so the signature does not verify for the real site.
- What do synced passkeys trade away? Convenience and recovery improve, but the sync provider and recovery path join the trust boundary.
Fun facts
- A TOTP code is just HMAC-SHA1 over a 30-second counter, truncated to 6 digits. Nothing secret is stored on the phone but the shared key (RFC 6238).
- RFC 7636 says PKCE is pronounced "pixy".
- The RFC 6238 test secret is the ASCII string
12345678901234567890; every TOTP library is tested against it. - A passkey never transmits a secret: the server only ever stores a public key.
- The "none" algorithm in JWT is not a bug but a feature of the spec (unsecured JWTs); the bug is verifiers that accept it from untrusted input.
Cheat sheet
- 401 / 403 - unauthenticated / unauthorised (RFC 9110)
- 3 parts, 2 dots - JWT header.payload.signature
- 32 / 256 / 64 bytes - HS256 / RS256 (2048-bit) / ES256 signature sizes, or 43 / 342 / 86 base64url characters
- 43 to 128 characters - PKCE
code_verifierlength (RFC 7636) - 10 minutes - recommended maximum authorization-code lifetime (RFC 6749 section 4.1.2)
- 30 seconds - default TOTP time step (RFC 6238)
- Argon2id 19 MiB, 2 iterations, 1 lane - OWASP minimum password-hash configuration
- First-party web app, one backend - server session in an
HttpOnlycookie. - Single-page app calling an API - code flow with PKCE, ideally behind a backend for frontend.
- Many services verifying the same caller - short-lived JWT signed with RS256 or ES256, published via JWKS.
- Service to service - mTLS or client-credentials with a certificate-bound or signed token.
- Humans - passkeys first, TOTP second, SMS last.