Three providers, three OAuth2s
We needed to let users sign in with three different identity providers: a big consumer platform everyone has an account with, a partner whose API we consumed, and an enterprise SSO that a few large customers insisted on. All three advertised OAuth 2.0 support. I budgeted a week, because OAuth is a standard, and standards mean you write the integration once.
It took most of a month. The consumer provider returned an opaque token and made you call a userinfo endpoint to learn anything about the user. Our partner's API handed back a JWT you were expected to validate yourself, with claims in a shape only their docs described. And the enterprise IdP wanted an extra audience parameter, scoped permissions named in a convention nobody else used, and a refresh flow with its own quirks. Three integrations that shared a vocabulary and almost nothing else. That was the week I understood that OAuth 2.0 is not really one thing.
The spec tells you in the title
The document everyone calls the OAuth standard is RFC 6749, and its title is "The OAuth 2.0 Authorization Framework." Not protocol. Framework. The people who wrote it were precise about that word, and we spent a decade ignoring it. A protocol tells two parties exactly how to talk so that any correct implementation interoperates with any other. A framework hands you a vocabulary and a box of building blocks and wishes you luck.
OAuth 2.0 standardized the nouns. Authorization server, resource server, access token, grant, scope, the shape of the authorization request. What it deliberately left open was most of the behavior: what a token looks like, how you validate it, what scopes actually mean, which grant you use, how errors come back. Two systems can both be flawlessly compliant and still be unable to talk without custom code on each side, because compliance here was never defined as interoperability.
What "framework" means in practice
The open questions are not edge cases. They are the center of the thing. A token can be an opaque string you have to introspect, or a JWT you validate locally, and the spec blesses both without telling you which one you are holding. Scopes are free-form strings whose meaning each provider invents, so read on one platform and read:user on another are unrelated decisions that happen to rhyme.
The grant types are a menu, not an instruction. Authorization code, the implicit flow, client credentials, resource owner password, device code, each with its own dance and its own caveats. Refresh tokens are optional and behave differently everywhere they exist. Token introspection is a separate RFC that some providers implement and others ignore. Every one of those choices is a fork in your code, and every provider walks a different combination of the forks.
The looseness is where the footguns live
A framework that leaves the security-critical parts optional is a framework that ships footguns by default. The implicit flow returned access tokens directly in a redirect URL, where they landed in browser history and server logs, and it survived for years as a blessed option before the community talked people off it. Bearer tokens are exactly what they sound like: whoever holds one can use it, with no proof they are the rightful owner, so a single leak is a full compromise.
The rest of the footguns come from optional safety you were trusted to add yourself. Exact redirect_uri matching is the difference between a safe flow and an open redirect that leaks codes, and the spec long permitted loose matching. The state parameter that blocks cross-site request forgery is something you have to remember to send and verify. PKCE, the extension that protects the code exchange, started life as optional. None of these were hard to get right. They were just easy to skip, and the framework let you skip them.
OAuth 2.1 is the spec admitting it
The most telling development of the last few years is OAuth 2.1, which is less a new version than the ecosystem consolidating its hard lessons back into the document. It makes PKCE mandatory for authorization code flows. The implicit flow and the resource owner password grant are removed outright. Redirect URIs now require exact string matching. Read that list as what it is: the framework quietly deleting the options that kept blowing up in production.
That is a framework growing toward being a protocol. Each thing 2.1 makes mandatory is one fewer fork in your integration and one fewer way to be fully compliant and insecure at the same time. The direction of travel is away from "here are your options" and toward "here is what you do," which is the shape OAuth probably should have shipped in to begin with.
The flexibility was not malice
It is easy to be unfair to OAuth 2.0 from here, so I want to be honest about why it looks the way it does. In 2012 it had to cover a server-rendered web app, a mobile client that cannot keep a secret, a smart TV with no keyboard, and a backend talking to another backend, all in one document. Those situations genuinely need different flows. A single rigid protocol would have failed half of them on day one.
So the working group chose flexibility, and that was a defensible call for the problem in front of them. The cost was simply moved downstream. Every team that integrated OAuth afterward inherited the job of choosing correctly among the options, and plenty of them reached for the convenient choice over the safe one. The framework was reasonable. The bill for its generality just landed on someone else's desk.
What I actually do
I treat OAuth 2.0 as what it says it is, a framework I have to narrow down before I use it. My default is one flow, authorization code with PKCE, used everywhere, including for clients that the older advice said could skip PKCE. A vetted library or a managed identity provider does the token handling, never my own code, because the footguns sit exactly in the parts that feel simple enough to write yourself.
The mistake is reading OAuth 2.0 as a protocol and expecting it to make your decisions for you. It will not. It hands you a box of parts and a glossary, and the safety of the result depends entirely on which parts you pick out of the box. Treat it as a framework, pin every option to its strict end, and most of its sharp edges stop being your problem.
Comments (0)