# Auth vs Authz (Sessions, JWT, OAuth)

> Authentication is proving who you are; authorization is what you're allowed to do - and after login, the server keeps you logged in with either a server-side session or a stateless token like a JWT. This guide untangles all of it, plus OAuth and 'Sign in with Google'.


---

# Auth vs Authz (Sessions, JWT, OAuth)

You've wired up a login form, copy-pasted a JWT library, clicked "Sign in with Google" a thousand times - and yet asked to explain the difference between *authentication* and *authorization*, or whether a JWT is encrypted, you'd hedge. That's not a you-problem: the words look almost identical (*authn*, *authz*), and most tutorials hand you working code without showing the moving parts underneath.

This guide fixes that. By the end you'll have a clean mental model for each piece and be able to *reason* about an auth system instead of guessing.

## How to read this

- **Need one specific answer right now?** Jump to the phase that matches: identity vs permissions is [Phase 1](01-authentication-vs-authorization.md), staying logged in is [Phase 2](02-sessions-vs-tokens.md), "Sign in with…" is [Phase 3](03-oauth-and-sign-in-with.md).
- **Want it to finally click for good?** Read in order. Each phase builds on the last - Phase 1 gives you the vocabulary the other two lean on.

## The phases

1. **[Authentication vs Authorization](01-authentication-vs-authorization.md)** - *who you are* (proving identity) versus *what you're allowed to do* (permissions). Two different jobs, both required. The passport-vs-ticket mental model.
2. **[Keeping You Logged In: Sessions vs Tokens](02-sessions-vs-tokens.md)** - after login, the server has to remember you. Server-side sessions versus stateless tokens (JWT), with the plain trade-offs: revocation, size, scaling.
3. **[Delegated Access: OAuth & "Sign in with…"](03-oauth-and-sign-in-with.md)** - how an app gets limited access to your data without your password, the valet-key mental model, access vs refresh tokens, scopes, and how OAuth (authz) differs from OpenID Connect (authn).

> This guide deliberately stops at the concepts and the shapes of things. Picking a specific library, hardening a production login flow, and the deeper cryptography of token signing are their own topics - the goal here is the mental model that makes those next steps make sense. Related reading: [How Passwords Are Stored](/guides/how-passwords-are-stored), [HTTPS & TLS](/guides/https-and-tls), [What an API Is](/guides/what-an-api-is).


---

# Authentication vs Authorization

Two words, almost the same spelling, abbreviated to *authn* and *authz* - which helps nobody. People use them interchangeably, ship the wrong check in the wrong place, and end up with a bug where a logged-in user can read someone else's invoices. But the *ideas* underneath are clean and separate. Once you see the split, you'll never blur them again.

Here's the whole thing in one sentence: **authentication proves who you are; authorization decides what you're allowed to do.** Different questions, different answers, and they happen in that order.

## The mental model: passport and ticket

Picture boarding a flight.

```mermaid
flowchart LR
  authn["AUTHENTICATION - 'Who are you?'<br/>Show your PASSPORT → proves identity"]
  authz["AUTHORIZATION - 'What may you do?'<br/>Show your TICKET → this seat, this flight, today"]
  authn -- then --> authz
```

Your **passport** proves you are who you claim to be. That's authentication. It says nothing about *where* you're allowed to go.

Your **ticket** says you may board *this* flight, in *this* seat, *today*. That's authorization. The ticket doesn't prove your identity - anyone holding it could try to use it - which is exactly why the gate checks both: passport first to confirm you're you, then ticket to confirm you're allowed on this particular plane.

You need both. A passport with no ticket gets you into the terminal and no further. A ticket with no passport gets you stopped at the gate. Software works the same way.

## Authentication - proving who you are

**What it actually is.** Authentication is the act of a user *proving an identity claim*. You claim "I am alice@example.com," and you back it up with something only Alice should have: a password, a one-time code from her phone, a fingerprint, a hardware key. The server checks the proof and, if it holds up, now believes you're Alice.

**Why people get this wrong.** It's tempting to think authentication is "the login form." But the form is just where the proof is collected. Authentication is the *verification* - comparing what you supplied against what the server has on record. (How the server stores that record safely, so a database leak doesn't hand attackers everyone's password, is its own subject: see [How Passwords Are Stored](/guides/how-passwords-are-stored).)

**What it does in real life.** You submit credentials; the server verifies them and, on success, establishes that *this request now belongs to Alice*. From this moment on, the system has an answer to "who are you?"

**A real example.**
```console
$ curl -i -X POST https://api.example.com/login \
       -d 'email=alice@example.com&password=correct-horse'
HTTP/2 200
set-cookie: session=8f3b...; HttpOnly; Secure; SameSite=Lax
content-type: application/json

{"user":"alice@example.com","authenticated":true}
```
*What just happened:* Alice proved her identity, and the server confirmed it (`"authenticated":true`). It also handed back a cookie so it can recognize her on the *next* request without making her log in again - that "how do we stay logged in" piece is the whole of [Phase 2](02-sessions-vs-tokens.md). For now, the key point is narrow: authentication just answered *who*.

⚠️ **Gotcha - authentication says nothing about permissions.** A successful login does *not* mean "this person can do anything." It means "we know who this person is." A brand-new user with zero privileges authenticates exactly as successfully as an admin. Confusing "logged in" with "allowed" is the root of a whole class of security bugs.

## Authorization - what you're allowed to do

**What it actually is.** Authorization is the act of *deciding whether a known identity may perform a specific action on a specific thing*. It always runs *after* authentication, because you can't decide what someone's allowed to do until you know who they are.

**Why people get this wrong.** People check authorization too coarsely - "are they logged in? then let them in" - and skip the part that matters: *is this particular user allowed to touch this particular resource?* Alice being logged in does not mean Alice may read Bob's invoice.

**What it does in real life.** For each protected action, the server asks a permission question: does this user have the role, the ownership, or the grant required? If yes, proceed. If no, refuse - typically with `403 Forbidden`.

**A real example.**
```console
$ curl -i https://api.example.com/invoices/777 \
       --cookie 'session=8f3b...'
HTTP/2 403
content-type: application/json

{"error":"forbidden","reason":"invoice 777 belongs to another user"}
```
*What just happened:* Alice is fully authenticated - the server knows it's her, and her session cookie is valid. But invoice 777 isn't hers, so the authorization check fails and the server returns `403 Forbidden`. Notice the status code tells the story: not `401` ("we don't know who you are"), but `403` ("we know exactly who you are, and the answer is no").

📝 **Terminology - 401 vs 403.** These two HTTP status codes map cleanly onto our two concepts, and getting them right makes your API clear about what failed. `401 Unauthorized` means *authentication* failed or is missing - "I don't know who you are." `403 Forbidden` means *authorization* failed - "I know who you are, and you may not do this." (Yes, `401` is literally named "Unauthorized" while meaning authentication - a historical naming wart. Read it as "unauthenticated" and you'll stay sane.)

## How they fit together

Every protected request runs both checks, in order:

```mermaid
flowchart TD
  req([incoming request]) --> authn{authenticated?<br/>who are you}
  authn -->|no| e401[401 - who are you?]
  authn -->|yes| authz{authorized?<br/>may you?}
  authz -->|no| e403[403 - not allowed]
  authz -->|yes| do([do the thing])
```

Authentication is the front door: it establishes identity once. Authorization is every interior door: it gets checked again and again, per action, because the answer changes depending on *what* you're trying to do and *which* resource you're touching.

**Why this saves you later.** Keep these two separate in your head and real bugs become obvious before you ship them. "Any logged-in user can delete any comment" is an *authorization* hole - authentication was fine, you just forgot to check ownership. "Our public API leaks data to anyone with the URL" is an *authentication* hole - no identity was ever required. Naming which check is missing tells you where to look.

## Recap

1. **Authentication (authn) = who you are.** You prove an identity claim with something only you have; the server verifies it.
2. **Authorization (authz) = what you're allowed to do.** Given a known identity, the server decides if you may perform a specific action on a specific resource.
3. **Order matters:** authenticate first, then authorize. You can't decide permissions for someone you can't identify.
4. **Both are required, and they fail differently:** `401` means authentication failed ("who are you?"); `403` means authorization failed ("you may not").
5. **"Logged in" is not "allowed."** Conflating the two is the source of a whole family of access-control bugs.

Now that you know how the server *figures out* who you are and what you can do, the next question is practical: after that first login, how does it remember you across every following request without asking for your password each time? That's sessions versus tokens.

Watch it animated: [authentication vs. authorization](/explainers/AuthAuthz.dc.html)


---

# Keeping You Logged In: Sessions vs Tokens

HTTP has no memory. Every request arrives at the server like a stranger walking in for the first time - it has no built-in idea that *this* request came from the person who logged in two seconds ago. That's by design (it's what lets the web scale), but it means the server needs a way to answer "who is this again?" on every request after login.

There are two mainstream ways to solve it, and the difference between them - *where the state lives* - is the single idea this phase rests on. Get that and the trade-offs fall out naturally.

## The one idea: where does the state live?

When you log in, *something* has to be remembered so the next request can be tied back to you. The two approaches differ only in **who keeps it**:

```text
  SERVER-SIDE SESSION                  STATELESS TOKEN (JWT)
  ───────────────────                  ─────────────────────
  Client holds: a random ID            Client holds: ALL the data, signed
  Server holds: the real data          Server holds: nothing (just a secret key)

  cookie: session=8f3b9c...            token: eyJhbGci...{user,role,exp}...sig
            │                                    │
            ▼                                    ▼
  server looks up 8f3b9c                server checks the signature is valid
  in its session store →                with its secret key →
  "ah, this is Alice"                   "this says Alice, and it's untampered"
```

A **session** hands the client a meaningless ticket stub (a random id) and keeps the actual facts ("this id belongs to Alice, role admin, logged in at 9am") in a store on the server. A **token** flips it: the facts are written *into* the token the client holds, and the server keeps only a secret key to check it wasn't forged.

## Server-side sessions

**What it actually is.** On login, the server generates a long random string - the **session id** - stores the real user data against that id in a session store (memory, a database, Redis), and sends the id to the browser in a cookie. On each later request the browser sends the cookie back and the server looks up the id.

**What it does in real life.** The cookie is a claim check, carrying nothing meaningful by itself - just an id. All the authority lives server-side.

```mermaid
sequenceDiagram
  participant Browser
  participant Server
  participant Store as session store
  Browser->>Server: POST /login (email + password)
  Server->>Store: save id 8f3b… → "Alice, admin"
  Server-->>Browser: Set-Cookie: session=8f3b…
  Browser->>Server: GET /invoices (cookie attached)
  Server->>Store: look up 8f3b…
  Store-->>Server: "this is Alice"
  Server-->>Browser: Alice's invoices
```

**A real example.** Here's the cookie the server set, annotated:
```text
Set-Cookie: session=8f3b9c2e1a7d4b60; HttpOnly; Secure; SameSite=Lax; Max-Age=86400
                     └──────┬───────┘  └──┬──┘  └─┬──┘ └─────┬─────┘ └────┬────┘
                     random session id    │       │         │            │
   JS can't read it (blunts XSS theft) ───┘       │         │            │
   only sent over HTTPS ───────────────────────────┘        │            │
   not sent on cross-site requests (blunts CSRF) ────────────┘            │
   expires in 86400 seconds (24h) ───────────────────────────────────────┘
```
*What just happened:* The server gave the browser a random id plus a set of rules for handling it safely (annotated above). The id itself reveals nothing - its only power is that the server can look it up.

**The trade-offs, plainly.**
- *Revocation is easy and instant.* To log someone out everywhere - or kill a stolen session - delete the id from the server store. The next request with that cookie finds nothing and is rejected.
- *Every request needs a lookup.* The server hits its session store each time. Usually cheap, but it's real work, and it means the server holds state.
- *Scaling needs shared state.* Run ten server instances behind a load balancer, and they all need to reach the *same* session store (commonly Redis), or a user who logs in on instance A is a stranger to instance B.

## Stateless tokens (JWT)

📝 **Terminology - JWT.** A **JSON Web Token** (pronounced "jot") is a compact, signed string carrying a small bundle of facts ("claims") about the user, in three dot-separated parts: header, payload, signature.

**What it actually is.** Instead of storing your identity server-side, the server writes it *into* the token - id, maybe role, an expiry time - and signs the whole thing with a secret key. The client holds the token (often in a cookie or an `Authorization` header) and presents it on each request. The server re-computes the signature; if it matches, the token is trustworthy and the server reads your identity straight out of it - no store, no lookup.

**A real example.** A JWT looks like one opaque blob, but it's three Base64URL pieces joined by dots. Decoded, the middle piece is plain JSON:
```text
eyJhbGciOiJIUzI1NiJ9 . eyJzdWIiOiJhbGljZSIsInJvbGUiOiJ1c2VyIiwiZXhwIjoxNzE4ODAwMDAwfQ . 3pK_mN...sig
└────── header ──────┘  └──────────────────── payload ────────────────────┘  └─── signature ───┘

  header  (decoded): {"alg":"HS256","typ":"JWT"}
  payload (decoded): {"sub":"alice","role":"user","exp":1718800000}
  signature: HMAC-SHA256(header + "." + payload, server's secret key)
```
*What just happened:* The signature is a fingerprint computed over the header and payload with the server's secret key. Flip `"role":"user"` to `"role":"admin"` and the signature no longer matches, so the server rejects it. It doesn't *hide* anything; it *proves nobody tampered*.

⚠️ **Gotcha - a JWT is signed, not encrypted. The payload is readable by anyone.** That Base64URL middle section is not a secret - anyone holding the token can decode it and read every claim, no key required. So: **never put anything secret in a JWT** - no passwords, no private data, no API secrets. The signature stops forgery; it does not provide privacy. (Encrypted variants exist - JWE - but a plain JWT, the kind you'll meet everywhere, is readable.)

⚠️ **Gotcha - JWTs are hard to revoke.** This is the flip side of "no server-side state" - the server can't delete a token to log you out, so it stays valid until it *expires* on its own. Fire an employee or have a token stolen, and it keeps working until its `exp` time; there's no built-in off switch. The common fixes *re-introduce* server state: short token lifetimes plus refresh tokens (covered in [Phase 3](03-oauth-and-sign-in-with.md)), or a server-side denylist - at which point you've partly given back the statelessness that was the whole appeal.

**The trade-offs, plainly.**
- *No per-request lookup, easy to scale.* Any server instance with the secret key can verify a token on its own - no shared session store needed. Real for distributed systems and APIs.
- *Revocation is genuinely hard.* As above - you trade instant logout for statelessness.
- *Size and exposure.* A token carries its claims on *every* request, bigger than a tiny session id, and those claims are out in the open.

## So which one?

There's no universal winner - it depends on whether you value easy revocation or easy scaling more. A single web app? Server-side sessions are simple and let you log people out instantly. A fleet of services or a public API? Stateless tokens shine. Plenty of real systems use *both*: short-lived tokens for speed, plus server-side state to claw back revocation.

| | Server-side session | Stateless token (JWT) |
|---|---|---|
| Where state lives | On the server | In the token (client holds it) |
| Per-request cost | A lookup in the session store | A signature check (no lookup) |
| Revoke / force logout | Easy and instant (delete the id) | Hard (valid until it expires) |
| Scaling across servers | Needs a shared store (e.g. Redis) | Any node with the key can verify |
| Payload privacy | Nothing meaningful in the cookie | Claims are readable by anyone |

**Why this saves you later.** When someone says "just use JWTs, they're stateless and modern," ask the right question back: *how do we log a stolen token out?* You're choosing a trade-off on purpose instead of cargo-culting a default.

## Recap

1. **HTTP is stateless** - after login, the server needs a way to recognize you on every request.
2. **The core difference is where the state lives:** a session keeps data server-side behind a random id; a token writes the data into a signed string the client carries.
3. **Sessions** make revocation instant (delete the id) but need a per-request lookup and a shared store to scale.
4. **JWTs** scale beautifully (any node can verify with the key) but are hard to revoke (valid until expiry).
5. **A JWT is signed, not encrypted** - the payload is readable by anyone, so never put secrets in it.
6. **Many systems mix both**, pairing short-lived tokens with server state to regain control over logout.

You now understand how *your own* server remembers you. Last piece: how does a *third* app - Google, GitHub - let you log in or grant access without ever handing over your password? That's OAuth.


---

# Delegated Access: OAuth & "Sign in with…"

You've clicked "Sign in with Google" countless times. A screen pops up, you approve, and suddenly some app you've never given a password to knows your email. It feels like magic - and the actual mechanism is genuinely clever once you see it.

The problem OAuth solves is specific: *how do you let an app do something with your data on another service, without giving that app your password?* Before OAuth, the grim answer was "type your Gmail password into this third-party app and trust them" - total, permanent access handed to a stranger. OAuth replaces that with something scoped, revocable, and password-free.

## The mental model: the valet key

Some cars come with a **valet key**. It starts the engine and opens the door - enough for the valet to park it - but it *won't* open the trunk or the glovebox, and you can stop honoring it later. You hand the valet limited, revocable access without giving them your real key.

```mermaid
flowchart TD
  you["YOU (resource owner)"] -- "let this app read my contacts, nothing else" --> google["GOOGLE<br/>holds your data + does the login"]
  google -- "VALET KEY (access token)<br/>scope = contacts:read, expires soon" --> app["THE APP (third party)"]
  app -- "reads contacts only - never saw your password" --> google
```

That's OAuth. **You** own the data. **Google** holds it and is the one place you actually type your password. **The app** gets a valet key - an *access token* - limited to exactly what you approved, nothing more. It never sees your password, and you can take the key back later from your Google account settings.

## The flow, walked through

When you click "Sign in with Google" (or "Connect your calendar"), here's the dance, with the real terms attached:

```mermaid
sequenceDiagram
  participant Browser as You / Browser
  participant App
  participant Google
  App->>Browser: redirect to Google for permission
  Browser->>Google: log in + see consent screen ("read your contacts?")
  Browser->>Google: click Allow (password stays at Google)
  Google-->>App: redirect back with short-lived authorization code
  App->>Google: exchange code for tokens (back channel)
  Google-->>App: access token (valet key) + refresh token
  App->>Google: call API with access token → read contacts
```

The crucial move is step 2: **you authenticate at Google, not at the app.** Your password never leaves Google. The app only ever receives tokens - and only after you explicitly consent to a specific scope.

📝 **Terminology - why the extra code-then-exchange step (steps 3–4)?** The browser-visible redirect carries only a short-lived *authorization code*, not the actual tokens. The app's *server* then exchanges that code for tokens over a direct, back-channel call - keeping the powerful tokens out of the browser's URL bar and history. (This is the "authorization code" flow, the standard one for apps with a backend.)

## Access tokens and refresh tokens

Two tokens come back in step 5, and they do different jobs - this trips people up constantly.

**The access token** is the valet key. It's what the app sends to Google's API to actually read your contacts. It is deliberately **short-lived** - often minutes to an hour - so that if it leaks, the damage window is small.

**The refresh token** is the "get me a fresh valet key" coupon - **long-lived**, kept safely on the app's server. When the access token expires, the app quietly trades the refresh token for a new one, no need to interrupt you to log in again.

```text
   access token   ──►  used constantly, expires fast (minutes–1h)
   refresh token  ──►  used rarely, lives long; trades itself for new access tokens
                       and can be REVOKED by you to cut the app off entirely
```

*Why this split exists:* it's the same revocation trade-off from [Phase 2](02-sessions-vs-tokens.md), solved deliberately. Short-lived access tokens limit blast radius; the long-lived refresh token gives *you* an off switch - revoke it and the app can no longer mint new access tokens.

## Scopes - the limits on the valet key

**What they actually are.** A **scope** is a named permission the app asks for: `contacts.read`, `calendar.events`, `email`. The consent screen you see is literally the list of scopes the app requested, in plain language. You're approving *exactly that list* - nothing broader.

**Why this matters.** Scopes are how OAuth stays the valet key instead of the master key. An app that asked for `contacts.read` cannot suddenly delete your files - it has no scope for that, and the API will refuse. If a consent screen asks for far more than the app should need, that's your cue to be suspicious.

## OAuth (authz) vs OpenID Connect (authn)

Here's the subtlety that ties this whole guide together.

**OAuth 2.0 is about authorization** - *delegated access*. Strictly, it answers "may this app do X with the user's data?" It was designed to grant *access*, not prove *identity* - nothing in the flow above actually tells the app *who you are* in a trustworthy way, it just gives the app a key to your contacts.

**OpenID Connect (OIDC) is a thin layer on top of OAuth that adds authentication.** When an app wants to *log you in* - to learn, reliably, "this is alice@example.com" - it uses OIDC, which rides the same flow but returns one extra thing: an **ID token** (a JWT) containing verified facts about *who you are*. That ID token is the part that makes "Sign in with Google" a genuine login rather than just a data-access grant.

```text
  OAuth 2.0  →  AUTHORIZATION  →  "this app may read your contacts"   → access token
  OIDC       →  AUTHENTICATION →  "this user IS alice@example.com"     → ID token (a JWT)
              (OIDC is built on top of OAuth - same flow, plus an ID token)
```

So "Sign in with Google" is **OIDC** (authentication) doing the login. "Connect your Google Calendar so we can add events" is plain **OAuth** (authorization) granting scoped access - often both happen at once. This is where the names from [Phase 1](01-authentication-vs-authorization.md) pay off: OAuth is authz, OIDC adds authn.

## A few rules that keep you out of trouble

⚠️ **Store tokens safely, and only over HTTPS.** Access and refresh tokens are bearer credentials - whoever holds one can use it. They must only travel over encrypted connections, and refresh tokens in particular should live on your server, not in the browser. (Why HTTPS is non-negotiable: [HTTPS & TLS](/guides/https-and-tls).)

⚠️ **Don't put secrets in a JWT.** This is the [Phase 2](02-sessions-vs-tokens.md) rule, and it applies squarely to ID tokens, which *are* JWTs: their claims are readable by anyone holding the token. Fine for carrying "who you are"; not a place for anything that must stay private.

⚠️ **Don't roll your own auth.** OAuth and OIDC are full of small, security-critical details - validating the authorization code, checking token signatures and expiry, matching redirect URIs exactly, guarding against replay. Getting one subtly wrong opens a real hole. Use a well-maintained library or a hosted identity provider; understanding the flow is what lets you use those tools correctly, not a license to hand-build the protocol.

**Why this saves you later.** The next time an app asks to connect to your Google account, you'll read the consent screen as a scope list and know precisely what you're granting. The magic became a mechanism you can reason about.

## Recap

1. **OAuth 2.0 grants delegated access** - it lets an app use your data on another service without your password, like a valet key.
2. **You authenticate at the service** (Google), not at the app; the app only ever receives tokens, after you consent.
3. **The flow** hands the browser a short-lived authorization code, which the app's server exchanges for tokens over a back channel - keeping tokens out of the URL.
4. **Access tokens are short-lived valet keys; refresh tokens are long-lived coupons** that mint new access tokens and can be revoked to cut an app off.
5. **Scopes are the limits on the key** - the consent screen is the exact list of permissions you're approving.
6. **OAuth is authorization; OpenID Connect adds authentication** via an ID token (a JWT). "Sign in with Google" is OIDC; "connect my calendar" is plain OAuth.
7. **Keep tokens on HTTPS, keep secrets out of JWTs, and don't roll your own auth** - understand the flow, then use a trusted library or provider.

That's the whole landscape: who you are versus what you can do (Phase 1), how a server remembers you (Phase 2), and how access gets delegated across services (Phase 3). You can now reason about any auth system instead of half-understanding it.
