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

json
{
  "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

Parent: DanaStudent: LeoClass 7BTeacher: ItoLeo's math gradeparent ofenrolled inteachesownsParent: DanaStudent: LeoClass 7BTeacher: ItoLeo's math gradeparent ofenrolled inteachesowns

"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

1. A logged-in parent changes the student ID in a URL and sees another child's grades. What failed?
2. Why should passwords be hashed with Argon2id or bcrypt rather than SHA-256?
3. A teacher leaves the school today. The platform uses JWT access tokens valid for 24 hours, with no other checks. What is the problem?
4. Which OAuth flow should a new mobile app use to get an access token?
5. Where should the check "may this parent see this student's grades" happen?

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.