The problem: who is asking, and may they?
An API on the internet gets requests from anyone, and any of them can say DELETE /notes/42. So it must answer two questions. Authentication: who is calling? A password, a session, a token or an API key proves it. Authorization: what may that caller do? May Ana open note 7, which is Ben's? The second needs the answer to the first.
The status codes follow the split. 401 Unauthorized means the server doesn't know who you are (no token, a bad one, an expired one): sign in again. 403 Forbidden means it knows you and the answer is no. A common design checks identity once, at the API gateway in front of every service, and leaves "who owns note 7?" to the service that owns the data.
Passwords: checked once, stored as a slow, salted hash
The auth service never stores a password, nor an encrypted copy that a stolen key would unlock. It stores a slow, salted hash from a function built for passwords, such as bcrypt, scrypt or Argon2. The salt, random per user, makes equal passwords hash differently; the slowness makes every guess against a stolen table expensive. A fast hash like SHA-256 is the wrong tool: one graphics card tries billions a second. Because the check is slow on purpose, it happens once, at sign-in.
Sessions: a random id the server looks up
At sign-in the auth service makes a long random id, saves id → Ana in a shared session store (often an in-memory store such as Redis) and sends the id in a cookie marked HttpOnly and Secure. On every request the gateway looks the id up; a cookie that just said user=ana could be edited to say user=ben.
The lookup is cheap: one round trip inside the data center, about 0.5 ms, and one cache server answers about 100,000 lookups a second. A session is also easy to cancel: signing out, or a stolen laptop, means deleting one row.
Signed tokens: no lookup at all
A signed token carries the answer itself: who the user is and when the token expires, signed with a key only the servers have. The common format is the JWT (JSON Web Token): a header, the claims and a signature, base64-encoded. The idea with a toy token and HMAC-SHA256, keeping the first 8 characters of each signature:
import hmac, hashlib
KEY = b"notes-signing-key" # only the servers know it
def sign(claims):
return hmac.new(KEY, claims.encode(), hashlib.sha256).hexdigest()[:8]
print(sign("user=ana;expires=May 31")) # e70efcbd, sent with the token
print(sign("user=ben;expires=May 31")) # f2807a11: an edited token fails
Any server with the key checks a token without asking anyone; an edited one gets a 401. A token is signed, not hidden: whoever holds it can read it, so it never carries secrets. With many services, a public-key signature (RS256 or ES256) lets each check tokens without holding the key that makes them.
The catch: a token can't be taken back
If tokens last 30 days and Ana's laptop is stolen on May 4, the thief's copy works for 27 more days: the gateway checks only the signature and the date, and the server stored nothing to delete. Changing the signing key stops it, but signs out every user at once.
The fix is a short expiry plus a refresh token. The access token lasts about 15 minutes; a long-lived refresh token, which the auth service records (in effect a session), gets a new one. Signing out deletes the refresh token, so a stolen token works for 15 minutes at most. The lookup is back, but once every 15 minutes instead of on every request. Ana's morning:
time request the check answer
10:00 sign in password matches the hash token until 10:15 + refresh token
10:10 GET /notes signature ok, before 10:15 200 OK
10:20 GET /notes signature ok, after 10:15 401 Unauthorized
10:20 refresh token found in the session store new token until 10:35
10:20 GET /notes signature ok, before 10:35 200 OK
Both are good interview answers: sessions suit one web app; short tokens suit many services or regions, which can each check them alone.
API keys: how programs prove who they are
A partner's backup service calling your API every night has nobody to type a password. It gets an API key: a long random secret, say 32 random bytes (256 bits), made for that partner alone. Treat it like a password a program holds:
- Send it in a header, never in the URL. URLs end up in server and proxy logs and browser history; request logs usually leave headers out.
- Store only its hash. A long random key has no short word to guess, so a fast hash like SHA-256 is enough. The gateway hashes the key it gets and looks up whose it is.
- Show it once. You kept only the hash, so a partner who loses it gets a new key.
GET /notes HTTP/1.1
Host: api.notes.example
Authorization: Bearer nk_QhTzRbWmXcVpLdKsJfGy
Scopes: least privilege
Each key carries scopes such as notes:read and notes:write. A backup service only reads; a cleanup tool that deletes old notes needs notes:write too. A call outside the key's scopes gets a 403. Giving each key only what it needs, least privilege, limits what a leaked key or a buggy partner can do.
Sign in with Google: OAuth in brief
A "Sign in with Google" button sends Ana to Google's own page, where she signs in and approves a scope: her name and email. Google sends her back with a code, which your auth service exchanges with Google for a token saying who she is. Your app never sees her Google password and can't reach anything she didn't approve. That flow is OAuth 2.0, with OpenID Connect adding the "who she is" part.
Rotating a key without downtime
Keys get changed on a schedule and when someone who knew one leaves. The partner calls all day, so during the change the gateway accepts two keys for it:
- Create a new key; the old one keeps working.
- Deploy the new key to the partner's servers, next to the old one.
- Switch the partner's requests to the new key.
- Watch the old key's requests in your logs drop to zero (a forgotten server shows up here).
- Revoke the old key.
When a key leaks, say into a public repository, skip the overlap: revoke it at once and accept a short outage, issue a new one, and search the logs for every request made with the leaked key.
Where it shows up in design interviews
Almost every design has users or partners calling an API. Name three things: how each client proves who it is (users sign in once and get a session, or short-lived signed tokens plus a refresh token; partners send API keys with scopes), where that's checked (the API gateway, once, before any service), and that every secret is hashed and can be rotated or revoked.
Common interview questions
Authentication or authorization: what's the difference?
Authentication proves who is calling; authorization decides what that caller may do. A failed authentication is a 401, a failed authorization a 403.
Sessions or JWTs?
Sessions cost a lookup per request but can be canceled at once. JWTs need no lookup but stay valid until they expire, so keep them to about 15 minutes and renew them with a refresh token the server can delete.
How do you store passwords and API keys?
Passwords as a slow, salted hash, never in plain text. API keys as a hash too, shown to the partner once.
How do you rotate an API key without downtime?
Let the gateway accept two keys: create, deploy next to the old one, switch, wait until the old key's requests reach zero, revoke. A leaked key is revoked at once.
Summary
- Authentication is who you are (401), authorization what you may do (403).
- Passwords are checked once, against a slow, salted hash.
- Sessions are looked up and easy to cancel; signed tokens skip the lookup, last about 15 minutes, and a refresh token renews them.
- API keys are long, random, sent in a header, stored hashed, shown once and limited by scopes.
- Rotate a key with an overlap; revoke a leaked one at once.