OAuth 2.0 & OpenID Connect
Letting an app act on your behalf without ever handing it your password — and telling authorization apart from authentication.
The four roles
OAuth defines four participants. Naming them precisely makes every flow that follows readable, because each step is just a message between two of these roles.
| Role | Plain meaning | In the photo example |
|---|---|---|
| Resource owner | You — the person who owns the data and grants access | The user who owns the photos |
| Client | The app that wants access on your behalf | The photo-printing app |
| Authorization server | The service that authenticates you and issues tokens | The photo service's login/token system |
| Resource server | The API holding the protected data | The photo service's photo API |
The authorization server and resource server are often run by the same company (the photo service is both), but they are separate roles: one proves who you are and issues tokens, the other serves data when shown a valid token. Keeping them distinct is what lets one login system guard many APIs.
Resource owner (you)
│ grants access
▼
Client (the app) ──asks──► Authorization server ──issues token──►
│ │
└──────── uses token to call ──► Resource server ◄───┘Tokens and scopes
OAuth replaces your password with tokens — strings that grant limited access. There are two kinds, and the difference between them is lifespan and purpose.
- Access token — the key the client presents to the resource server on each API call. It is deliberately short-lived (minutes to an hour), so that if it leaks, it expires quickly. The client sends it in an HTTP header.
- Refresh token — a longer-lived credential the client uses to get a new access token when the old one expires, without bothering you to log in again. It is more sensitive (it can mint access tokens) so it is stored carefully and never sent to the resource server.
A scope is the mechanism for limited access: it names exactly what the token is allowed to do. When the client requests access, it asks for specific scopes (photos.read), and you see and approve exactly those. The token then carries only that permission — it cannot delete photos or read your messages.
GET /api/photos HTTP/1.1
Host: api.photoservice.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
The design intent: a short-lived, narrowly-scoped access token limits the damage of any leak in time and in power, while the refresh token keeps the experience smooth by renewing access silently.
Why implicit and password grants are deprecated
OAuth originally offered simpler flows that are now discouraged. Understanding why they were retired reinforces what the code-plus-PKCE flow protects.
- Implicit grant. This returned the access token directly in the redirect URL, skipping the code exchange. The token ended up in the browser's address bar and history, where it could leak — and there was no PKCE-style proof, so an intercepted token was immediately usable. The Authorization Code flow with PKCE now gives browser and mobile apps the same simplicity without exposing the token, so implicit is obsolete.
- Resource owner password credentials (password) grant. This had the user type their password directly into the client, which forwarded it to the authorization server. That defeats the entire point of OAuth — the client sees the password, so all the naive-approach dangers return. It only ever existed for legacy migration and is now strongly discouraged.
The through-line: both retired flows either exposed the token or exposed the password. Modern OAuth's guidance is simple — use the Authorization Code flow with PKCE for anything a user logs into.
The client credentials flow
Not every OAuth exchange involves a person. When one service needs to call another with no user in the loop — a backend job calling an internal API — there is a simpler flow: client credentials.
Here the client is the resource owner: it authenticates as itself using its own credentials (a client ID and secret it holds securely) and receives an access token directly. There is no browser redirect, no login page, no user consent, because there is no user — just one trusted service proving its own identity to get a scoped token for another service.
POST /oauth/token HTTP/1.1
Host: auth.internal.com
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&client_id=svc-billing&client_secret=...&scope=invoices.read
Use this for service-to-service (machine-to-machine) access, and only there — because it relies on a stored secret, which is safe for a locked-down backend but never for an app running on a user's device where the secret could be extracted.
OAuth 2.0 versus OpenID Connect
Here is the single most common confusion in this whole area, and it is worth stating bluntly. OAuth 2.0 is about authorization, not authentication. It answers "may this app do X on my behalf?" It was never designed to answer "who is this user?" — an access token tells the resource server the bearer is allowed to do something, not who they are.
OpenID Connect (OIDC) is a thin layer built on top of OAuth 2.0 that adds authentication. It reuses the same Authorization Code flow but adds one thing: an ID token, a token whose entire job is to tell the client who the user is (their identity — a stable user id, and optionally name and email). This is what powers "Sign in with Google": Google's OIDC layer gives the app a verified statement of who you are.
| OAuth 2.0 | OpenID Connect | |
|---|---|---|
| Question it answers | What may this app do? (authZ) | Who is this user? (authN) |
| Main token | Access token | ID token (plus access token) |
| Token is for | The resource server (an API) | The client (to learn the user's identity) |
| Use it for | Granting an app access to your data | Logging users in with an existing account |
The rule to remember: use plain OAuth when you want an app to access resources; use OIDC when you want an app to know who the user is. Using a bare access token as proof of identity is a classic security mistake — it was not built to carry that meaning.
JWT structure and validation
The ID token, and often access tokens, take the form of a JWT — a JSON Web Token. A JWT is a compact, self-contained token made of three Base64-encoded parts joined by dots: a header, a payload, and a signature. "Self-contained" is the key trait — it carries its own data (called claims) and its own proof of authenticity, so the receiver can validate it without calling back to the issuer.
Decoded, the first two parts are just readable JSON:
{
"alg": "RS256",
"typ": "JWT"
}
{
"iss": "https://auth.example.com",
"sub": "user-4815162342",
"aud": "photo-print-app",
"exp": 1720545600,
"iat": 1720542000,
"scope": "photos.read",
"email": "alex@example.com"
}
The payload's claims carry the meaning: iss (issuer — who made it), sub (subject — the user's stable id), aud (audience — who it is for), exp (expiry time), iat (issued-at time), and application claims like scope or email. The third part, the signature, is a cryptographic seal the issuer makes with a private key; anyone can verify it with the issuer's public key.
Crucially, the header and payload are only encoded, not encrypted — anyone can read them. The signature does not hide the contents; it proves they were not tampered with. Validating a JWT therefore means checking, in order: the signature is valid (it really came from the issuer and was not altered), exp has not passed (not expired), iss is who you expect, and aud includes you. Skipping any of these — especially trusting the payload without checking the signature — is how forged tokens get accepted.
header . payload . signature ────── ─────── ───────── xxxxx . yyyyy . zzzzz
Token expiry and rotation
Tokens do not live forever, and their short lives are a feature, not an inconvenience. This ties back to the two token types and to the broader security habit of limiting how long any leaked credential stays useful.
The access token expires quickly (its exp claim), so a stolen one is soon worthless. When it expires, the client silently trades its refresh token for a fresh access token — the user notices nothing. Rotation hardens this further: each time a refresh token is used, the authorization server can issue a new refresh token and invalidate the old one. If an attacker steals a refresh token and uses it, the legitimate client's next use fails (the token was already rotated), which signals the theft so the server can revoke the whole chain.
The layered result: access tokens keep leaks brief, refresh tokens keep the experience seamless, and rotation turns a stolen refresh token from a lasting compromise into a detectable, revocable event.
access token ── short life ──► expires ──► use refresh token
▲ │
└───────── new access token ◄──────────────────┘
refresh rotation: old refresh invalidated on each use
→ reuse reveals theftKey takeaways
- OAuth 2.0 solves delegated authorization: let an app do one scoped thing on your behalf without your password, revocably.
- The four roles — resource owner (you), client (the app), authorization server (issues tokens), resource server (holds data) — make every flow a message between two of them.
- Access tokens are short-lived keys for the resource server; refresh tokens silently renew them; scopes limit exactly what a token may do.
- The Authorization Code flow with PKCE is the default: a one-time code exchanged over a back-channel, with a secret verifier so a stolen code is useless.
- Implicit and password grants are deprecated because they exposed the token or the password respectively; PKCE made them unnecessary.
- Client credentials is the no-user, service-to-service flow — use it only for locked-down backends that can hold a secret.
- OAuth is authorization ("what may this app do?"); OpenID Connect adds authentication ("who is this user?") via an ID token — never use a bare access token as proof of identity.
- A JWT is header.payload.signature — encoded, not encrypted; validate the signature, expiry, issuer, and audience before trusting it.
- Short-lived access tokens plus refresh-token rotation keep leaks brief and make a stolen refresh token detectable and revocable.
Checklist
- [ ] I can explain the delegated-authorization problem and why sharing a password is unacceptable.
- [ ] I can name the four OAuth roles and place each in a flow.
- [ ] I can distinguish access tokens, refresh tokens, and scopes by lifespan and purpose.
- [ ] I can walk through the Authorization Code flow with PKCE and explain what the code and verifier protect.
- [ ] I can explain why the implicit and password grants are deprecated.
- [ ] I can describe when to use the client credentials flow and its one constraint.
- [ ] I can clearly separate OAuth 2.0 (authorization) from OpenID Connect (authentication) and the role of the ID token.
- [ ] I can describe a JWT's three parts and the checks required to validate one.
- [ ] I can explain token expiry and refresh-token rotation and what each buys.