Security · 13 min
OAuth grant types
How an app gets a limited key to your data without ever seeing your password, and which of the handover routes to use for a website, a TV or a server job.
You find Printly, an app that prints your photos onto mugs and calendars. Your photos live on PhotoCloud, a big photo service. Printly shows a button: “Connect PhotoCloud”.
You press it. A PhotoCloud page asks you to sign in, then asks “Allow Printly to see your photos?” You press Allow, and a moment later Printly shows your albums.
What did Printly just get from PhotoCloud? Most developers carry one of two pictures of this, and both are wrong.
Two pictures that are wrong
Wrong picture 1: “Printly now has your password.” It doesn’t. You typed your password on PhotoCloud’s own page, at PhotoCloud’s address. Printly never saw it, and the whole design exists to keep it that way.
Wrong picture 2: “OAuth is a login protocol.” It isn’t, on its own. OAuth answers “may this app read these photos?”, not “who is this person?”. Printly got permission to call an API. It did not learn your name or email.
What Printly really gets: a valet key
Some cars come with a valet key. It starts the engine but won’t open the trunk or the glovebox, so you can hand it to a parking attendant without handing over everything. OAuth gives apps a key like that.
So Printly holds a key that reads your photos for an hour, and nothing else. If Printly is hacked tomorrow, the thief gets a key that can’t change your password, can’t delete anything, and soon stops working. You can also cancel it from your PhotoCloud settings without changing your password.
Two parts of PhotoCloud are involved. The authorization server signs you in, asks “Allow?”, and hands out tokens. The photos API is a separate server that only checks tokens.
That is all OAuth is: a way to hand an app a limited key. The tricky part is the handover. The token must get from PhotoCloud to Printly without anyone in between copying it. The different OAuth “flows” (the spec calls them grant types) are just different handover routes, chosen by who is asking.
The handover for a website: a code, then a swap
Printly’s server could easily call PhotoCloud’s server directly. But that call can’t include you. Only you can sign in to PhotoCloud and press Allow, and you are sitting in a browser. So Printly has to send your browser to PhotoCloud’s page, and PhotoCloud has to send it back afterwards. The tool for that is a redirect.
Anything on the front channel should be treated as seen by others. So the flow sends something harmless through the browser and fetches the real token on the back channel. Here is the authorization code flow:
- Printly redirects your browser to PhotoCloud’s address, saying who it is (client_id), where to send you back (redirect_uri) and what it wants (scope=photos.read).
- You sign in on PhotoCloud’s page and press Allow.
- PhotoCloud redirects you back to Printly with a one-time authorization code in the address: ?code=SplxlOBeZQQYbYS6WxSbIA. It works once and expires in about a minute.
- Printly sends that code straight to PhotoCloud on the back channel and gets the access token in the reply.
- Printly calls the photos API with the token.
Guess first: The code in step 3 sits in your browser history. Someone copies it from there. Can they swap it for your token?
No. To swap a code you must also prove you are the app that asked for it. In step 1, Printly made up a random secret just for this login and kept it. It put only a hash of it in the address: a fingerprint you can compute from the secret but can’t turn back into it. To swap the code, you must show the original secret, which never went through the browser. This trick is called PKCE. Printly’s web server also sends its client secret. A copied code alone is useless, and it expires in a minute anyway.
PKCE stands for Proof Key for Code Exchange, said “pixy”. It is what makes this flow safe for phone apps and single-page web apps, which can’t hide a client secret. Current advice is to use it everywhere, even on servers that also have a secret. In the demo, the random secret is the code_verifier, its hash is the code_challenge, and S256 names the hash used (SHA-256). The PKCE concept (next in the Concepts list) shows the details.
A phone app has no address bar of its own, so it opens PhotoCloud’s page in the phone’s browser or an in-app browser tab. The redirect back goes to an address the phone knows belongs to Printly (an app link, or a custom one like printly://callback), so the phone hands it to the app. A command-line tool can open your browser and listen on a localhost address for the redirect, or use the device flow below.
A single-page app does the swap in step 4 from its JavaScript, with a fetch() call. Scripts on the page can read the token either way, so that is not the gain. The gain is that the token never sits in history, logs or Referer headers, and what does pass through the address, the code, works once and only with the PKCE secret.
One more value rides in the first address: state, a random string. Printly also keeps a copy in your browser, in a cookie or the tab’s session storage. When you come back, it checks that the state in the address matches its copy.
Here is the attack it stops. An attacker starts a Printly login with their own PhotoCloud account, stops just before their code is used, and puts that code into a link they send you. If you click it, Printly would swap their code and connect your Printly account to their PhotoCloud, so anything you save through Printly lands in their account. With state, the link carries the attacker’s state, which doesn’t match the copy in your browser, so Printly refuses.
Not asking every hour: the refresh token
The reply in step 4 also carries a refresh token. It lets Printly get a fresh access token from PhotoCloud on the back channel when the hour is up, with no screen and no click. The spec counts this as a grant type of its own. Refresh tokens last weeks or months, so they only ever go to the authorization server, never to the API, and Printly’s website adds its client secret when it uses one.
A phone app has no secret to add, so PhotoCloud uses rotation: each refresh token works once, and using it returns a new one. If a thief copies a refresh token, the thief and the app now hold the same one. Whoever uses it second presents a token that is already spent. PhotoCloud sees that, knows a copy exists, and cancels the whole chain for both.
Step through the flows
- With “Authorization code + PKCE” picked, press Next → until the demo says “Step 4 of 9”, the arrow labelled “redirect with ?code=”. The box below the diagram shows the address PhotoCloud sends your browser to, with the code in it. (That is step 3 in the list above; the demo counts each trip through the browser on its own.)
- Tick “What can an attacker see?”. Every arrow fades except the red front-channel ones. Step through them: none carries the token.
- Leave the attacker view on, pick “Implicit (don’t use)” and press Play. Steps 4 and 5 are labelled TOKEN: the access token itself rides in the address.
- Under “Which flow do I need?”, answer Yes, then No, then press “Show this flow”. You land on the device code flow.
Printly on a TV, and Printly at 2 a.m.
Printly grows. It now has a TV app that shows your albums as a slideshow, and a server job that runs every night with nobody awake. The website’s flow fits neither.
Guess first: The TV has only a remote. Typing an email and a 16-character password with it is miserable. How could the TV end up with a token without you typing your password on it?
Sign in on your phone and let the TV collect the token. That is the device authorization flow.
In the device flow, the TV asks PhotoCloud for two codes: a long device code it keeps, and a short user code like WDJB-MJHT. The TV shows “go to photocloud.example/device and enter WDJB-MJHT”. You do that on your phone, sign in there, and press Allow.
Meanwhile the TV asks PhotoCloud every 5 seconds, on the back channel, “is it approved yet?”. Until you press Allow, PhotoCloud answers with an HTTP 400 error saying authorization_pending; the spec uses that error to mean “not yet, keep asking”. Then it hands the TV its tokens. In the demo the TV’s request carries a long grant_type name, urn:ietf:params:oauth:grant-type:device_code, which is just the official label for this flow.
Your password only ever touches your phone and PhotoCloud. GitHub’s command-line tool, gh, signs you in this way: it prints a code and asks you to open github.com/login/device.
The nightly job is simpler still. It doesn’t act for any person: it uploads sample prints into Printly’s own shared album. So there is nobody to ask. In the client credentials flow, the job sends its client ID and secret straight to PhotoCloud and gets back a token for Printly itself. Two requests, both on the back channel. No refresh token either: when the hour runs out, it just asks again with its secret.
Two flows that are wrong on purpose
Internet standards are written as numbered documents called RFCs. The original OAuth 2.0 one, RFC 6749 from 2012, has two more flows. The current security advice (RFC 9700, from 2025) says not to use implicit and forbids password outright, and the coming OAuth 2.1 drops both.
Implicit dates from when a page’s JavaScript couldn’t call another site’s server, so it skips the swap. PhotoCloud puts the access token itself in the redirect address, after a #, the part of an address the browser keeps for the page’s JavaScript. The token is now on the front channel and in history. And nothing proves it reached the right app: in the code flow, the swap needs the PKCE secret (or a server’s client secret), which only the real app has.
Password has Printly show its own form, take your PhotoCloud password, and send it to PhotoCloud for a token. It brings back exactly what OAuth exists to end, and PhotoCloud can’t show you what you’re approving or ask for your second factor.
| Flow | Use it when | Person? | Token travels | Status |
|---|---|---|---|---|
| Authorization code + PKCE | Websites, phone apps, single-page apps | Yes, in a browser | Back channel | Use |
| Device code | TVs, consoles, command-line tools | Yes, on another device | Back channel | Use |
| Client credentials | Server jobs, service-to-service calls | No | Back channel | Use |
| Refresh token | Renewing a token from either of the first two | Only at the first sign-in | Back channel | Use |
| Implicit | Nothing | Yes | Front channel (the address) | Don’t use |
| Password | Nothing | Yes, typed into the app | Back channel, but the app saw your password | Don’t use |
Printly, all four parts
Put together: Printly’s website and phone app use the code flow with PKCE, and only the website adds a client secret, because only it runs where a secret stays hidden. The TV uses the device flow, and the nightly job uses client credentials. The three that act for you keep a refresh token, so you press Allow once. Not one of them ever asks for your PhotoCloud password, which is the whole point.
Remember
- OAuth gives an app a limited, expiring key (an access token) to an API. It does not give the app your password, and on its own it does not log you in.
- The flows differ only in how the token is handed over safely, chosen by who is asking.
- Anything sent through the browser’s address bar may be seen; the token comes back in the reply to a direct request instead.
- Person with a browser: authorization code + PKCE. No keyboard: device code. No person: client credentials. Renew with a refresh token.
- Implicit puts the token in the address and password hands the app your password: don’t use either.