Reliability & Operations · lesson 9 of 9 · about 30 min
Security basics: authN, authZ, TLS, JWT, OAuth
The security basics every design review expects: storing passwords so a leak is survivable, credential stuffing and multi-factor authentication, sessions versus JWTs and their revocation trade-off, how OAuth and OpenID Connect delegate login and which small settings close the big holes, why authorisation must be checked on every request, and why certificates must renew themselves. Tap any for a plain-language card.
The database leaks. How long do the passwords survive?
The table shows how long an attacker with your leaked hashes needs, on a rig of the size you choose. The speeds are rounded orders of magnitude from public GPU benchmarks. The big caveat is that people do not choose random strings: a human password usually falls to a list of common passwords in minutes whatever the hash, so the slow hash buys time mostly for strong passwords.
Compare plain SHA-256 with bcrypt for the same 8-character password. Then switch to “human” and see the common-password list fall quickly.
Guesses per second per card2.0e+10
Average time to crack9 minutes
Same password “hunter2”
Stored hash (first 12 bytes)
salt a91f
daa09086e4ef5dc4f7a77868
salt 7c04
25c53bb47c18f8410a623b37
A random 8-character password of letters and digits, hashed with Plain SHA-256 (fast), falls to 10 graphics cards in about 9 minutes. That is within reach of any attacker. The two stored hashes differ because of their salts, even though the password is the same.
Rates per GPU (approximate, illustrative): SHA-256 2×10¹⁰/s, bcrypt cost 12 about 1.4×10³/s, Argon2id about 8×10²/s. Average time = half the search space ÷ rate. The hashes shown are real SHA-256 of the password plus the salt.
Authentication
Nobody guessed a password. An attacker simply tried the ones that already leaked elsewhere.
Defences stack. makes a stolen password insufficient (a code from an app, a hardware key, a passkey), though SMS codes and fatigue prompts can be bypassed. Add per-account and per-device rate limits, bot detection, checks against known-breached passwords, and anomaly alerts. Passkeys, which replace the password with a cryptographic key pair, remove the reusable secret altogether.
With 10 million leaked pairs and 1% reuse, a hundred thousand accounts fall. Raise MFA adoption to 80% and see how many remain.
No MFA anywhere
100,000 accounts taken
With your MFA adoption
71,500 accounts taken
Pairs that work on your site100,000
Stopped by MFA28,500
Accounts taken over71,500
10,000,000 leaked pairs with 1.0% reuse give the attacker 100,000 working logins. With 30% of accounts on MFA (and 5% of those bypassed) 28,500 are blocked and 71,500 accounts are still taken over.
Hits = pairs × reuse rate; MFA blocks hits × adoption × (1 − bypass rate). A model, not a prediction for any real site.
Tokens
A JWT needs no database lookup. It also can't be un-issued.
The usual compromise is a short-lived access token (minutes) plus a refresh token that can be revoked: a stolen access token is useful only for its remaining lifetime, and the authorisation server sees one refresh per user per lifetime. Shorter lifetimes mean smaller exposure and more refresh traffic. Move the lifetime and see both sides.
At 15 minutes with 100,000 users there are about 111 refreshes a second. At one minute it is 1,667 a second, but a stolen token dies in a minute.
lifetime 1 min
1,667 refreshes/s · exposed 1 min
lifetime 5 min
333 refreshes/s · exposed 5 min
lifetime 15 min
111 refreshes/s · exposed 15 min
lifetime 1.0 hours
28 refreshes/s · exposed 1.0 hours
lifetime 1.0 days
1 refreshes/s · exposed 1.0 days
A stolen access token works for up to15 min
Refreshes per second at the auth server111
A session store would serve10,000 lookups/s
With 15 min access tokens, a stolen token is usable for up to 15 min, and 100,000 active users cause about 111 refreshes a second. Session ids would allow instant revocation but cost 10,000 store lookups a second on every request path.
Refreshes per second = users ÷ (lifetime in seconds), assuming everyone is active all the time (an upper bound). Lookups = users × requests per minute ÷ 60.
OAuth and OpenID Connect
“Log in with Google” lets an app act for you without ever learning your password.
Three small details close three large holes. PKCE stops anyone who intercepts the code from redeeming it. The state parameter stops an attacker from tricking your browser into finishing their login. Exact-match redirect URIs stop codes and tokens being sent to an attacker's address. Switch each on and see which attacks remain.
Start with everything off and three attacks work. Turn on all three protections.
1.The app sends the browser to the provider with client_id, redirect_uri, state and a PKCE code_challenge.
2.The user logs in at the provider and approves the requested scopes. The app never sees the password.
3.The provider redirects back to redirect_uri with a short-lived authorization code and the same state.
4.The app checks state, then exchanges code + the PKCE code_verifier for an access token (and an ID token).
5.The app calls APIs with the access token. It carries only the scopes the user approved.
WORKSStolen authorization code is redeemed by the attacker
WORKSAttacker logs the victim in to the attacker's account (login CSRF)
WORKSCode or token sent to an attacker-controlled redirect URL
Attacks that still work3 of 3
3 of the three classic attacks still work: stolen authorization code is redeemed by the attacker; attacker logs the victim in to the attacker's account (login csrf); code or token sent to an attacker-controlled redirect url. Each is closed by one inexpensive setting.
Rule-based: each protection is the standard defence for exactly one attack in RFC 6749, RFC 7636 (PKCE) and the OAuth security best practices.
Authorisation
User 1041 changes the number in the URL to 1042 and reads someone else's invoice.
A tempting half-fix is to make ids unguessable (random UUIDs). That makes enumeration hopeless, but it is not authorisation: the first leaked link, log line or shared screenshot hands over access, because the server still never asks whether the requester may see it. The real fix is an ownership or permission check in the data layer (deny by default). Try an attacker walking through ids at the rate you choose.
With sequential ids and no check, ten minutes at 50 requests a second exposes 30,000 records. Switch to UUIDs: nothing is exposed by guessing, but the lesson is the third control.
Requests made30,000
Other people's records exposed by guessing30,000
If one id leaks (a shared link, a log line)readable by anyone
With sequential ids and no ownership check the attacker simply counts upward: 30,000 requests expose 30,000 records.
Sequential ids: exposed = min(records, requests). UUID v4: 122 random bits, expected hits = requests × records ÷ 2¹²². With an ownership check nothing is exposed.
Which control?
Six threats, four controls.
Hashing protects passwords at rest, MFA protects against stolen passwords, TLS protects data in transit, and authorisation checks protect objects from the wrong user. Each answer is worth 20 XP.
Ask: what is being attacked: the stored secret, the login, the wire, or the object?
Question 1 / 6
The user table has leaked. Make cracking the passwords as hard as possible.
Tip: press 1 to 4 to answer, Enter for the next one.
Who lives by this
From LinkedIn's hashes to Let's Encrypt.
Four published incidents and standards.
Unsalted hashesLinkedIn (2012)
a breach of password hashes
In 2012 millions of LinkedIn password hashes were posted online; they were unsalted SHA-1, so common passwords were recovered in bulk with precomputed tables and fast hardware. The incident is a standard example for why passwords need salts and a slow hash.
A bug in the wireHeartbleed (2014)
an OpenSSL TLS heartbeat flaw
Heartbleed, CVE-2014-0160, was a missing bounds check in OpenSSL's TLS heartbeat extension that let a client read up to 64 KB of the server's memory per request, potentially exposing private keys and passwords. It showed that TLS protects the wire only as well as its implementation.
Free, automated certificatesLet's Encrypt (2015)
ACME and 90-day certificates
Let's Encrypt issues free certificates automatically through the ACME protocol, with a 90-day lifetime chosen to push people toward automation. It helped move most of the web to HTTPS.
The top riskOWASP Top 10 (2021)
A01: Broken Access Control
OWASP's 2021 list of the most critical web application risks put Broken Access Control first, meaning failures to enforce what an authenticated user may do, such as reading other users' records by changing an identifier.
Interview angle
What interviewers actually ask.
Six questions, each worth 20 XP. Security basics come up in almost every design review, usually as 'how do you authenticate and authorise this?'.
Separate authentication from authorisation, store passwords with a salted slow hash, use short-lived tokens, and always check ownership on the server.
Question 1 / 6
Why add a salt to a password hash?
Tip: press 1 to 3 to answer, Enter for the next one.
Break it
Nothing was deployed. At midnight the API started failing for every client: the certificate had expired.
The cure is not a better reminder; it is automation. With the ACME protocol (used by Let's Encrypt, which issues free 90-day certificates) a renewal job runs by itself weeks before expiry and deploys the new certificate, and monitoring alerts on certificates that will expire within a few weeks. Shorter lifetimes then become a benefit, because a process exercised every 60 days stays working. Set your fleet below.
With 200 certificates renewed by hand and 2% missed, you get about 4 expiry outages a year. Shorten the lifetime without automating and it gets worse; automate and it nearly disappears.
365-day certificates
4.0 outages a year
90-day certificates
16.2 outages a year
30-day certificates
48.7 outages a year
Renewals to perform each year200
Expected expiry outages a year4.0
200 certificates lasting 365 days mean 200 manual renewals a year. Missing 2% of them is about 4.0 expiry outages a year, each a total failure for that endpoint at a moment nobody chose.
Renewals per year = certificates × 365 ÷ lifetime; outages = renewals × miss probability. The automated figure (0.1%) is an assumption, not a measurement.