Authentication: sessions, JWT, two-step and passkeys
How a server proves who is calling: authentication versus authorization, how a JWT is built, signed and checked, where a browser keeps a session and how it survives XSS and CSRF, how a session is ended early, how passwords are stored and guessing is stopped, and how authenticator codes and passkeys work inside - step by step, with what {{brand}} chose and why.
In short
Authentication answers one question: who is making this request? In {{brand}} you prove it with a password and, if you turned it on, a second step - an authenticator code or a passkey. The server then gives your browser a signed token, a JWT, inside an HttpOnly cookie that page scripts cannot read. Every later request carries that cookie; the server checks the signature, checks that the session behind it is still alive, and reads your roles and permissions fresh from the database.
After this lesson you can 1. Explain how a JWT is built, signed and checked. 2. Choose where a browser keeps a session and defend it against XSS and CSRF. 3. End a session early and store passwords safely. 4. Walk through TOTP and passkeys step by step.
Words you need
- Authentication and authorization Authentication proves who is calling: a password, a code, a passkey, a company sign-in. Authorization decides what that caller may do, on every request, after authentication.
- Session One signed-in browser or device. Many systems keep a row per session on the server so it can be ended.
- JWT JSON Web Token: a small piece of JSON with a signature. Anyone can read it; only the key holder can make a valid one.
- Refresh token A long-lived credential used only to get a new short-lived access token; the server can rotate or refuse it.
- HttpOnly cookie A cookie the browser sends with requests but page scripts cannot read.
- XSS and CSRF XSS (cross-site scripting) is an attacker's script running inside your page. CSRF (cross-site request forgery) is another website making your browser send a request to yours, with your cookie attached.
- HTML sanitizer Code that takes untrusted HTML and keeps only an allow-list of safe tags and attributes, so rich text can be shown without running an attacker's script. DOMPurify is the usual one in the browser.
- Password hash A slow one-way fingerprint made with a random salt. The password itself is never stored.
- Account enumeration Learning which email addresses have accounts from messages, timing or lockouts.
- TOTP Time-based one-time password: the six digits in an authenticator app, new every 30 seconds.
- QR code A square grid of dark and light modules that stores a short text - here the address a phone scans to learn the TOTP secret. Error correction inside it lets a scuffed code still scan.
- Passkey A key pair your device makes for one site; the site keeps only the public key. Built on WebAuthn.
Authentication and authorization are two questions
The two words are often mixed up, and the difference is a classic first interview question. Authentication proves identity once, at sign-in, and is then re-checked cheaply on every request. Authorization decides, on every request and for every record, whether that identity may act. This lesson is about the first; the access lesson covers the second.
One sign-in, from password to cookie
- 1. You send email and password The sign-in form posts them over HTTPS. Nothing is stored in the browser yet.
- 2. The password is checked The server recomputes the slow, salted hash and compares. Repeated wrong passwords are slowed down rather than locking the account.
- 3. A second step, if you have one Instead of a session, the server answers with a short-lived, sealed challenge proving the password was right. You add an authenticator code, a recovery code or a passkey.
- 4. A session is created The server writes a session row - which device and when last used - and signs a JWT that names that session.
- 5. The token goes into a cookie The response sets an HttpOnly, Secure, SameSite=Strict cookie. The body carries your profile, never the token, so page scripts never touch it.
- 6. Every request after that The browser sends the cookie by itself. The server checks the signature and expiry, then whether the session is still live, then your permissions.
A passkey replaces steps 1 to 3 With a passkey, the device proves who you are with a key it holds and your fingerprint, face or PIN. That is two factors at once - something you have and something you are or know - so the sign-in goes straight to step 4.
Inside a JWT
A JWT (RFC 7519) is three pieces of text joined by dots: a header, a payload of claims and a signature. The first two are only base64url-encoded JSON, so anyone holding the token can read them. Only the signature needs the key.
Signed is not secret A JWT is signed, not encrypted. Never put a password, a personal detail or anything secret in the payload; a display name is tolerable only when nothing trusts it for decisions. An encrypted form, JWE, exists, but it is rarely needed when the token only travels between your own browser and your own API over HTTPS.
How a JWT is signed: HS256, RS256, ES256
The header's alg says how the signature was made. There are two families. Symmetric algorithms (HMAC, the HS family) use one shared secret that both signs and checks. Asymmetric algorithms (RS, PS, ES and EdDSA) sign with a private key and check with a public key, so a service that only checks tokens never holds anything that can make one.
Key ids make rotation painless Put a kid (key id) in the header. The server then accepts the current key and the previous ones by id: it can start signing with a new key today and let tokens signed with the old one run out naturally, so a key rotation signs nobody out.
How a server checks a JWT, step by step
- 1. Split and decode Exactly three base64url parts. Anything else is refused before any cryptography runs.
- 2. Pin the algorithm Accept only the alg you issue. Never let the header choose: that is how alg none and algorithm confusion attacks work.
- 3. Find the key Use the kid to pick the current or a previous key from your own list; an unknown kid is refused.
- 4. Verify the signature Recompute (HMAC) or verify with the public key, comparing in constant time.
- 5. Check the time exp in the future, nbf in the past, with a clock skew of seconds - not the five minutes many libraries allow by default.
- 6. Check issuer and audience iss and aud must be exactly yours.
- 7. Ask whether it is still wanted Cryptography cannot know about a sign-out. Check the deny list and the session the token names.
- 8. Load permissions, not the token's opinion Read roles and permissions from the database for this request. The token says who; the server decides what.
A browser session usually rides in a cookie instead of a header. The server sets the cookie once at sign-in, and a small hook copies it into the token slot when no Authorization header is present. A header, if there is one, wins.
- alg none: a token with no signature that a careless library accepts. Defence: an allow-list of algorithms and signed tokens required.
- Algorithm confusion: an RS256 public key used as an HS256 secret, so the attacker signs with a key everyone has. Defence: one algorithm per key, never chosen by the header.
- Weak HMAC secret: a short, password-like secret is cracked offline from one captured token. Defence: at least 32 random bytes from a secret store, never from source code.
- kid injection: a kid that points the server at a file path or a database value the attacker controls. Defence: the kid is only a lookup in your own fixed list.
The options in the market
- Token in localStorage, sent in a header The classic single-page-app tutorial.; Simple: no cookies, so no CSRF; Works across any domains; Any script on the page - one XSS bug, one bad npm package - can read and steal it; Cannot be revoked without a server-side check
- Random session id in a cookie The classic framework way: all state on the server.; Revoked at once by deleting the row; Nothing readable in the browser; A database or cache read on every request; Needs CSRF defences like any cookie
- Access token in memory, refresh token in a cookie Common with OAuth: a short access token renewed quietly.; A stolen access token dies within minutes; Refresh tokens can be rotated and revoked; A refresh endpoint, rotation and reuse detection to build; More moving parts in the client
- Signed JWT in an HttpOnly cookie, tied to a server session A signed token that names a session the server can end.; Scripts cannot read it; Signature checked without a lookup; the session check is small and cached; Every device can be listed and ended; Needs CSRF defences; A session check on every request, cached to stay cheap
- HS256 One shared secret.; Simplest: one key to protect; Fastest to sign and check; Right when the same service issues and checks; Whoever can check can also forge; Other services cannot verify without the secret
- RS256 RSA key pair.; The public key can be shared freely (JWKS); Supported everywhere; Large signatures and tokens; Keys to publish and rotate
- ES256 Elliptic-curve key pair.; Small, fast signatures; Public-key checking; Signing needs a fresh random nonce (or RFC 6979); a repeated nonce leaks the key; Keys to publish and rotate
- EdDSA Ed25519.; Modern and deterministic; Small keys and signatures; Not every JWT library supports it yet
What {{brand}} chose, and why
{{brand}} is one API serving its own web app. The same service issues and checks every session token, so an HS256 signature with a strong key from configuration is enough - and a key that never leaves the server is easier to protect than a published key set. The token rides in an HttpOnly cookie, because then the most common web attack, XSS, cannot steal it.
The price is a small, cached session check on every request. Signing out and ending a device take effect at once. {{brand}} accepted that in exchange for simplicity and a list of signed-in devices that people can see and end themselves.
When another choice is right Many services checking tokens, or tokens handed to third parties: RS256 or ES256 with a published key set. A mobile app: tokens in the platform's secure storage, with OAuth 2.0, PKCE and refresh tokens. A server-rendered site on one domain: a plain session id cookie is simpler than any JWT.
Cookie settings that matter
A SameSite=Strict cookie travels between same-site hosts, but a call from app.example.com to api.example.com is still cross-origin. CORS is the browser rule that lets a page read a response from another origin only if that server allows it. For cookies the API must name the exact origin (never *) and send Access-Control-Allow-Credentials: true.
XSS and CSRF: the two attacks a session cookie must survive
XSS is an attacker's script running inside your page - through an unescaped comment, a bad dependency or a crafted link. CSRF is another website making your browser send a request to yours with your cookie attached. HttpOnly answers the first; SameSite, origin checks and a custom header answer the second.
- You are signed in Your browser holds a session cookie for bank.example.
- You open another page evil.example hides a form that posts to bank.example/transfer and submits it with a script.
- The browser attaches the cookie Without SameSite, a cookie goes with every request to its site, whoever started it.
- The server sees a valid session Without a CSRF defence it cannot tell the forged request from yours, and the money moves.
In practice, layer them: SameSite, an origin check and a custom header. Feed the origin check and CORS from one list of allowed origins so they cannot disagree. Requests that authenticate with an explicit Authorization header are not ambient, because browsers never add that header on their own.
The main defence against XSS is output encoding: show user text as text, so < becomes < and cannot start a tag. Frameworks such as React do this for you. The risk returns when you must show rich text - bold, lists, links - because that is HTML by nature. Setting innerHTML (React's dangerouslySetInnerHTML) with raw HTML runs any script inside it, for example an img tag with an onerror handler. The answer is a sanitizer.
DOMPurify parses the string with the browser's own HTML parser into an inert document, where scripts never run and images never load. It then walks every node and attribute and deletes whatever is not on its allow-list: script elements, event attributes such as onclick, javascript: addresses and markup that would turn dangerous when parsed a second time (mutation XSS). Last, it writes the clean tree back as a string. Because it uses the real parser, it reads ambiguous markup the way the browser will. You can narrow the allow-list with ALLOWED_TAGS and ALLOWED_ATTR.
{{brand}} sanitizes rich text twice, in two places: the server keeps only an allow-list of tags when text is saved, and the browser runs DOMPurify again before showing it. Stored text can be older than today's rules, imported, or written by another client, so no single check is trusted. DOMPurify was chosen because it is the most widely reviewed browser sanitizer and works with the browser's own parser; the trade-off is a library to keep updated, since bypasses have been found and fixed in sanitizers before.
Sanitize last, and never edit the result Clean the HTML at the moment you hand it to the page and insert exactly what came back. Joining strings after cleaning, or decoding after cleaning, can bring a script back. Keep DOMPurify updated, and add a Content-Security-Policy as the net underneath.
HttpOnly does not make XSS harmless An injected script cannot read the cookie, but it can still send requests from your page, and the browser attaches the cookie. Escape all output, set a Content-Security-Policy, and keep dependencies few and reviewed. HttpOnly limits the damage to the life of the page; it does not remove it.
Ending a session before its token expires
A signed token stays valid until exp: the signature cannot know that you signed out, reset your password or were deactivated. Every system that uses JWTs must add a server-side check and decide what it costs. These are the four tools.
- Deny list Ids (jti) of tokens signed out early, kept in a fast shared cache only until each would expire anyway, so the list stays small.
- Session row One database row per signed-in device. The token names it (sid); ending the row ends that device.
- Security stamp A random value on the account that changes when its credentials change. Tokens carry a fingerprint of it; a new stamp fails every older token.
- Short life plus refresh A token that dies in minutes, renewed with a refresh token the server can refuse.
Why the token carries no roles If the token said role: admin, removing that role would only count when the token expired. {{brand}} puts who you are in the token and reads what you may do from the database on every request, so a removed permission counts on the very next click.
Short access token plus refresh token
The other classic answer to revocation is a very short-lived access token paired with a refresh token. It is the pattern behind OAuth 2.0 for mobile and single-page apps (with PKCE protecting the first hand-over), and interviewers ask about it constantly.
- 1. Sign-in returns two tokens An access token that lives minutes and a refresh token that lives days.
- 2. The client calls the API Every request carries the access token.
- 3. The access token expires The client sends the refresh token to the refresh endpoint.
- 4. The server rotates It checks the refresh token against its store, issues a new access token and a new refresh token, and marks the old one used.
- 5. Reuse is detected If a used refresh token ever arrives again, someone copied it: revoke the whole family of tokens (reuse detection).
- 6. Sign-out deletes the refresh token No new access token can be minted, so the session ends within minutes.
The price is a refresh endpoint, a token store and the rotation logic.
The session remembers how it signed in
An account may have a passkey while this particular browser signed in with the password alone. A rule such as a company requiring two-step sign-in must therefore ask about the session in use, not only the account. OpenID Connect carries this as the amr claim (authentication methods such as pwd, otp or hwk); a system can record with each session whether a second step was used.
- Step-up: when a password-only session meets a rule that needs more, ask for the second step instead of failing silently.
- Re-authentication (a recent-auth check): sensitive actions require proof from the last few minutes, via an auth_time-style claim or a fresh password prompt, so a stolen unattended session cannot change protections.
- Notification: email the account owner on a new-device sign-in and when protections change, so a stranger's access does not go unnoticed.
Storing passwords: slow, salted, versioned hashes
A server must be able to check a password without being able to read it, so it stores a one-way hash. Ordinary hashes such as SHA-256 are far too fast: one graphics card tries billions per second. Password hashes are deliberately slow and add a random salt per password, so two people with the same password get different hashes and precomputed tables are useless.
- At sign-up: salt Generate a random salt, typically 16 bytes, new for every password.
- Stretch Run password and salt through a slow function: many iterations, or a lot of memory, or both.
- Store with its settings Keep algorithm, cost, salt and hash together, for example $argon2id$v=19$m=19456,t=2,p=1$salt$hash.
- At sign-in: recompute Read the settings from the stored value, hash the typed password the same way, compare in constant time.
- Upgrade quietly If the stored hash used weaker settings than today's, save a new one while the password is in hand: rehash on login.
- PBKDF2 Many iterations of HMAC (RFC 8018).; Standard and FIPS-approved; Built into .NET and many other platforms; Only CPU-hard: graphics cards crack it faster than memory-hard functions; OWASP asks for 600,000 iterations with SHA-256, or 210,000 with SHA-512
- bcrypt Since 1999; each step of the cost factor doubles the work.; Battle-tested and available everywhere; Some resistance to graphics cards; Ignores everything after 72 bytes of input; Small, fixed memory use
- scrypt Memory-hard: every guess needs a lot of RAM.; Expensive for graphics cards and custom chips; Parameters are easy to get wrong; Fewer libraries
- Argon2id Winner of the Password Hashing Competition, RFC 9106.; Memory-hard, tunable in time, memory and parallelism; OWASP's first recommendation; Not built into .NET; needs a library; Its memory cost limits how many sign-ins a server runs at once
- Length over symbols: a long passphrase beats P@ssw0rd!. Revision 4 asks for at least 15 characters when a password is the only factor, and 8 when a second factor follows.
- No composition rules: forcing one capital and one digit makes people choose predictable patterns.
- No forced changes on a schedule - only when there is a sign of compromise.
- Check a breached-password list and refuse passwords attackers already know.
- Allow paste and password managers: they make long, unique passwords practical.
- A good service applies these rules: a length-first minimum with no composition rules, a breached-password check, and a slow hash whose settings are reviewed and upgraded over time.
Breached-password checks with k-anonymity
Attackers start with the passwords that leaked from other sites. Have I Been Pwned publishes hundreds of millions of them through the Pwned Passwords range API, which you can ask without sending the password - or even its full hash. The trick is called k-anonymity: the service learns only a bucket shared by hundreds of known hashes and countless possible passwords.
- Hash locally SHA-1 of the password, in hex: 40 characters. SHA-1 is fine here - it is a lookup key, not storage.
- Send five characters Only the first 5 characters leave the server: one of about a million buckets.
- Receive the bucket The service returns every breached suffix with that prefix and how often it was seen, padded with fake rows so the size of the answer says nothing.
- Match locally Look for your 35-character suffix. Found means breached: refuse the password with a friendly explanation.
{{brand}} can run this check wherever a password is set - at sign-up and at a password reset - as an extra rule next to the length rule, when the deployment enables it. An outage of the outside service never blocks a sign-up.
Account enumeration and password guessing
Account enumeration is learning which email addresses have accounts - the first step of a targeted attack. It leaks through words (no account with this email), through timing (an unknown address answers faster because no hash runs) and through side doors such as sign-up and password reset.
- Back off on repeated failures The wait grows with each failure and is keyed on the device, so the owner on another device is not affected.
- Count per account too Credential stuffing spreads one account's guesses over thousands of addresses, so a sustained run of failures should ask for a human check (a CAPTCHA-style challenge).
- Rate-limit by client address Every sign-in route gets a rate limit per client address.
- Prefer throttling to lockout A lockout lets anyone lock anyone out - a denial of service - and proves the account exists.
- Throttle second-step codes the same way A stolen password alone should not be enough to guess the code.
Authenticator codes: how TOTP works (RFC 6238)
TOTP is how Google Authenticator, Microsoft Authenticator, 1Password and the rest make a new six-digit code every 30 seconds. The phone and the server share one secret; each computes the code from that secret and the current time, so no network is needed. TOTP is HOTP (RFC 4226) with the counter replaced by the clock.
- The server makes a secret 20 random bytes (160 bits, as RFC 4226 recommends). Unlike a password it must be read back, so a good server stores it encrypted, not hashed.
- Show it as a QR code The secret in base32 inside an otpauth address, for example otpauth://totp/Example:[email protected]?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ&issuer=Example&digits=6&period=30.
- Confirm with a first code Two-step turns on only after one correct code - proof that the app really holds the secret.
- Hand out recovery codes A set of one-use codes, shown once, for the day the phone is lost.
A QR code carries the otpauth address from the screen to the phone, so nobody types 32 characters. Inside, it is a grid of dark and light modules. The text becomes bytes, the bytes are split into blocks, and Reed-Solomon error correction is added, so the code scans even when part of it is scuffed. There are four levels: L, M, Q and H recover about 7, 15, 25 and 30 percent. Three big corner squares, the finder patterns, tell the camera where the code is and how it is turned, and a mask is chosen that avoids long runs of one colour. The version (1 to 40, from 21 by 21 to 177 by 177 modules) is simply the grid size: the smallest one that holds the text at the chosen level.
qrcode-generator was chosen because it is small, has no dependencies and gives you the grid itself: you decide how to draw it, so the page can build its own SVG instead of inserting markup a library produced. Heavier alternatives such as the qrcode package render to canvas, terminal or image for you, and qr-code-styling adds logos and rounded dots - useful for marketing, but decoration lowers scan reliability. Generating on the server (for example with QRCoder in .NET) also works, but then an image carrying the secret travels over the network. {{brand}} draws the code in the browser from the address its own server sent. The trade-off: you write the drawing code yourself, and you must handle a text too long for any version (show the typed key instead).
Never draw the secret with someone else's QR service Some tutorials build the picture from a web address such as qr.example/?data=otpauth://... The secret then travels to a third party and lands in their logs and caches. Draw the code locally, keep it dark on light in every theme (an inverted code often fails to scan), and show the text key beside it for people who cannot scan.
- Count time steps T = floor(unix seconds / 30). At 59 seconds after 1970, T is 1.
- HMAC the counter HMAC-SHA1(secret, T as 8 bytes, big-endian) gives 20 bytes.
- Dynamic truncation The last 4 bits pick an offset; read 4 bytes there and drop the top bit.
- Keep six digits That number modulo 1,000,000, padded with leading zeros.
- Long and random, from an alphabet without look-alike characters (no 0 and o, no 1 and l), in short groups that are easy to copy.
- Stored only as hashes and shown once; nobody, support included, can see them again.
- Each works once; the person can make a fresh set at any time, which voids the old one.
- Forgiving to type: case, spaces and dashes do not matter.
TOTP can still be phished A fake sign-in page can ask for your code and use it within the same 30 seconds. TOTP defeats leaked and reused passwords, not real-time phishing. Passkeys defeat both.
Passkeys and WebAuthn, step by step
A passkey is a key pair that your device makes for one website. The private key never leaves the device or your synced password manager; the site stores only the public key. To sign in, the site sends a random challenge, the device asks for your fingerprint, face or PIN, and signs it. The browser standard is WebAuthn (W3C); with CTAP for external security keys, the family is called FIDO2.
- Relying party (RP) The website that asks for the passkey.
- RP ID The domain a passkey belongs to, such as example.com. The browser offers it only there and on subdomains.
- Authenticator What holds the key: a phone, a laptop's secure chip, a password manager or a USB security key.
- Challenge Random bytes from the server, used once, so an old answer cannot be replayed.
- Attestation The answer at registration: the new public key and the authenticator's data.
- Assertion The answer at sign-in: a signature over the challenge.
- User verification (UV) The device checked you - fingerprint, face or PIN - not only that someone touched it (user presence, UP).
- Discoverable credential A passkey the device finds without a username, so sign-in starts without typing an email.
- 1. The server sends options A fresh challenge, the RP ID, a user handle (an opaque id, never the email), the allowed algorithms (-7 is ES256, -257 is RS256) and userVerification required.
- 2. navigator.credentials.create() The browser adds the page's real origin; the device asks for your fingerprint, face or PIN.
- 3. The device makes a key pair It returns clientDataJSON (type, challenge, origin) and an attestation object holding the authenticator data and the new public key in COSE format.
- 4. The server verifies The challenge is the one it issued and unused, the origin is its own, the RP ID hash matches, the UP and UV flags are set.
- 5. The server stores Credential id, public key, sign counter and a name the person chose. No secret is stored at all.
- 1. The server sends a challenge With the RP ID; for a discoverable passkey no username is needed.
- 2. navigator.credentials.get() The device lists its passkeys for this RP ID and asks you to unlock one.
- 3. The device signs signature = sign(privateKey, authenticatorData + SHA-256(clientDataJSON)).
- 4. The server verifies It finds the credential, checks challenge, origin, RP ID hash and flags, verifies the signature with the stored public key and updates the counter.
- 5. A session starts Exactly as after a password and a second step.
Why passkeys cannot be phished The browser, not the person, writes the origin into clientDataJSON, and the device signs over the RP ID's hash. A look-alike site at examp1e.com cannot get a signature for example.com, and there is no code to type into the wrong page. A database leak gives attackers only public keys.
{{brand}} uses the passkey support built into ASP.NET Core Identity in .NET 10. Whatever library you use, enforce the same rules: bind each ceremony to one purpose and one person, accept it once and briefly, require user verification, and accept only your own origins.
ASP.NET Core Identity: what the framework gives you
ASP.NET Core Identity is .NET's membership library: users, password hashing, lockout, tokens for email links, two-factor plumbing and - since .NET 10 - passkeys, stored through EF Core. It is not a sign-in server: to issue OpenID Connect tokens to other apps you add one, such as Duende IdentityServer or OpenIddict, or use a provider such as Entra ID.
{{brand}} builds on ASP.NET Core Identity for users, link tokens and passkeys, and signs its own session token, so one token format serves the browser and other clients. Authenticator codes use a small RFC 6238 implementation.
Try it yourself: sign and break a JWT, then make a TOTP
All you need is Node.js 20 or newer. In ten minutes you will see why the signature matters, why signed is not secret, and that a six-digit code is plain arithmetic.
- Make a key 32 random bytes - never a word you could guess.
- Sign a token base64url the header and the payload, HMAC-SHA256 them, join the three parts with dots.
- Verify it Check alg, recompute the signature, compare in constant time, check exp.
- Tamper with it Change sub to admin and keep the old signature: verification fails.
- Read it without the key Decode the middle part with Buffer.from(part, "base64url") - proof that signed is not secret.
- Make a TOTP code Run totp.mjs below: it prints 94287082 for the RFC 6238 test vector, then the current code for a base32 secret. Add the same secret to an authenticator app and watch both agree.
- Inspect a cookie Sign in to any site, open DevTools > Application > Cookies and note which cookies have HttpOnly, Secure and SameSite. Then type document.cookie in the console and see which ones are missing.
Common mistakes
- Session token in localStorage: one XSS bug or one compromised npm package reads it. Keep browser sessions in an HttpOnly cookie.
- Trusting the alg header: pin the algorithm per key and require signed tokens; never accept none.
- Roles inside a long-lived token: a removed permission keeps working until expiry. Put identity in the token and read permissions on each request.
- No way to revoke: a JWT with no deny list, session check or security stamp survives sign-out and a password reset.
- Fast hashes for passwords: SHA-256, even salted, falls at billions of guesses per second. Use Argon2id, scrypt, bcrypt or PBKDF2 with a high cost.
- Telling attackers who has an account: different messages, different timing or a locked-account message all leak it. One answer, one hash, throttling instead of lockout.
Interview questions
- What is the difference between authentication and authorization? Authentication proves who is calling; authorization decides what they may do. A failed authentication is 401; a failed authorization is 403, or 404 when even the record's existence must stay hidden.
- What is the difference between XSS and CSRF, and does HttpOnly stop XSS? XSS runs an attacker's script inside your page; CSRF makes the victim's browser send a request to your site with its cookie. HttpOnly stops the script from reading the cookie, but the script can still send requests from the page, so you must also escape output and set a Content-Security-Policy. CSRF is answered by SameSite, an origin check and a custom header or token.
- HS256 or RS256 - how do you choose? HS256 uses one shared secret, so whoever can check can also forge; that is fine when one service issues and checks its own tokens. RS256 or ES256 lets other services verify with a public key they cannot sign with - the choice for identity providers and many services.
- How do you validate a JWT? Pin the algorithm, pick the key by kid, verify the signature in constant time, check exp and nbf with a small skew, and check iss and aud. Then ask the application whether the token is on a deny list and whether its session is still live.
- Where should a single-page app keep its session token? In an HttpOnly, Secure, SameSite cookie, ideally with the __Host- prefix, so scripts cannot read it. Then defend writes against CSRF with an origin check and a custom header or a CSRF token. localStorage is readable by any script on the page, which is why a cookie is safer, at the price of needing CSRF defences.
- How do you revoke a JWT before it expires? Keep a deny list of token ids until they would expire, tie each token to a server-side session row, and carry a fingerprint of the account's security stamp so a credential change fails every older token. The other common answer is a short access token plus a rotating refresh token that the server can refuse.
- How should passwords be stored? With a slow, salted, versioned password hash - Argon2id, scrypt, bcrypt or PBKDF2 with a high cost - and rehashed on login when the settings improve. Add a breached-password check and NIST's length-over-complexity rules.
- What is account enumeration and how do you prevent it? Learning which emails have accounts from messages, timing, sign-up or lockouts. Give one answer for a wrong email and a wrong password, spend the same hashing time for unknown addresses, and throttle instead of locking accounts.
- How does TOTP work? Phone and server share a secret; each computes HMAC-SHA1 of the current 30-second step, truncates it and keeps six digits. The server allows one step of drift, refuses a step already used and throttles wrong guesses.
- Why are passkeys phishing-resistant? The browser writes the real origin into the signed data and the device signs only for the RP ID the passkey was made for, so a look-alike site gets nothing usable. The site stores only a public key, so a leaked database gives no way to sign in.
Related
- /help/technology/sso
- /help/technology/access
- /help/technology/request-path