Express course · No. 08
Two questions hide behind one word, 'auth.' Authentication proves who you are; authorization decides what you're allowed to do. Confuse them, skip either, or trust the client, and the door is open.
Essence only · One picture per idea · Standards over DIY
Behind the word "auth" hide two different questions. Most security bugs come from confusing them — or from answering one and forgetting the other.
Authentication: who are you?
Showing your passport at the airport — proving you really are the person you claim to be.
Authentication (authN) is proving identity: a login, a passkey, a token that says "this really is Alice." It usually happens once per session. Get it wrong and an impostor walks in as someone else. But proving who you are says nothing about what you may do — that's the other question entirely.
Authorization: what may you do?
A hotel keycard — it proves nothing about your name, but it opens your room and not the others, and not the manager's office.
Authorization (authZ) is deciding what an identity is allowed to do: read this, edit that, never touch the admin panel. Unlike authN, it has to be checked on every request, because one logged-in user may touch a hundred different things. Knowing who someone is is not permission to do anything in particular.
Two layers — never collapse them
A nightclub: the bouncer checks your ID at the door once, and the bartender still checks you're of age every time you order. Two checks, two moments.
The classic mistake is fusing the two — proving identity once and trusting that forever, or stuffing every permission into a login token and never re-checking. AuthN happens at the door; authZ happens at every door inside. Keep them separate, and check the second one constantly.
Never roll your own
You don't weld your own bank vault from sheet metal in the garage — you buy one built and tested by people who do nothing else.
Identity is the foundation everything else stands on, and it's brutally easy to get subtly wrong. Use proven standards and libraries — established protocols, a battle-tested auth library or provider — not a clever scheme you invented. The history of security is paved with home-made auth that looked fine until it suddenly wasn't.
Authentication is the door. Authorization is every door inside. Skip the second and the first means nothing.
Authentication comes down to proving you're you. The factors range from the weak-but-universal password to the modern, phishing-resistant passkey.
Passwords: the weak default
A secret word shared with the doorman — fine until someone overhears it, guesses it, or you use the same one everywhere.
Passwords are universal and the weakest common factor: reused, guessable, phishable. If you must use them, the non-negotiable rule is never store them as text — store a slow, salted hash (Argon2, bcrypt), so a stolen database doesn't hand over everyone's password. (More on that in the mistakes chapter.)
Multi-factor: something you know, have, are
A safe-deposit box needs your key and the bank's key. One alone opens nothing.
MFA (two-factor) demands a second proof beyond the password — something you have (a code from an app, a security key) or something you are (a fingerprint). Even a stolen password is then useless alone. An authenticator-app code (TOTP) is solid; SMS codes are weak (interceptable) but better than nothing. MFA is the single biggest, cheapest security win you can add.
Passkeys: the passwordless future
A key that only works at the one door it was cut for — fake the website, copy the lock's photo, and the key simply won't turn.
Passkeys (built on WebAuthn) replace passwords with a cryptographic key pair tied to your device and the real site, so there's nothing to phish, reuse, or leak — and Apple, Google, and Microsoft now default to them. The private key never leaves your device; the site only ever sees a signature. This is where authentication is heading: nothing to steal.
A password is a secret you can lose. A passkey is a proof you can't. Add a second factor either way.
HTTP forgets you between requests, so after you log in the system needs a way to keep recognising you. Two approaches, with a real trade-off.
The problem: HTTP has no memory
A shop where the clerk forgets you the instant you turn around — so you need a ticket to prove, on your next step, that you already paid.
Every HTTP request is independent; the server doesn't remember that you logged in a second ago. So after authentication you're handed something — a cookie or a token — that you send with each request to say "it's still me." Everything below is two ways to do that. (This is the statelessness from the protocols course.)
Sessions: the server remembers
A coat check — you hold a little numbered ticket, and all the real information sits behind the counter under that number.
With sessions, the server stores who you are and hands you a cookie holding only a random session id. Each request, it looks the id up. The server is the source of truth, so logging out or banning someone is instant — just delete the session. The cost is that the server must store and look up that state. The right default for most web apps.
JWT: the token carries the truth
A festival wristband with your access level already printed and tamper-sealed on it — the guard checks the seal, not a guest list.
A JWT is a signed token the client holds that contains the identity and claims; the server just verifies the signature, no lookup needed. That makes it stateless and easy to scale across services — but hard to revoke: a leaked token stays valid until it expires. So you keep lifetimes short and pair it with a refresh token. Great for APIs and microservices; overkill for a simple web app.
Where you keep it matters
A key in a locked drawer versus a key taped to the front door — same key, very different safety.
A token in the browser belongs in an httpOnly cookie (JavaScript can't read it), not in localStorage, where any injected script can steal it. The store is part of the security: get it wrong and one XSS bug hands an attacker every session. How you transport and store the credential is as important as the credential itself.
Sessions: the server remembers, and can forget you instantly. JWT: the token remembers, and you can't easily take it back.
Often you don't want to handle the password at all — you want users to sign in with an account they already have, or one service to act for another. That's what OAuth2 and OIDC are for.
OAuth2: grant access without sharing a password
A valet key that starts the car and opens the door, but not the boot or the glovebox — you hand over limited access, not your whole keyring.
OAuth2 lets a user grant your app limited access to their account on another service — your app reads their Google Calendar — without ever giving you their Google password. The other service hands your app a scoped access token ("read calendar, nothing else"). It's about delegated authorisation, not identity.
OIDC: OAuth2 that also says who you are
The valet key, now with a photo ID clipped to it — not just "what this key opens," but "and here's who it belongs to."
OAuth2 alone tells you what an app may access, not who the user is. OpenID Connect (OIDC) adds a thin identity layer — an ID token with verified claims (this is Alice, here's her email) — on top. This is what powers "Log in with Google" and single sign-on. When you want social login or SSO, OIDC is the standard.
API keys: the simple machine credential
A spare key you give a trusted contractor — no name attached, just "whoever holds this may come in."
An API key is a long secret string that identifies a program, not a person — used for service-to-service and programmatic access. Simple and effective, but it is the credential: anyone who finds it is in. So you scope it, rotate it, and never commit it to git or put it in a URL. Good for machines; not a substitute for real user identity.
OAuth2 borrows access without the password. OIDC adds "and here's who they are." Use the standard — don't invent your own token dance.
Once you know who someone is, you decide what they may touch. The models run from coarse roles to fine-grained policy — and the rule under all of them is deny by default.
Deny by default, least privilege
A new employee's badge that opens nothing until each door is explicitly granted — rather than opening everything until someone remembers to lock a few.
The foundation of authorisation: everything is forbidden unless explicitly allowed, and each identity gets the minimum access it needs, no more. Default-open systems leak — someone always forgets to restrict the new endpoint. Default-closed systems fail safe. Start from "no" and grant deliberately.
RBAC: permissions by role
A theatre where your ticket type — stalls, circle, backstage pass — decides where you can go, without naming you personally.
Role-Based Access Control groups permissions into roles — admin, editor, viewer — and assigns users to roles. Simple, readable, and enough for most apps: "editors can publish, viewers can't." It gets coarse when rules depend on context or on the specific record, which is where the next models come in.
ABAC: permissions by policy
A rule rather than a list — "managers may approve expenses under 10k, in business hours, from a company device" — evaluated fresh each time.
Attribute-Based Access Control decides from attributes of the user, the resource, and the context — role and amount and time and device. Far more flexible than fixed roles, and far more complex. The signal to adopt it: you're stacking more than a couple of custom if conditions onto your role checks.
Ownership: can you touch this record?
You have a valid library card (a role), but that doesn't let you read the notes scribbled in someone else's borrowed book.
The most-missed check: even a valid "editor" must only edit their own documents, not everyone's. The role says "editors may edit"; you still have to verify this editor owns this row. Forgetting this is the single most common access-control bug — change the id in the URL and you're in someone else's data. Always check the object, not just the role.
Deny by default. Grant the least. And always ask not just "what role" but "whose record."
Auth fails in a handful of well-known ways — the same ones, over and over, in app after app. Knowing them is half the defence.
Broken access control: forgetting to check
A building where the front door is guarded but every internal door is unlocked — once you're in, you can walk anywhere.
The number-one web vulnerability: an endpoint authenticates the user but forgets to check they're authorised for what they asked. Its commonest form is IDOR — changing /orders/123 to /orders/124 and seeing someone else's order, because the server never checked ownership. Breaches of millions of records trace to exactly this. Check authZ on every request, server-side, against the actual object.
Storing passwords wrong
Keeping the spare keys to every house on the street in a glass jar on the porch.
Storing passwords as plain text — or with a fast hash like MD5 — means one database leak exposes everyone, and reused passwords compromise their other accounts too. Passwords must be slowly and individually hashed (Argon2, bcrypt) so they're expensive to crack even when stolen. The hash being slow is the point, not a flaw.
Leaking and trusting tokens
Writing the alarm code on a sticky note stuck to the door — the lock is fine; you just handed out the secret.
Tokens and keys leak through URLs (which get logged), browser localStorage (which a script can read), and secrets committed to git. And a token must be verified, not trusted because it showed up — a tampered or expired one must be rejected. Treat every credential as something that will leak someday: short lifetimes, rotation, and never in a place that gets logged or scraped.
Trusting the client
A shop that lets customers write their own price tags and never checks them at the till.
Anything the client sends — a hidden form field, a role in a cookie, an isAdmin=false flag — can be forged. The browser, the app, the request are all under the user's control. Every real check must happen on the server, which the user can't edit. The client is a suggestion; the server is the authority.
Almost every auth breach is one of these: a check that was skipped, a secret stored badly, or the client believed.
Good auth is mostly about using proven tools correctly and checking permissions relentlessly — not cleverness. Here's how to choose.
Use a library or a provider
You don't manufacture your own seatbelts — you fit certified ones designed by specialists and crash-tested for years.
For almost everyone, the right move is to not build auth yourself: use your framework's auth, or a provider (Auth0, Clerk, Supabase, Cognito, and the like) that handles hashing, sessions, MFA, OAuth, and the edge cases you'd never think of. Your job is to wire it in correctly and own the authorisation — the part only you know.
Match the mechanism to the app
A house key, a car key, and a hotel keycard are all "keys" — you pick by the door, not by fashion.
Sessions for a normal web app (simple, instantly revocable); short-lived JWTs plus refresh when you need stateless scale across services; OIDC for social login and SSO; API keys for machine-to-machine. Add MFA everywhere it matters, especially admin accounts. Choose by the shape of your app, not by what sounds advanced.
- Are passwords slowly hashed (Argon2/bcrypt), never stored as text? - Is MFA available, and required for admins? - Is every endpoint checking authZ — the role and the ownership of the object? - Are tokens in httpOnly cookies, short-lived, and never in URLs or git? - Is the real check on the server, never trusting client-sent roles? - Am I using a proven library/provider instead of a hand-rolled scheme?
- Changing an id in the URL shows someone else's data. - A permission lives in a cookie or hidden field the client can edit. - Passwords stored in plain text or MD5. - No way to revoke a logged-in user except waiting for expiry. - No MFA on the accounts that can do the most damage. - You wrote your own token or crypto scheme.
- AuthN and authZ are separate, and authZ is checked on every request. - Every object access verifies this user owns this thing. - Credentials are hashed, scoped, short-lived, and rotatable. - The server is the authority; nothing trusts the client. - You can revoke access and see who did what in a log. - The heavy lifting sits in a proven library or provider, not your own code.
Auth isn't where you get clever. It's where you use standards correctly, deny by default, and never, ever trust the client.