OAuth 2.0
40.0 What this chapter gives you#
- You will be able to name the four roles OAuth defines, say which real piece of software plays each one in a system you use, and spot the common mistake of collapsing two of them into one.
- You will be able to state exactly what OAuth is for, in one sentence, and list three jobs people routinely give it that it was never designed to do.
- You will be able to write out the authorization code flow as a sequence of real HTTP messages, with every required parameter present and correctly named.
- You will be able to generate a code verifier and derive its challenge by hand, and explain the two distinct attacks that PKCE stops.
- You will be able to choose correctly between the authorization code grant, the client credentials grant and the device authorization grant for a given piece of software, and defend the choice.
- You will be able to say precisely why the implicit grant and the resource owner password credentials grant were removed, and name the specification section that removed each.
- You will be able to write a redirect URI validation rule that a security reviewer would accept, and explain how an open redirector on an unrelated page of your own site steals authorization codes.
- You will be able to say what the
stateparameter protects, what it does not protect, and why its status changed once PKCE became mandatory. - You will be able to design a set of scopes that a non-technical person can consent to meaningfully, and recognize the scope designs that make consent a formality.
- You will be able to describe the status of OAuth 2.1 as of August 2026, list its consolidated changes, and say which existing document each change comes from.
OAuth is the machinery behind almost every “connect your account” and “sign in with” button you have ever pressed. It is roughly twenty years old, it is deployed at a scale few protocols reach, and it is misunderstood in a very specific and very consequential way. This chapter is about the gap between what OAuth actually does and what the world uses it for.
Here is the gap in one sentence. OAuth is a delegation protocol: it exists so that you can let one piece of software act on your behalf at another piece of software, with limited powers, without handing over your password. That is all it does. It has no opinion about who you are. It issues a permission, not an identity. And yet the single most common use of OAuth on the public internet is logging people in, which is a question about identity. That works, most of the time, because a thin extra layer sits on top and supplies the missing statement. OAuth itself does not provide that layer. The layer is OpenID Connect, and chapter 41 is devoted to it. This chapter builds everything up to the edge of that gap and then stops at it deliberately, because you cannot understand why OpenID Connect exists until you have felt the shape of the hole it fills.
The other half of this chapter is security, and there is a lot of it. OAuth’s core specification, RFC 6749, was published in October 2012 with Dick Hardt as editor. It is a framework rather than a protocol, which means it deliberately leaves choices open, and fourteen years of deployment have shown which of those choices are landmines. Almost everything you need to know about avoiding them was collected in January 2025 into RFC 9700, “Best Current Practice for OAuth 2.0 Security”, designated BCP 240, by Torsten Lodderstedt, John Bradley, Andrey Labunets and Daniel Fett. A successor specification called OAuth 2.1 is folding those practices into the core. As of August 2026, OAuth 2.1 is still an Internet-Draft, at revision 15 dated 2 March 2026, and it is not yet an RFC. We will be precise about that status later, because a great deal of writing on the subject is not.
We will follow one transaction the whole way through. Priya banks with a bank we will call Northbank. She wants a budgeting app called Ledgerly to read her transaction history so it can categorize her spending. She does not want to give Ledgerly her banking password, and she certainly does not want Ledgerly to be able to move money. Everything in this chapter is a step in getting Ledgerly a narrow, temporary, revocable permission to read, and in stopping the eleven or twelve things that can go wrong on the way.
The plain version#
The bookkeeper who must not have your PIN#
Imagine you hire a bookkeeper to go through your bank statements and tell you where your money goes. The bookkeeper needs to see your statements. That is all. They do not need to move money, close accounts, or change your address.
The obvious way to arrange this is the worst way. You give the bookkeeper your bank card and your PIN. Now six bad things are true at once. The bookkeeper has to write your PIN down somewhere. Your bank has no idea the bookkeeper exists and thinks every visit is you. The bookkeeper can do anything you can do, not just the one thing you wanted. If you use that PIN anywhere else, the bookkeeper now has that too. If you want the bookkeeper to stop, your only move is to change the PIN, which also stops your own phone, your standing orders and everyone else you have ever told. And if the bookkeeper’s office is burgled, the thief has your PIN.
Every one of those six problems is a problem of the same kind. You gave away a thing that means “you”, when what you meant to give away was permission to do one small job.
The better arrangement is to involve the bank. You walk into the branch yourself. You say to the clerk: this bookkeeper, the one from Ledgerly, may read my transaction list for the last twelve months. Nothing else. For ninety days. The clerk checks that it is really you, notes it down, and gives the bookkeeper a numbered pass. The pass is not your card. It does not carry your PIN. It says, in effect, “the holder of this pass may read the transaction list on account 88214, and nothing else, until the twelfth of November”.
Now count the six problems again. The bookkeeper has no PIN to write down. The bank knows exactly who is asking. The pass only permits reading. Your PIN is untouched everywhere else. You can walk into the branch tomorrow and tear the pass up without disturbing anything of your own. And if the bookkeeper’s office is burgled, the thief gets a pass that reads one account’s transactions for ninety days, which is bad, but it is a much smaller kind of bad.
That is OAuth, complete. The rest of this chapter is detail about how the walk to the branch is arranged so that nobody can intercept you on the way.
The four people in the story#
There are exactly four parts to play, and getting them straight is most of the battle.
There is you, the person who owns the thing and is entitled to say who may touch it. In the story you are the account holder. In the specification’s words you are the resource owner.
There is the bookkeeper, the piece of software that wants to do something on your behalf. It is the one asking. In the specification’s words it is the client. The word is confusing, because in everyday computing “client” often means the thing in front of a user, but here it means the thing making the request for a permission. Ledgerly is the client whether it is a phone app, a website, or a program running unattended on a server at three in the morning.
There is the clerk at the branch, the counter that checks it is really you, asks whether you agree, and writes out passes. In the specification’s words this is the authorization server. It is the only party that ever sees your password, and that is the whole point of it existing.
There is the filing room, which holds the statements and honours passes at the door. It looks at the pass, checks it is genuine and still valid and permits the thing being asked for, and then hands over the papers. In the specification’s words this is the resource server. It usually never sees you at all.
Two of these four are commonly run by the same organization. Northbank runs both the clerk and the filing room. But they are still two roles, and mixing them up in your head is where a lot of bad design starts. The clerk decides. The filing room enforces. A filing room that decides is a filing room that has to know all the rules, and a clerk that enforces is a clerk who has to be present at every door.
The ticket and the pass#
Here is the part that looks like unnecessary ceremony and is not.
You might expect the clerk to hand the pass straight to you, and for you to carry it out to the bookkeeper waiting in the street. That is not what happens. Instead the clerk hands you a small paper ticket, valid for a few minutes, worth nothing on its own. You carry the ticket out and give it to the bookkeeper. The bookkeeper then goes round to the staff entrance at the back of the branch, on their own, and swaps the ticket for the real pass.
Two things about that arrangement matter enormously.
The first is that the street is public. Anything you carry through the street can be seen by people looking out of windows, written down by the person who sold you the map, remembered by the taxi driver, and photographed by the security camera at the corner. A ticket that is worthless on its own can survive that journey. A pass that opens the filing room cannot.
The second is that the staff entrance is private. It is a direct, door-to-door conversation between the bookkeeper and the branch, with nobody in between. The bookkeeper can prove who they are there in a way they cannot prove in the street. So the valuable object is handed over in the private place, and only the worthless object travels through the public one.
The paper ticket is called an authorization code. The pass is called an access token. Almost every security decision in the rest of this chapter follows from the fact that the ticket travels in public and the pass does not.
Why you walk to the branch yourself#
Notice that the bookkeeper never comes into the branch with you, and never watches you talk to the clerk.
This is deliberate and it is the second great idea in the design. If the bookkeeper stood at your shoulder while you typed your PIN into the clerk’s keypad, then the entire arrangement would be pointless, because the bookkeeper would have your PIN again. So the design insists that the conversation between you and the clerk happens in a place the bookkeeper cannot see into.
On a computer, the “place the bookkeeper cannot see into” is your own web browser. The app sends your browser off to the bank’s own website. You type your password into the bank’s page, in the bank’s window, with the bank’s address showing at the top. The app is not running there and cannot read what you type. When you are finished, the bank’s website sends your browser back to the app with the paper ticket attached.
That send-you-off-and-get-you-back move is called a redirect, and it is the reason this whole family of designs is sometimes called redirect-based. It is also, as we will see, the single largest source of security holes in real deployments, because sending a browser somewhere and getting it back safely is much harder than it sounds.
The secret word the bookkeeper never says out loud#
There is one more piece, and it exists because tickets can be stolen in the street.
Before the bookkeeper sends you off to the branch, they do something odd. They think of a long random secret word and write it on a slip of paper which they keep in their own pocket. Then they run that word through a fixed, one-way scrambling machine and give you only the scrambled version to carry to the clerk. The clerk writes the scrambled version down next to your ticket number and says nothing about it to anyone.
Later, when the bookkeeper goes to the staff entrance to swap the ticket for the pass, the clerk asks: “and what was the secret word?” The bookkeeper reads it off their slip. The clerk runs it through the same scrambling machine and checks that the result matches what was written down. If it matches, the pass is handed over. If not, nothing is handed over and the ticket is destroyed.
Now think about the pickpocket. Somebody snatches your ticket in the street. They run to the staff entrance and present it. The clerk asks for the secret word. The pickpocket does not have it, because the pickpocket only ever saw the scrambled version, and the machine only runs one way: you cannot work backwards from the scrambled version to the original. The ticket is worthless to them.
That mechanism is called PKCE, spelled from “Proof Key for Code Exchange” and pronounced “pixy”. The secret word is the code verifier, the scrambled version is the code challenge, and the scrambling machine is a standard one-way function that everybody has. It started life as a fix for one narrow problem on phones and has ended up being required of nearly everybody, for reasons we will come to.
A worked example: Priya, Ledgerly and Northbank#
Let us do it once with real values, all the way through.
Priya opens Ledgerly and taps “Connect your Northbank account”.
Ledgerly first invents its secret word. It takes thirty-two random bytes and writes them in the compact alphabet that survives web addresses, giving a forty-three character string. We will use the exact example values published in the PKCE specification, so you can check every step yourself:
secret word (code verifier), 43 characters:
dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
Ledgerly keeps that in its own memory and does not send it anywhere. It runs it through the one-way machine, which here is SHA-256, and writes the result in the same compact alphabet:
scrambled version (code challenge):
E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
Ledgerly now opens Priya’s browser at Northbank’s own sign-in page and attaches, in the address, five things: its own name as a business customer of the bank, the fact that it wants a paper ticket rather than anything else, the address it wants Priya sent back to afterwards, a plain description of what it is asking permission for, and the scrambled secret.
Priya sees Northbank’s page, in Northbank’s window. She signs in the way she always does. Northbank shows her a screen that says, in words: “Ledgerly is asking to read your account balances and your transaction history. It is not asking to move money.” She presses Allow.
Northbank writes down the ticket number against Priya’s account, the fact that it was Ledgerly who asked, the exact address it will send her back to, and the scrambled secret. Then it sends her browser back to Ledgerly with the ticket attached. The ticket is good for a few minutes only.
Ledgerly’s server now telephones Northbank’s staff entrance directly. It says: here is the ticket, here is my own business identity, and here is the secret word, dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk. Northbank scrambles the word it was given, compares it to E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM, sees they match, destroys the ticket so it can never be used again, and hands over the pass.
Ledgerly now asks the filing room for Priya’s transactions and shows the pass. The filing room checks the pass and answers. Priya sees her spending broken down by category about a second after she pressed Allow.
Three months later the pass stops working. If Priya changes her mind before then, she goes into Northbank’s app, finds a list of connected services, and removes Ledgerly. Nothing else in her life is disturbed. That last sentence is the entire commercial value of the design.
What the pass does not say#
Read the pass again. It says: the holder may read the transaction list on account 88214 until the twelfth of November.
It does not say who the holder is. It does not say who authorized it. It does not say that Priya was standing there. It is a permission, and permissions and identities are different things.
Ledgerly could guess. It could take the pass, use it to read the account, see the name printed on the statements, and decide that whoever pressed the button must be that person. That guess is right most of the time and wrong in exactly the circumstances that matter. Holding a pass and being the person who was issued it are not the same, and building a login on the assumption that they are is how the most famous class of OAuth failure works.
That is the hole this chapter is about, and the layer that fills it is OpenID Connect, which chapter 41 covers properly.
Where the plain version stops being true#
A pass is evidence of permission, never of presence#
The plain version had the clerk hand a pass to a bookkeeper who had just watched you walk into the branch. It is tempting to read that as evidence that you were there.
It is not. The pass is a bearer instrument, which means it works for whoever is holding it, exactly like a banknote. Chapter 38 deals with bearer semantics in full and this chapter does not repeat it. What matters here is the consequence for identity: a piece of software that receives a valid access token has learned that somebody, at some time, authorized somebody to have this access. It has learned nothing about who is sitting in front of it right now.
The honest version: OAuth 2.0 defines no way at all for a client to learn the identity of the user. There is no user identifier in an access token response. RFC 6749 has no such field, and the OAuth 2.1 draft says so directly in its introduction, noting that it is common practice for a client to present its access token to some proprietary API that returns a user identifier and then treat that as authentication, and that this practice “is not part of the OAuth standard or security considerations, and may not have been considered by the resource owner”. That is a specification telling you, in its own opening pages, that the thing most people do with it is outside its scope.
The gap has a name and a fix. The fix adds a separate, signed statement, addressed to one specific client, saying “this authorization server authenticated this user at this time, and the user’s identifier here is this”. That statement is the ID token and the layer is OpenID Connect. Chapter 41 handles it.
The bookkeeper is not always working for a person#
The plain version needed you to be in the room. Two of the most heavily used arrangements in OAuth have no person in the room at all.
A payroll system that pulls yesterday’s transactions from a bank at four in the morning has no user to redirect anywhere. There is nobody to press Allow. The permission it needs is its own, granted once by contract and configuration, not per user per session. For that there is a separate arrangement in which the software simply proves who it is and receives a pass covering what it is allowed to do on its own account. The four roles collapse: the client and the resource owner are the same entity.
A television has a user, but the television has no keyboard worth typing a password on and no browser worth redirecting. So the redirect happens on a different device entirely: the television shows a short code, you type that code into your phone, and the television waits, asking the bank every few seconds whether you have finished.
The honest version: the four roles are a model, not a description of a room. Sometimes the resource owner is a company rather than a person, sometimes the client and the resource owner are the same program, and sometimes the user agent in which consent happens is not on the same device or even in the same building as the client. The technical half gives each of these its correct name and its correct place.
OAuth is a framework, and two correct implementations may not interoperate#
The plain version implied a standard procedure that any bank and any bookkeeper would both know.
This is the most commercially painful place where the plain story breaks. RFC 6749 is titled “The OAuth 2.0 Authorization Framework”, and the word framework is load-bearing. The specification deliberately leaves several required components undefined. It does not define how a client registers with an authorization server. It does not define how a client discovers where the endpoints are. It does not define what an access token looks like. It does not define what any scope means. It does not define how a resource server checks a token.
The OAuth 2.1 draft is candid about this in its section 1.7 on interoperability, listing four separate optional extensions that fill the holes: RFC 8414 for authorization server metadata, published June 2018, which gives clients a document to read to find the endpoints; RFC 7591 for dynamic client registration, published July 2015; RFC 7592 for managing those registrations; and RFC 7662 for token introspection, published October 2015, which lets a resource server ask the authorization server what a token means.
The honest version: “we support OAuth 2.0” tells you almost nothing about whether your code will work against a given server. What makes integration practical in 2026 is not OAuth by itself but the accumulated conventions on top: OpenID Connect’s discovery document, the metadata RFC, and the fact that most large providers have converged on similar shapes. Standard, convention and implementation detail are genuinely tangled here, and a working engineer has to know which is which.
The consent screen is not consent#
The plain version had a clerk read out, in words, exactly what was being asked for. Real consent screens frequently do not.
Consent quality is determined by scope design, and scope design is unregulated. The specification says only that scopes are space-separated case-sensitive strings defined by the authorization server. A provider is free to define one scope called full_access, present it as “Ledgerly would like to access your account”, and call the resulting click consent. A provider is equally free to define fifteen narrow scopes and show a checklist. Nothing in OAuth prefers one over the other.
The honest version: the ability to consent meaningfully is a product decision made by the authorization server’s designers, not a property OAuth confers. Where it goes wrong it goes very wrong, and the technical half returns to the Google Docs phishing worm of 3 May 2017 to show exactly how.
Tearing up the pass is not part of the protocol#
The plain version said you could walk into the branch and tear the pass up. In the core specification, you cannot.
RFC 6749 defines no revocation mechanism whatsoever. Revoking access is left to the authorization server to arrange, and the arrangement typically works by refusing to issue any more passes rather than by cancelling one already issued. If a bank issues access tokens that last an hour and you revoke Ledgerly’s access at ten past nine, Ledgerly may well still be reading your transactions at five past ten.
A separate specification, RFC 7009, published August 2013, defines a revocation endpoint, and it is widely but not universally implemented. Whether revoking a token actually stops a request depends on whether the resource server checks with the authorization server on every call or validates the token by itself. That trade belongs to chapter 38.
The honest version: revocation in OAuth is a deployment property, not a protocol guarantee, and the honest description of “I removed that app’s access” is “no new access will be granted, and existing access will expire”.
The redirect is a security boundary that looks like a convenience#
In the plain version, the walk back from the branch to the bookkeeper’s office was uneventful. In software it is where most of the attacks live.
A redirect back to the client is a URL supplied, in part, by the client. If the authorization server accepts a supplied return address without checking it hard enough, an attacker registers a client, or abuses an existing one, and has the ticket delivered to themselves. If the client’s own website contains any page anywhere that forwards visitors to a URL taken from a query parameter, that page can be used to bounce a correctly-delivered ticket straight out to an attacker’s server. Neither of those is a flaw in the client’s OAuth code. The second one is not in the OAuth code at all.
The honest version: the security of the whole flow depends on properties of the client’s entire web presence and of the authorization server’s URL matching rules, and both are outside the part of the system a developer thinks of as “the login code”.
The plain version’s timing is too generous#
The plain version had a ticket good for “a few minutes”. Real limits are tighter and they matter.
The OAuth 2.1 draft recommends a maximum authorization code lifetime of ten minutes and requires that a code be exchanged only once: if a second valid token request arrives with a code that has already been redeemed, the server must deny it and should revoke every access token and refresh token already issued from that code. That last clause is a deliberate tripwire. A replayed code is evidence of theft, and the correct response to evidence of theft is to destroy everything downstream of it.
The honest version: single use is not politeness, it is a detection mechanism, and an authorization server that quietly returns the same tokens twice for one code has discarded a free intrusion alarm.
The technical version#
The four roles, defined precisely#
RFC 6749 section 1.1 and the OAuth 2.1 draft section 1.1 define four roles, and the definitions are worth reading in their exact form because each one draws a boundary.
The resource owner is an entity capable of granting access to a protected resource; when it is a person it is called an end user. The client is an application making protected resource requests on behalf of the resource owner and with its authorization; the term implies nothing about where the software runs. The authorization server is the server that issues access tokens to the client after successfully authenticating the resource owner and obtaining authorization. The resource server hosts the protected resources and accepts and responds to requests bearing access tokens.
| Role | Holds | Never sees |
|---|---|---|
| Resource owner | The account | Client secrets |
| Client | Tokens, own secret | User password |
| Authorization server | User credentials | Resource data |
| Resource server | The data | User password |
The abstract flow between them, as drawn in figure 1 of the OAuth 2.1 draft, has six steps. The concrete version we care about routes steps one and two through the authorization server rather than directly to the resource owner, which the specification calls the preferred method.
Priya Browser Ledgerly Northbank
(resource (client) (auth server
owner) + resource srv)
| | | |
| tap connect | | |
|--------------->| | |
| | build URL | |
| |<-------------| |
| | GET /authorize (1) |
| |---------------------------->|
| sign in + consent shown |
|<--------------------------------------------->|
| | 302 to /callback?code (2) |
| |<----------------------------|
| | deliver code| |
| |------------->| |
| | | POST /token (3)
| | |------------->|
| | | access token|
| | |<-------------|
| | | GET /accounts (4)
| | |------------->|
| | | JSON data |
| | |<-------------|
Steps 1 and 2 travel through the browser and are visible to it. Steps 3 and 4 are direct machine-to-machine calls. Everything PKCE does is about the difference between those two categories.
Client types, registration and what a client_id is not#
OAuth 2.1 section 2.1 defines exactly two client types, on one criterion: whether the client can hold credentials securely. A confidential client has credentials registered with the authorization server. A public client does not, because it cannot keep a secret. A single client_id should not be treated as more than one type.
The draft then describes three client profiles. A web application runs on a web server, and its credentials and tokens are never exposed to the resource owner. A browser-based application is downloaded to and executed inside the user’s browser, where the specification observes that protocol data and credentials are “easily accessible (and often visible) to the resource owner”. A native application is installed on the user’s device, and the specification is blunt: it is assumed that any client authentication credentials included in the application can be extracted.
That last sentence disposes of an argument that still recurs. A mobile app’s “client secret”, compiled into the binary and shipped to a million phones, is not a secret. It is a value that every one of those million users can read out with a hex editor. A native app is a public client, and OAuth 2.1 recommends either the backend-for-frontend pattern, where a server the developer controls holds the real credentials, or issuing credentials at runtime through dynamic client registration under RFC 7591.
The client_id is a public identifier, not a credential. It appears in URLs, in browser history, in server logs and in any screenshot a user posts. Nothing in the protocol treats it as secret and no security property should be built on it being unknown.
The authorization code flow, message by message#
Here is the whole flow for Priya, Ledgerly and Northbank, in real HTTP. Every parameter name below is taken from the OAuth 2.1 draft sections 4.1.1, 4.1.2 and 4.1.3, and matches RFC 6749 section 4.1 except where noted.
Step one, the authorization request. Ledgerly sends Priya’s browser to Northbank’s authorization endpoint. Line breaks are for display only; in reality this is one line.
GET /authorize?response_type=code
&client_id=ledgerly-3f9
&redirect_uri=https%3A%2F%2Fapp.ledgerly.example%2Fcallback
&scope=accounts%3Aread%20transactions%3Aread
&state=Vb3n8QpL2s
&code_challenge=
E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256 HTTP/1.1
Host: id.northbank.example
response_type=code is required and selects the authorization code flow. client_id is required. code_challenge is required unless the narrow exception in section 7.5.1 applies, discussed below. code_challenge_method is optional and defaults to plain, which is why you must always send S256 explicitly. redirect_uri is optional if exactly one is registered and required if several are. scope and state are optional.
Northbank now authenticates Priya, shows her a consent screen, and on approval issues a code.
Step two, the authorization response.
HTTP/1.1 302 Found
Location: https://app.ledgerly.example/callback
?code=SplxlOBeZQQYbYS6WxSbIA
&state=Vb3n8QpL2s
&iss=https%3A%2F%2Fid.northbank.example
Cache-Control: no-store
code is required and opaque to the client. state is required if and only if the client sent one, and must be returned byte for byte. iss is optional and carries the authorization server’s issuer identifier, defined by RFC 9207, published March 2022 by Karsten Meyer zu Selhausen and Daniel Fett, as a defence against mix-up attacks. The code must expire shortly after issue, with a maximum of ten minutes recommended, and is bound to the client identifier, the code challenge and the redirect URI.
Step three, the token request. This is a direct server-to-server call from Ledgerly, not from the browser.
POST /token HTTP/1.1
Host: id.northbank.example
Authorization: Basic bGVkZ2VybHktM2Y5OnNlY3JldA==
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&code_verifier=
dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
grant_type=authorization_code is required. code is required. code_verifier is required if and only if a code_challenge was sent in the authorization request, and must not be sent otherwise. client_id is required only if the client is not authenticating some other way; here it authenticates with HTTP Basic, so the identifier is in the header.
Note what is absent. In RFC 6749 section 4.1.3 the token request carried a redirect_uri parameter, repeated so that the server could confirm it. OAuth 2.1 removes it, on the grounds that the code challenge now prevents the injection attack that parameter was defending against. For backwards compatibility a server must still accept and enforce it for clients that send it.
Step four, the token response.
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
{
"access_token": "2YotnFZFEjr1zCsicMWpAA",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "accounts:read transactions:read",
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA"
}
access_token and token_type are required. expires_in is recommended and is a number of seconds. scope is required whenever the granted scope differs from the requested scope, which is how a client learns it got less than it asked for. refresh_token is optional. The Cache-Control: no-store header is mandatory on any response containing tokens.
Step five, using the token at the resource server.
GET /accounts/88214/transactions?from=2025-08-01 HTTP/1.1
Host: api.northbank.example
Authorization: Bearer 2YotnFZFEjr1zCsicMWpAA
The Authorization: Bearer header is the required form. OAuth 2.1 removes the option, present in RFC 6750, of putting a bearer token in the URI query string, following section 4.3.2 of RFC 9700, because query strings end up in browser history, server access logs, referrer headers and analytics pipelines.
PKCE: the verifier, the challenge, and two different attacks#
PKCE is defined in RFC 7636, “Proof Key for Code Exchange by OAuth Public Clients”, published September 2015 by Nat Sakimura, John Bradley and Naveen Agarwal.
The code verifier, per section 4.1, is a high-entropy cryptographically random string using only the unreserved characters A-Z, a-z, 0-9, hyphen, full stop, underscore and tilde, with a minimum length of 43 characters and a maximum of 128. The recommended construction is to take 32 random octets and base64url-encode them, which yields exactly 43 characters. The code challenge, per section 4.2, is one of two transformations:
plain: code_challenge = code_verifier
S256: code_challenge =
BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
S256 is mandatory to implement on the server. A client capable of S256 must use it. plain exists only for constrained environments with no hashing function available, and a client may use it only if it knows out of band, or from authorization server metadata under RFC 8414, that the server supports it. Because code_challenge_method defaults to plain when absent, omitting it is a downgrade, not a default.
The worked values from RFC 7636 appendix B, which you can reproduce with any SHA-256 tool:
| Stage | Value |
|---|---|
| Random octets | 32 bytes |
| Verifier chars | 43 |
| Challenge chars | 43 |
| Transform | SHA-256 then base64url |
verifier :
dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
SHA-256 of the ASCII of that string, first 8 octets:
19 211 30 150 26 26 216 236
challenge:
E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
The original attack, described in section 1 of RFC 7636, is authorization code interception on a mobile device. A native app registers a private-use URI scheme, something like com.ledgerly.app, so that the operating system will hand it the redirect. A malicious app installed on the same device registers the same scheme. On most platforms there is no adjudication of collisions: the operating system may hand the redirect, including the authorization code, to the malicious app. The malicious app knows the client identifier, because it is public, and can extract any client secret compiled into the legitimate app. Without PKCE it exchanges the stolen code for a real access token.
Legitimate app Malicious app
| |
|-- opens browser, sends ---->|
| code_challenge to AS |
| |
AS redirects to com.ledgerly.app:/cb?code=...
| |
| OS hands it to --X (attacker)
| |
| POST /token with code
| but NO code_verifier
| |
| <-- invalid_grant --|
The second attack is broader and is the reason PKCE is no longer just for phones. It is authorization code injection: an attacker who has obtained a code, by any means, feeds it into a victim’s session at a legitimate client, so that the victim’s client exchanges the attacker’s code and binds the attacker’s account to the victim’s session, or the reverse. RFC 9700 covers it in section 4.5. The OAuth 2.1 draft’s section 7.5.1 states the countermeasure plainly and adds a historical note worth quoting in substance: although PKCE was originally designed to protect native apps from code exfiltration, all kinds of OAuth clients, including web applications and other confidential clients, are susceptible to code injection, and the same mechanism solves it.
The exception is narrow. Under OAuth 2.1 section 7.5.1.1, a server may omit enforcement only when both of two things hold: the client is confidential, and in that specific deployment and request the authorization server has reasonable assurance that the client implements the OpenID Connect nonce mechanism properly. Even then, enforcing PKCE is still recommended, and the draft gives the reason: relying on nonce means the client detects the injection after it has already exchanged the code and obtained tokens, whereas enforcing the code challenge means the authorization server refuses to issue tokens in the first place.
There is also a downgrade to defend against, covered in RFC 9700 section 4.8. If an attacker can strip the code_challenge from an authorization request, and the server treats its absence as “this client does not use PKCE”, the protection evaporates. This is why OAuth 2.1 section 4.1.3 requires that a server reject a token request when there was no code challenge in the corresponding authorization request, and why section 4.1.2.1 requires servers to reject authorization requests without a code challenge from public clients outright.
Redirect URI validation, exact matching and open redirectors#
Section 2.3.1 of the OAuth 2.1 draft is one of the most important paragraphs in the whole document. Authorization servers must require clients to register their complete redirect URI, including the path component, and must reject authorization requests specifying a redirect URI that does not exactly match a registered one. The comparison rule is simple string comparison as defined in section 6.2.1 of RFC 3986. There is exactly one exception: for loopback redirects used by native apps, the match must be exact except for the port, because a desktop app cannot know in advance which local port will be free.
RFC 9700 section 4.1.3 states the same countermeasure. This is the settled position, not a matter of opinion.
The failures are all forms of matching that are looser than string equality.
| Rule used | Registered | Attacker gets |
|---|---|---|
| Prefix match | app.example/cb | app.example/cb.evil |
| Subdomain wildcard | *.example.com | evil.example.com |
| Ignore query string | app.example/cb | cb?next=evil |
| Host only | app.example | any path on host |
The subdomain case deserves a note. A wildcard covering an entire domain means anyone who can put content on any host under it, including a marketing microsite or an abandoned staging server, can receive authorization codes for the whole company.
The open redirector case is the one that catches teams who have done everything else correctly. Suppose the exact registered redirect URI is honoured, and the code is delivered to the right page on the right host. If any other page on that host takes a URL from a query parameter and forwards the browser to it, an attacker can construct a chain: the authorization server redirects to the legitimate client, the legitimate client’s own redirector then forwards onwards to the attacker, and browsers helpfully carry the query string along. RFC 9700 section 4.1.1 describes this and section 2.3.1 of the OAuth 2.1 draft turns it into a requirement on clients: clients must not expose URLs that forward the user’s browser to arbitrary URIs obtained from a query parameter.
The authorization server can be an open redirector too, and the draft’s section 7.12.2 sets out three ways to abuse it: send a deliberately invalid request such as a bad scope so the server bounces the user to the attacker’s registered URI; send a valid request and rely on the server redirecting even when the user declines; or use a silent authentication request so the redirect happens with no interaction at all. The mitigation in section 4.1.2.1 is that a server must not redirect at all when the client identifier or redirect URI is missing or invalid, and must instead show the error to the user in the browser.
For native applications, RFC 8252, “OAuth 2.0 for Native Apps”, BCP 212, published October 2017 by William Denniss and John Bradley, defines three legitimate redirect styles: private-use URI schemes based on a reverse domain name the developer controls, claimed https scheme URIs where the operating system verifies the association with the domain, and loopback interface redirection using 127.0.0.1 or the IPv6 loopback with any port. Its central normative rule, in section 5, is that native apps must use an external user agent, the system browser, and not an embedded web view, because an embedded web view is by definition readable by the app that embeds it, which reintroduces exactly the password exposure OAuth exists to prevent.
acceptable native redirect targets (RFC 8252 s7)
------------------------------------------------
private-use scheme com.ledgerly.app:/oauth2/cb
claimed https app.ledgerly.example/cb
loopback IPv4 http://127.0.0.1:PORT/cb
loopback IPv6 http://[::1]:PORT/cb
state, nonce, CSRF and mix-up#
The state parameter in RFC 6749 section 4.1.1 is an opaque value the client sends and the server returns unchanged. RFC 6749 recommended it and said it should be used for preventing cross-site request forgery, described in its section 10.12.
The attack it was defending against is worth stating exactly, because it is not the attack most people describe. An attacker performs an OAuth flow against the authorization server using their own account, stops just before the final step, and captures the authorization code delivered to the redirect URI. They then cause the victim’s browser to visit the client’s callback URL carrying that code. The victim’s client exchanges it and attaches the attacker’s account to the victim’s logged-in session. The victim now, without knowing it, is operating on the attacker’s connected account, and anything they upload, save or type goes into an account the attacker controls. Defending against it requires that the client refuse any callback it did not itself initiate in this browser session, which it does by generating a one-time unguessable value, storing it bound to the user agent, sending it as state, and comparing on return.
Two things have changed since 2012 and both are in the OAuth 2.1 draft.
First, state is now optional rather than recommended, because the code challenge does the same job better. Section 2.3.3 puts it plainly: clients must prevent CSRF; clients that have ensured the authorization server supports code_challenge may rely on the CSRF protection that mechanism provides; in OpenID Connect flows, validating nonce provides CSRF protection; otherwise one-time CSRF tokens carried in state and securely bound to the user agent must be used. The requirement never went away. Only the recommended mechanism changed.
Second, state is now free to do the job developers were always using it for anyway, which is carrying application state such as “the user was on the invoices page when they started this”. The draft notes explicitly that state should not contain sensitive client or resource owner information in plain text, since it travels through the browser.
nonce is not an OAuth parameter at all. It is defined by OpenID Connect, it appears in the authorization request and is echoed inside the ID token, and its job is to bind the identity statement to the browser session that asked for it. Chapter 41 covers it.
The mix-up attack is a distinct problem that neither state nor PKCE fully solves. It applies when one client talks to more than one authorization server and at least one of them is hostile, which dynamic client registration makes cheap to arrange. The attacker’s server can declare an honest server’s authorization endpoint as its own while keeping the token endpoint under its control, so storing the authorization server’s URL is not enough. The OAuth 2.1 draft section 7.14 requires that a client store, per authorization request, the issuer it sent the request to, bind that to the user agent, and check that the response came from that issuer. There are two ways to do it: the iss response parameter from RFC 9207, or using a distinct redirect URI for every authorization server.
| Control | Stops | Where defined |
|---|---|---|
| PKCE | Code theft, injection | RFC 7636 |
| state | CSRF on callback | RFC 6749 s4.1.1 |
| nonce | ID token replay | OpenID Connect |
| iss | Mix-up attacks | RFC 9207 |
The other grants and where each belongs#
Beyond the authorization code grant, two grants matter and one is a special case.
The client credentials grant is for a client acting on its own behalf, with no resource owner involved. OAuth 2.1 section 4.2 restricts it to confidential clients only. The request is a single call:
POST /token HTTP/1.1
Host: id.northbank.example
Authorization: Basic bGVkZ2VybHktM2Y5OnNlY3JldA==
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&scope=reports%3Aread
There is no browser, no redirect, no consent screen and no user. The correct place for this is service-to-service work where the permission belongs to the service: a nightly reconciliation job, a partner’s batch import, an internal microservice calling another. The incorrect and depressingly common place is a service using client credentials to obtain broad access and then acting as whichever user it likes, which turns the authorization server into a rubber stamp and moves all the real access control into code nobody audits.
The device authorization grant is defined by RFC 8628, published August 2019 by William Denniss, John Bradley, Michael Jones and Hannes Tschofenig. It is for devices that either have no browser or have no reasonable way to type: smart televisions, media consoles, digital picture frames, printers and command-line tools. The device asks a new endpoint for a pair of codes:
{
"device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS",
"user_code": "WDJB-MJHT",
"verification_uri": "https://example.com/device",
"expires_in": 1800,
"interval": 5
}
The device displays user_code and verification_uri on the screen. The user opens that address on a phone or laptop, signs in there and types the short code. Meanwhile the device polls the token endpoint with grant_type=urn:ietf:params:oauth:grant-type:device_code, waiting interval seconds between attempts, defaulting to five if the field is absent. The polling responses are the interesting part: authorization_pending means keep waiting, slow_down means keep waiting and add five seconds to the interval, access_denied means the user refused, and expired_token means the codes have died. The optional verification_uri_complete field embeds the user code in the address so it can be rendered as a QR code.
| Grant | Who it is for | Human present |
|---|---|---|
| Authorization code | Apps acting for a user | Yes |
| Client credentials | Service acting for itself | No |
| Device authorization | Input-limited devices | Yes, elsewhere |
| Refresh token | Renewing without asking | No |
The refresh token grant is not really a separate use case, it is a renewal mechanism. OAuth 2.1 section 3.2.3 requires that refresh tokens be bound to the scope and resource servers the resource owner consented to, to prevent a legitimate client escalating its own privilege. Section 4.3.3 says a server may revoke the old refresh token when issuing a new one, and that if a new one is issued its scope must be identical to the old. The most important rule sits in RFC 9700 section 4.14.2 and is imported into OAuth 2.1’s list of differences: refresh tokens issued to public clients must either be sender-constrained or one-time use. Sender-constrained means the token only works when presented by the client that holds a particular key, which is the subject of chapter 38. One-time use means rotation: every refresh returns a new refresh token and invalidates the old, so that reuse of a retired token is detectable evidence of theft.
Implicit and password grants, and why both are gone#
RFC 6749 defined four grant types. Two of them are absent from OAuth 2.1, and the reasoning is documented rather than fashionable.
The implicit grant, RFC 6749 section 4.2, returned an access token directly from the authorization endpoint in the fragment of the redirect URL, with response_type=token. It existed for a real reason: in 2012 a JavaScript application in a browser could not make a cross-origin call to a token endpoint, because cross-origin resource sharing was not yet universally deployed, so there was no way for it to complete step three. The design traded security for feasibility.
Three things went wrong with it. An access token in a URL fragment is in the browser’s address bar, in its history, and available to any script running on the page; RFC 9700 section 4.3.2 covers credential leakage via browser history. An access token delivered through the front channel can be injected: an attacker who obtains a token from anywhere can feed it into a victim’s client, and unlike an authorization code there is no second step at which the fraud could be caught. And a token delivered this way cannot be sender-constrained, because the client never authenticates to get it.
RFC 9700 section 2.1.2 states that clients should not use the implicit grant or other response types issuing access tokens in the authorization response, unless access token injection is prevented. OAuth 2.1 section 10.1 omits it entirely: response_type=token is no longer defined. The removal does not affect other extension response types that return other artefacts from the authorization endpoint, such as response_type=id_token from OpenID Connect. The feasibility argument that justified implicit disappeared years ago; cross-origin requests from browser applications are now routine, so a browser application uses the authorization code flow with PKCE like everything else.
The resource owner password credentials grant, RFC 6749 section 4.3, had the user type their username and password directly into the client, which then posted them to the token endpoint. It was included as a migration path for legacy applications being converted to OAuth.
It is the exact thing OAuth was invented to abolish. The client sees the password. The client is tempted to store the password. The authorization server cannot apply multi-factor authentication, step-up challenges, risk scoring, a device check, a passkey prompt or any other modern control, because there is no browser and no interaction; all it receives is a string. RFC 9700 section 2.4 does not hedge: the resource owner password credentials grant must not be used, because this grant type insecurely exposes the credentials of the resource owner to the client. OAuth 2.1 omits it.
| Grant | Status | Reason given |
|---|---|---|
| Implicit | Removed in 2.1 | RFC 9700 s2.1.2 |
| Password | Removed in 2.1 | RFC 9700 s2.4 |
| Code without PKCE | Restricted | RFC 9700 s2.1.1 |
| Token in query | Removed | RFC 9700 s4.3.2 |
Scope design that a human can consent to#
Scope is where OAuth touches a real person, and it is almost entirely undefined by the specification. OAuth 2.1 section 1.4.1 says only that scopes are space-delimited case-sensitive strings whose meaning the authorization server defines, that order does not matter, that the server may fully or partially ignore what was requested, and that if the granted scope differs from the requested scope the server must tell the client in the scope response parameter.
Everything that makes consent meaningful is therefore a design choice. Four principles follow from watching where it goes wrong.
The first is that a scope must correspond to a sentence a person can evaluate. “Read your transaction history” is such a sentence. “Access your account” is not, because it does not distinguish reading a balance from emptying it. The test is whether a reasonable user could later say they were surprised.
The second is that read and write must never share a scope. The overwhelming majority of integrations only read. If your scopes are accounts:read and accounts:write rather than a single accounts, then the consent screen for a budgeting app says “read” and the user’s exposure is bounded by that word. This is the single highest-value split in most designs.
The third is that scope should be as narrow as the resource, not as wide as the service. A scope that grants access to every mailbox in an organization is different in kind from one that grants access to one folder in one mailbox. For requirements finer than a string can express, RFC 9396, “OAuth 2.0 Rich Authorization Requests”, published May 2023 by Torsten Lodderstedt, Justin Richer and Brian Campbell, defines an authorization_details parameter carrying structured JSON, which is what you need to express “payments up to 500 pounds to this named payee only”.
The fourth is that the resource server must actually enforce it. A beautifully designed scope set that no API checks is decoration. OAuth 2.1 section 1.4.1 is explicit that the resource server is responsible for enforcing scopes when presented with a limited-scope token.
| Scope name | Consent reads as | Verdict |
|---|---|---|
| accounts:read | See balances | Good |
| transactions:read | See past payments | Good |
| payments:create | Send money | Good |
| account | Do anything | Bad |
The Google Docs phishing worm of 3 May 2017 is the case study. An attacker registered an OAuth application and named it “Google Docs”. Victims received a genuine-looking share notification, clicked, and were shown a real Google consent screen, on a real Google domain, with a real padlock, asking whether “Google Docs” could read, send, delete and manage their email and manage their contacts. The application name was attacker-chosen and looked exactly like a Google product. Google stopped the attack in approximately one hour and stated that fewer than 0.1 per cent of Gmail users were affected, which at the time was around one million people. No cryptography failed. No token leaked. The consent screen worked exactly as designed and asked a question the user could not correctly answer, because the only piece of information they were shown that mattered, the application’s name, was chosen by the attacker.
What a stolen token costs, in one real case#
Bearer tokens and their theft belong to chapter 38 and we will not repeat that material. One case is worth stating here because it shows the scale.
On 28 September 2018 Facebook announced a security issue affecting around 50 million accounts. The vulnerability, which had existed since July 2017, arose from an interaction of three bugs in the “View As” feature, which lets a person see their own profile as another user sees it. It caused Facebook access tokens to be issued in the wrong context, and those tokens could be used to take over accounts. On 12 October 2018 Facebook published a correction: of the 50 million believed affected, about 30 million actually had tokens stolen. Attackers had started from around 400,000 accounts they controlled and walked outwards through friend lists, reaching name and contact details for 15 million people and a far longer profile record, including birth date, employer and recent check-ins, for another 14 million.
On 17 December 2024 the Irish Data Protection Commission announced fines totalling 251 million euro against Meta Platforms Ireland arising from that breach, in a decision dated 12 December 2024, describing approximately 29 million affected accounts globally of which about 3 million were in the European Economic Area.
The lesson for this chapter is not about Facebook’s bug. It is that a token which represents delegated access to one account, multiplied by a flaw in how tokens are minted, becomes a mechanism for mass compromise, and that regulators now price that outcome in nine figures.
OAuth 2.1: what changed, and its status in August 2026#
Be precise about status, because a great deal of published writing is not. OAuth 2.1 is an Internet-Draft of the IETF OAuth working group, not an RFC. The first individual draft, draft-parecki-oauth-v2-1-00, was posted on 7 March 2020. It was adopted as a working group document on 30 July 2020. The current revision as of August 2026 is draft-ietf-oauth-v2-1-15, posted on 2 March 2026 by Dick Hardt, Aaron Parecki and Torsten Lodderstedt, with an expiry of 3 September 2026. The working group milestone for submitting it to the IESG, the body that approves documents for publication, is December 2026. It has not been submitted at the time of writing. That is six years and fifteen revisions, which tells you something about how much accumulated practice is being folded into one document.
When published, it will replace and obsolete RFC 6749 and RFC 6750. It consolidates RFC 6749, RFC 8252 for native apps, RFC 7636 for PKCE, RFC 9700 for security best practice, RFC 6750 for bearer token usage, and the browser-based apps document, which as of August 2026 is draft-ietf-oauth-browser-based-apps-27 and sits in the RFC Editor queue awaiting a number.
Section 10 of the draft gives a non-normative list of changes. Here it is, with the source of each.
| Change in OAuth 2.1 | Comes from |
|---|---|
| PKCE folded into code grant | RFC 7636 |
| Redirect URIs exactly matched | RFC 9700 s4.1.3 |
| Implicit grant omitted | RFC 9700 s2.1.2 |
| Password grant omitted | RFC 9700 s2.4 |
The list continues beyond four rows: bearer tokens in the query string of a URI are omitted, per RFC 9700 section 4.3.2; refresh tokens for public clients must be either sender-constrained or one-time use, per RFC 9700 section 4.14.2; the token request carrying an authorization code no longer contains the redirect_uri parameter; and authorization servers must support client credentials in the request body.
Two honest caveats. First, none of this is new security guidance. Every item is already normative somewhere else, most of it in RFC 9700, which has been BCP 240 since January 2025. A team that has implemented RFC 9700 has already implemented OAuth 2.1’s security posture. Second, there is a genuine interoperability wrinkle in the redirect_uri removal, and the draft admits it: a client following only OAuth 2.1 will not send redirect_uri in the token request, and will therefore fail against an authorization server that still requires it. Servers are told to accept both. Clients in the field will meet servers that do not.
Putting it together: what a correct deployment does#
Everything above reduces to a short list of decisions. Priya’s transaction goes through unharmed if, and only if, each of the following holds.
The client is classified honestly. If it runs on a user’s device or in a user’s browser, it is public, its “secret” is not one, and it either uses a backend it controls or registers dynamically.
The redirect URI is registered in full, path included, and compared by simple string equality, with the loopback port exception and nothing else. No wildcards, no prefixes, no subdomain patterns.
Every authorization request carries code_challenge with code_challenge_method=S256, and the authorization server rejects requests without one. The token request carries code_verifier, and the server rejects a token request whose corresponding authorization request had no challenge.
Codes are single use with a lifetime of ten minutes or less, and a second presentation of a code revokes every token descended from it.
The client refuses any callback it did not initiate, using state bound to the user agent or relying on the code challenge where the server is known to enforce it, and checks iss if it talks to more than one authorization server.
The client’s entire domain contains no page that forwards a browser to a URL taken from a query parameter.
Access tokens are short-lived, arrive only in the Authorization header, and carry scopes narrow enough that a person could have consented to them. Refresh tokens for public clients rotate or are bound to a key.
And the identity of the user, if the client needs it, comes from a signed statement addressed to that client and validated as such, not from an access token and a hopeful API call. That statement is the ID token, and chapter 41 is where it is defined, required, and checked.
40.98 Common wrong ideas#
Wrong: OAuth logs users in, so it is an authentication protocol. Right: OAuth is an authorization protocol for delegated access, and it defines no way for a client to learn who the user is; the OAuth 2.1 draft says so in its introduction and points at OpenID Connect. Login works because OIDC adds a signed ID token addressed to one specific client, which chapter 41 covers.
Wrong: PKCE is for mobile apps, so my server-side web application does not need it. Right: PKCE was designed for native apps, but authorization code injection affects every client type including confidential ones, which is why OAuth 2.1 requires code_challenge from all clients with one narrow exception in section 7.5.1 and RFC 9700 recommends it for confidential clients in section 2.1.1.
Wrong: My mobile app has a client secret, so it is a confidential client. Right: A secret shipped inside an application on a user’s device can be extracted, and OAuth 2.1 section 2.1 says so explicitly; such a client is public. If it genuinely needs client authentication, use a backend the developer controls or issue credentials at runtime through dynamic client registration.
Wrong: Registering the redirect URI’s domain is enough, since only my code runs there. Right: The registered value must be the complete URI including the path, and the comparison must be simple string equality per RFC 3986 section 6.2.1, with a port exception only for loopback. Wildcards and prefix matches let anyone who can host content anywhere under that domain collect authorization codes.
Wrong: The state parameter is obsolete now that PKCE is mandatory. Right: The CSRF requirement is unchanged; only the acceptable mechanism has widened. OAuth 2.1 section 2.3.3 permits reliance on the code challenge where the server is known to support it, or on the OpenID Connect nonce, but otherwise still requires one-time state values bound to the user agent.
Wrong: An access token proves the user was present and consented just now. Right: An access token is a bearer credential proving that some authorization exists; it carries no statement about who is holding it or when they authenticated. Presence and recency require a separate authenticated statement, which is what OpenID Connect’s ID token and its auth_time claim exist for.
Wrong: The implicit grant is deprecated because tokens in URLs look untidy. Right: It was removed for three specific reasons: tokens in the fragment leak through history and scripts, front-channel tokens can be injected with no second step to catch it, and such tokens cannot be sender-constrained. RFC 9700 section 2.1.2 sets it out and OAuth 2.1 section 10.1 removes response_type=token.
Wrong: The password grant is fine for our own first-party mobile app, since we wrote it. Right: RFC 9700 section 2.4 says it must not be used, and the reason is structural rather than about trust: a client that handles the password can store it, and an authorization server that receives only a password string cannot apply multi-factor prompts, risk checks or passkeys. Use the authorization code flow with PKCE in the system browser.
Wrong: Revoking an app’s access in my account settings immediately cuts it off. Right: RFC 6749 defines no revocation mechanism at all; revocation is a deployment feature, optionally standardized by RFC 7009 of August 2013. Whether it takes effect immediately depends on whether the resource server validates tokens by asking the authorization server or by checking them itself, so existing access typically persists until expiry.
Wrong: OAuth 2.1 is the current standard and OAuth 2.0 is out of date. Right: As of August 2026 OAuth 2.1 is an Internet-Draft, revision 15 dated 2 March 2026, with an IESG submission milestone of December 2026; the published standards are still RFC 6749 and RFC 6750 as updated by RFC 9700. Implementing RFC 9700, which has been BCP 240 since January 2025, gives you OAuth 2.1’s security posture today.
40.99 Chapter summary in 20 lines#
- OAuth is a delegation protocol whose purpose is to let one application act on a user’s behalf at another with limited powers, without the user handing over a password.
- It defines four roles: the resource owner who grants access, the client that asks, the authorization server that authenticates the user and issues tokens, and the resource server that holds the data and honours tokens.
- The core specification is RFC 6749, published October 2012 with Dick Hardt as editor, and it is a framework that deliberately leaves registration, discovery, token format and scope meaning undefined.
- OAuth carries no statement about who the user is, and the OAuth 2.1 draft’s own introduction warns against using an access token plus a proprietary API as a substitute for authentication.
- The authorization code flow separates a short-lived code that travels through the browser from an access token that is fetched over a direct back-channel call, so the valuable credential never crosses the public path.
- PKCE, defined in RFC 7636 of September 2015, has the client generate a 43 to 128 character code verifier, send only its SHA-256 hash as the code challenge, and prove possession of the verifier when redeeming the code.
- The
S256transformation is mandatory to implement and must be used by any capable client, becausecode_challenge_methoddefaults toplainwhen the parameter is absent. - PKCE was created against authorization code interception on mobile devices, where a malicious app can claim the same private-use URI scheme, but it now defends every client type against authorization code injection.
- OAuth 2.1 requires the code challenge from all clients, with one narrow exception for confidential clients whose correct use of the OpenID Connect
noncethe server can be sure of, and even then still recommends it. - Redirect URIs must be registered in full including the path and compared by simple string equality per RFC 3986 section 6.2.1, with a port-only exception for loopback redirects used by native apps.
- An open redirector anywhere on the client’s own domain defeats correct redirect URI validation by bouncing a legitimately delivered code onwards to an attacker.
- The
stateparameter defends against cross-site request forgery on the callback by binding the response to the browser session that started the request; the requirement remains even though PKCE can now satisfy it. - The
nonceparameter belongs to OpenID Connect rather than OAuth, and theissparameter from RFC 9207 of March 2022 defends against mix-up attacks when a client talks to several authorization servers. - The client credentials grant is for confidential clients acting on their own behalf with no user present, and it is misused whenever a service uses it to act as arbitrary users.
- The device authorization grant, RFC 8628 of August 2019, moves consent to a second device using a short user code and a polling loop whose responses include
authorization_pendingandslow_down. - The implicit grant was removed because front-channel access tokens leak through browser history, can be injected with no second step to catch it, and cannot be sender-constrained.
- The resource owner password credentials grant was removed because it exposes the password to the client and prevents the authorization server from applying multi-factor, risk-based or passkey authentication.
- Meaningful consent depends entirely on scope design, which OAuth leaves to the authorization server, and the Google Docs worm of 3 May 2017 showed that an attacker-chosen application name can defeat a technically flawless consent screen.
- As of August 2026 OAuth 2.1 remains an Internet-Draft at revision 15 dated 2 March 2026, with an IESG submission milestone of December 2026, and its security content is already normative in RFC 9700, BCP 240, of January 2025.
- OAuth delivers a permission and never an identity, which is why the login button that everyone attributes to OAuth actually depends on OpenID Connect, the subject of chapter 41.
Chapter sources: RFC 6749, “The OAuth 2.0 Authorization Framework”, D. Hardt (Ed.), October 2012, sections 1.1, 1.2, 1.3, 2.1, 3.1.2, 4.1.1 to 4.1.4, 4.2, 4.3, 4.4, 10.12 and 10.15; RFC 6750, “The OAuth 2.0 Authorization Framework: Bearer Token Usage”, M. Jones and D. Hardt, October 2012; RFC 5849, “The OAuth 1.0 Protocol”, E. Hammer-Lahav (Ed.), April 2010, Informational, obsoleted by RFC 6749; RFC 6819, “OAuth 2.0 Threat Model and Security Considerations”, T. Lodderstedt (Ed.), M. McGloin and P. Hunt, January 2013; RFC 7636, “Proof Key for Code Exchange by OAuth Public Clients”, N. Sakimura (Ed.), J. Bradley and N. Agarwal, September 2015, sections 1, 4.1 to 4.6 and appendices A and B; RFC 8252, “OAuth 2.0 for Native Apps”, W. Denniss and J. Bradley, BCP 212, October 2017, sections 5, 7.1, 7.2, 7.3 and 8.1; RFC 8628, “OAuth 2.0 Device Authorization Grant”, W. Denniss, J. Bradley, M. Jones and H. Tschofenig, August 2019, sections 3.1 to 3.5; RFC 8414, “OAuth 2.0 Authorization Server Metadata”, June 2018; RFC 7591, “OAuth 2.0 Dynamic Client Registration Protocol”, July 2015; RFC 7662, “OAuth 2.0 Token Introspection”, October 2015; RFC 7009, “OAuth 2.0 Token Revocation”, August 2013; RFC 9126, “OAuth 2.0 Pushed Authorization Requests”, T. Lodderstedt, B. Campbell, N. Sakimura, D. Tonge and F. Skokan, September 2021; RFC 9207, “OAuth 2.0 Authorization Server Issuer Identification”, K. Meyer zu Selhausen and D. Fett, March 2022; RFC 9396, “OAuth 2.0 Rich Authorization Requests”, T. Lodderstedt, J. Richer and B. Campbell, May 2023; RFC 9700, “Best Current Practice for OAuth 2.0 Security”, T. Lodderstedt, J. Bradley, A. Labunets and D. Fett, BCP 240, January 2025, sections 2.1.1, 2.1.2, 2.4, 4.1.1, 4.1.3, 4.3.2, 4.4, 4.5, 4.8, 4.11 and 4.14.2; the Internet-Draft “The OAuth 2.1 Authorization Framework”, draft-ietf-oauth-v2-1-15, D. Hardt, A. Parecki and T. Lodderstedt, 2 March 2026, sections 1.1, 1.7, 1.8, 2.1, 2.3.1, 2.3.3, 3.2.3, 4.1.1, 4.1.2, 4.1.3, 4.2, 4.3.3, 7.5.1, 7.12, 7.14 and 10, with revision history from the IETF Datatracker showing draft-parecki-oauth-v2-1-00 of 7 March 2020 and working group adoption on 30 July 2020; the Internet-Draft “OAuth 2.0 for Browser-Based Applications”, draft-ietf-oauth-browser-based-apps-27, in the RFC Editor queue as of August 2026; RFC 3986, “Uniform Resource Identifier (URI): Generic Syntax”, January 2005, section 6.2.1 for simple string comparison; the oauth.net project history recording that OAuth began around November 2006 with Blaine Cook’s work on Twitter’s OpenID implementation and that the OAuth Core 1.0 final draft was released on 3 October 2007; OAuth Security Advisory 2009.1 of 23 April 2009 describing the session fixation flaw that produced OAuth Core 1.0 Revision A; Facebook’s security updates of 28 September 2018 and 12 October 2018 on the “View As” access token breach, revising 50 million affected accounts to about 30 million with tokens actually stolen; the Irish Data Protection Commission decision of 12 December 2024, announced 17 December 2024, imposing fines totalling 251 million euro on Meta Platforms Ireland over that breach; and contemporaneous reporting of the Google Docs OAuth phishing worm of 3 May 2017, including Google’s statement that the attack was stopped in approximately one hour and affected fewer than 0.1 per cent of Gmail users.