alice lets a photo-printing shop read the photos she keeps at another service without giving it her password — the authorization code flow message by message, the front and back channels, state and PKCE, the scope written into the token, refresh tokens, and what removing the app's access does and does not reach.
The flow, step by step
- Once, beforehand: prints.example registers at photos.example, and gets a
client_id (public) and a
client_secret (kept on its server). - alice clicks Import from photos.example at prints.example.
- prints.example redirects her browser to photos.example's
/authorize, with its client_id, the
scope it wants, a state and a code_challenge. - photos.example checks that it knows the app, keeps the request under an id of its own (
rid), and asks
alice to sign in. - alice signs in: her password goes to photos.example only.
- photos.example asks whether prints.example may see her photos, and alice clicks Allow.
- photos.example swaps the
rid for a one-time code, and redirects her browser back to prints.example
with the code and the state. - prints.example checks the
state, then sends the code and its code_verifier to photos.example's
/token, server to server, with its client_id and client_secret in an Authorization: Basic header,
sent at once, with no 401 first. - photos.example checks all three, and answers with an access token and a refresh token.
- prints.example calls the API with
Authorization: Bearer <access token>; the API checks the token by
itself. - When the access token expires, prints.example sends the refresh token to
/token for a new one.
The sections below take these steps apart: the roles, the two channels, state and PKCE, the scope, the
tokens, and taking the access back.
Delegation: alice's photos and a print shop
OAuth2 is delegation: alice lets an app use something she owns at another service, without giving the app
her password.
- alice keeps her photos at photos.example.
- prints.example, a print shop, wants to read one album to print it.
- alice signs in at photos.example, never at prints.example, and says yes to one thing: reading her photos.
- prints.example gets a token limited to that, and calls photos.example's API with it.
- It follows
Bearer Tokens
: the token here is the same kind of signed JWT,
checked by the API alone. What is new is who asks for it, and how it is handed over.
Reading the drawing
- Step plays eight scenes, one message per step.
- The left side asks for access: alice's browser on top, prints.example under it.
- The right side holds the photos: photos.example's authorization server on top, its photo API under it.
- The wire, in the middle, shows each message as the two ends read it, and which channel it travels on.
- The cards show what each party keeps: the app's sessions, the codes, consents and refresh tokens at
photos.example, and each server's checks on the request it handled.
- The buttons in the browser's page and on the app's card take the story our own way; the scene picker
or Restart brings it back.
- Messages so far, on the right, draws the scene as a sequence diagram; the back channel is dashed.
The four roles
OAuth2 names four parties, and every diagram of it uses these names.
| Role | In the story | What it does |
|---|
| Resource owner | alice, in her browser | owns the photos and says yes |
| Client | prints.example | the app that wants access |
| Authorization server | photos.example | signs alice in, asks for consent, issues tokens |
| Resource server | api.photos.example | holds the photos and accepts the token |
- The client never sees alice's password. Only the authorization server does.
- The resource server never sees alice sign in. It sees a token, and checks it.
- One company often runs both servers, as photos.example does here. They are still two roles: one issues
tokens, the other accepts them.
Front channel and back channel
Steps 1 to 6 travel through the browser; steps 7 to 10 go server to server.
- The front channel is the browser: redirects and addresses. Whatever travels there can end up in the
address bar, the history, a server log, a browser extension.
- Only the request and the code travel there, never a token.
- The browser's card keeps its history: the callback address, with the code in it, is there.
- The back channel is the app's server talking to photos.example and its API directly. The browser never
sees it, and neither does alice.
- The token travels only there.
- The code is the hand-over between the two. It is good once, for about a minute, for this app alone, and
worthless without the app's secret and verifier.
- An older flow, implicit, put the token itself in the redirect. Browsers leak addresses, and the flow is
now deprecated.
state and PKCE
Two random values tie the code that comes back to the sign-in that asked for it.
state protects alice's session at the app.- The app makes it, keeps it in alice's session, and sends it to
/authorize. - photos.example returns it unchanged with the code.
- The app accepts a code only with the
state it is waiting for, once. - The scene Callbacks prints.example did not start opens the callback twice more: the old address from
the history, and a link carrying Mallory's code. The app refuses both before asking photos.example
anything. Without the check, Mallory's link would connect alice's session to Mallory's photos.
- PKCE (Proof Key for Code Exchange) binds the code to the session that started the flow.
- The app makes a random
code_verifier and keeps it. - It sends only its SHA-256, the
code_challenge, to /authorize; photos.example stores it beside the
code. - At
/token the app sends the verifier, and photos.example checks that its SHA-256 is the challenge. - A stolen code is useless to anyone who does not hold the verifier, which never crossed the front channel.
- The challenges on the page are the verifiers' real SHA-256 values.
- The client secret proves the app itself at
/token. An app on a server can keep one. A phone app or a
single-page app cannot, and relies on PKCE alone.
Scope and consent
The scope is what the app asked for, alice consented to, and photos.example wrote into the signed token.
- prints.example asks for
photos.read, and the consent page says it in words: See your photos and albums. - The access token carries
scope: photos.read, under photos.example's signature. - The API checks it on every request. The scene The token allows photos.read, and nothing more makes the app
delete a photo: the API answers
403 Forbidden with error="insufficient_scope". - The app holds no token that could delete, and no password that could get one.
The access token and the refresh token
The access token is short-lived and goes to the API; the refresh token is long-lived and goes only to
photos.example.
- The access token is a JWT, signed by photos.example:
iss, sub, aud (the API), scope, exp.
The API checks it with photos.example's public key, as the orders service did in
Bearer Tokens
, and asks no one. It lives ten minutes. - The refresh token is a random string that photos.example keeps a record of. When the access token
expires, the app sends the refresh token to
/token and gets a new access token, with no browser and no
alice involved. - The app never learns who alice is. It keeps the tokens in its own session, keyed by its own cookie.
An access token is meant for the API, not for the app to read; saying who the user is belongs to OIDC.
Removing the app's access
alice can take the access back at photos.example, and it reaches the app at its next refresh, not at once.
- On her account page, Remove deletes her consent and revokes the refresh tokens issued to the app.
- The access token the app already holds is a signed JWT. Nothing about it changed, and the API checks it
alone, so it keeps working until its
exp: the lesson of Bearer Tokens' logout. - At the next refresh photos.example answers
invalid_grant, and the app has nothing left. Only alice can
give it access again. - Short access tokens are what keep this window small.
Flows the page leaves out
The authorization code flow with PKCE is the one to know; the others are special cases or retired.
- Client credentials: a program acting for itself, with no user, sends its own id and secret to
/token. - Device code: a device with no browser, such as a TV or a terminal, shows a short code that the user
types on a phone.
- Implicit and password grants: deprecated. One put the token in the browser's address; the other had
the app collect the user's password, the problem OAuth2 exists to remove.
OAuth2 gives an app access, not a login. "Sign in with photos.example" adds a second token saying who the
user is: that is
OpenID Connect (OIDC)
, the next page.
The course
Authentication and Secrets
runs the same flow by hand
against Keycloak in its chapters OAuth2 and An identity provider of your own.