System designCore11 min
Authentication & authorization
Authentication proves who is calling; authorization decides what they may do. They are different questions with different failure modes, and the second is where most real breaches happen: a logged-in user changes an ID in a URL and reads someone else's data.
1 · Two questions
Authentication asks who you are; authorization asks what you may do
A school platform has students, teachers and parents. Students see their homework and grades; teachers enter grades for their classes; parents see their own children's grades. Every request answers two separate questions:
Aspect | Authentication | Authorization |
|---|---|---|
Question | Who is making this request? | May this caller do this, to this record? |
Answered by | Password, passkey, second factor, a single sign-on provider | Roles, attributes and relationships, checked against the record |
Answered when | Once at login; then a session or token carries the answer | On every request, for every record touched |
If it fails, HTTP says | 401: no valid credentials | 403: known caller, not allowed |
Both matter, but they fail differently. Authentication failures are usually loud: credential stuffing, phishing, stolen sessions. Authorization failures are quiet: the request is from a real, logged-in user, and the server simply forgets to check that the record belongs to them. In the 2025 OWASP Top 10, broken access control is ranked first, and every application in its data set had some form of it; authentication failures rank seventh.
This page follows a request from login to the data: storing passwords, remembering a login with sessions or tokens, delegating login and access with OAuth and OpenID Connect, deciding permissions, and enforcing them on every record. Encrypting the connections and the stored data is Encryption in transit & at rest.
2 · Passwords
Store passwords as slow salted hashes, and ask for length, not complexity
Passwords are never stored, and never stored encrypted: anything that can be decrypted can be stolen and decrypted. The platform stores a hash, a one-way function of the password, and at login hashes the attempt and compares. Two details make that safe:
- A salt, a random value stored per user and mixed into the hash, so identical passwords produce different hashes and precomputed tables are useless.
- A deliberately slow algorithm. Fast hashes like SHA-256 let an attacker with a stolen table test enormous numbers of guesses per second. A password hash is designed to cost time and memory on every guess.
Algorithm | Recommended minimum settings | Note |
|---|---|---|
Argon2id | 19 MiB of memory, 2 iterations, 1 degree of parallelism | First choice; memory-hard, which raises the cost of every guess |
scrypt | N = 2¹⁷ (128 MiB), r = 8, p = 1 | Also memory-hard |
bcrypt | Work factor 10 or more | Older systems; uses only the first 72 bytes of a password |
PBKDF2 | 600,000 iterations of HMAC-SHA-256 | When FIPS-140 compliance is required |
With PBKDF2 at 600,000 iterations, every guess costs 600,000 HMAC computations instead of one. A login pays that once, in a fraction of a second; an attacker pays it for every guess in a dictionary.
The rules users see changed too. Current guidance:
- Length: at least 15 characters when a password is the only factor, 8 when it is one of several; accept at least 64.
- No composition rules ("one digit, one symbol") and no forced periodic changes, only a change when there is evidence of compromise.
- Block known passwords: compare new passwords against lists from earlier breaches and common choices.
The strongest step is a second factor, and the strongest factors are phishing-resistant: passkeys (WebAuthn) are bound to the site's real domain, so a look-alike phishing site can't use them.
3 · Staying logged in
A session is easy to revoke; a token is easy to verify
After login, the platform must recognize the user on every later request without asking for the password again. Two designs dominate:
- A server-side session: the server stores "session 7f3a… belongs to teacher 1042" and gives the browser only that random ID in a cookie.
- A self-contained token, usually a JWT (JSON Web Token): the server signs a small document of claims (who, which roles, until when) and the client sends it back; any service with the key can check the signature without a lookup.
The readable middle part of a JWT
{
"iss": "https://login.school.example",
"sub": "teacher-1042",
"aud": "grades-api",
"roles": ["teacher"],
"exp": 1790950800
}Anyone holding a signed JWT can read it; the signature prevents changes, not reading, so it must hold nothing secret. A service that accepts it must check the signature with an algorithm it chose itself (never the one the token names, which an attacker can set to none), and check iss, aud and exp.
Aspect | Server-side session | Signed token (JWT) |
|---|---|---|
Checking a request | Look up the session ID in a shared store | Verify a signature; no lookup |
Logging someone out now | Delete the session: instant | Hard: the token is valid until it expires |
Stolen credential | Revoke it | Usable until expiry, so keep expiry short |
Best fit | Browser apps talking to their own backend | Many services or third-party APIs verifying the same caller |
Most systems combine them. Access tokens live minutes, and a longer-lived refresh token gets new ones and can be revoked centrally. Browsers keep session IDs and tokens in cookies marked Secure, HttpOnly and SameSite, never in local storage that any injected script can read. Session IDs need at least 64 bits of randomness and must change at login, so an attacker can't plant one beforehand (Stateless services & sessions).
Refresh tokens are long-lived, so they are worth stealing. With refresh token rotation, every refresh returns a new refresh token and invalidates the old one. If a stolen token is used, either the thief or the real client later presents one that is already invalidated; the server can't tell which, so it revokes the active refresh token and the user must sign in again.
4 · Delegation
OAuth delegates access; OpenID Connect adds who logged in
The school wants parents to sign in with accounts they already have, and a homework-planner app to read a student's assignments without ever seeing the student's password. Both use OAuth 2.0, which lets a user grant an application limited access through an authorization server that issues tokens. The recommended flow is the authorization code flow with PKCE:
The app never sees the password, and the token allows only what the student approved. PKCE ties the code to the app that started the flow: a stolen code is useless without the verifier, which never left the app.
Two older OAuth shortcuts should no longer be used: no implicit grant, which returned tokens in the browser address, and no password grant, where the app collects the user's password itself. PKCE is mandatory for apps that can't keep a secret, such as mobile and single-page apps.
OAuth alone answers "what may this app do", not "who is the user". OpenID Connect adds that: alongside the access token, the login server returns an ID token, a signed JWT saying who logged in (sub), at which provider (iss), for which app (aud). "Sign in with" buttons use it. Treating an access token as proof of login is a known mistake: a token issued to another app says nothing about who logged into yours.
5 · Permissions
Roles get you started; real rules depend on relationships
Now the platform must decide what each user may do. The simplest model is role-based access control (RBAC): users get roles, roles get permissions. "Teachers may edit grades" is one line. But the real rule is "teachers may edit grades of students in the classes they teach, until the term closes", and roles alone can't express which class or which term. Three models, each adding what the previous one can't say:
Model | A decision uses | School rule it expresses | Gets hard when |
|---|---|---|---|
RBAC | The user's roles | Teachers may edit grades | Rules depend on which record: roles multiply per class |
ABAC (attributes) | Attributes of user, record, action and context | Grades are editable only while the term is open | Policies sprawl and become hard to audit |
ReBAC (relationships) | A graph of who relates to what | A parent may see grades of their own children | The graph must be stored, kept current and queried fast |
"May Dana read this grade?" becomes "is there a path parent of → owns?". "May Ito edit it?" becomes "does Ito teach a class Leo is enrolled in?". Adding a parent or a class is a new edge, not a new role.
Google's Zanzibar is the best-known large relationship-based system: it stores permissions as relationships, and handles trillions of access-control entries and millions of checks a second, 95% of them in under 10 ms. Most applications combine all three: roles for coarse permissions, relationships for ownership and sharing, attributes for conditions like time.
6 · Enforcement
Check every record, on the server, on every request
Dana is logged in and opens Leo's grades at /students/48213/grades. She changes the number to 48214 and presses Enter. If the server only checks that the caller is a logged-in parent, Dana now sees another child's grades. This is broken object-level authorization, also called an insecure direct object reference, which OWASP ranks first among API security risks:
The gateway can say who Dana is. Only the grades service, which knows whose grades these are, can say whether Dana may see them, so that check must live there.
The rules that prevent it:
- Deny by default. Every endpoint is closed unless a rule allows it.
- Check the record, not just the role, in every function that takes an ID from the client, including writes and deletes.
- Enforce on the server. Hiding a button in the app protects nothing; the API is reachable directly.
- Authenticate at the edge, authorize near the data. The gateway verifies tokens once (Reverse proxies & API gateways); each service decides on its own records, possibly by asking a central policy service.
- Random IDs help, but don't fix it. Unguessable IDs slow an attacker down; only the check stops one.
Finally, keep permissions small and visible. Give each token the narrowest scope it needs, log every denied request, and alert on bursts of them: a user receiving hundreds of 403s on sequential IDs is someone probing for exactly this flaw.
Check yourself
5 questions
Takeaways
Remember this
- Authentication proves who is calling; authorization decides, per request and per record, what they may do. Broken access control is the most common serious web flaw.
- Store passwords as salted, slow hashes (Argon2id first); require length, block breached passwords, and prefer phishing-resistant second factors such as passkeys.
- Server-side sessions are easy to revoke; signed tokens are easy to verify but live until they expire, so keep them short and pair them with refresh tokens.
- OAuth delegates limited access with the authorization code flow and PKCE; OpenID Connect's ID token says who logged in.
- Use roles for coarse rules and relationships or attributes for real ones, and check every record on the server near the data.