Computing History · Lessons from Failures

OAuth 2.0 and the Implicit Grant: Removing a Token from the Browser

Why the implicit grant existed, what the browser redirect model made impossible to secure, and how the password and hybrid grants traded a front-channel leak for a different attack surface rather than eliminating it.

The implicit grant returns an access token directly in the redirect fragment of an authorization response. It was written into the original OAuth 2.0 specification as a convenience for clients that could not keep a secret, specifically browser-based applications that had no server to hold one. It was documented, widely implemented, and then formally deprecated in a best-practice document years later, not because the flow was implemented incorrectly but because its security argument rested on a property the browser does not have.

The redirect is the interface

OAuth’s authorization code flow requires the client to register a redirect URI with the authorization server. The server, after the user authenticates and consents, sends the browser to that URI with a code. The code is short-lived and single-use, and it is exchanged for an access token by a direct request from the client’s server to the authorization server’s token endpoint, authenticated with a client secret.

The security of that exchange rests entirely on the code being useless to anyone who does not possess the client secret. An attacker who observes the redirect obtains the code, but the token endpoint rejects the exchange. The redirect URI therefore only needs to be protected against delivery to the wrong party, not against disclosure, and it may travel through the user agent, through logs, and through the referrer header without granting access.

The implicit grant removes the exchange. The authorization server returns the access token as a fragment parameter, and the client-side script reads it. There is no secret, because a browser application has no way to keep one: any secret shipped in the application’s code is readable by every user. The design constraint is genuine and the specification’s own threat model document names it. The concession was to shorten the token’s lifetime and to rely on the token being delivered to the right script.

The fragment is not a secret channel

A URL fragment is not sent to any server, which the original specification treats as a mitigation: the token does not appear in server logs or in the referrer header. What it does not prevent is any script in the page reading it, and the page necessarily loads scripts from several origins in any non-trivial application.

The attack that the threat model document identifies is redirection: an attacker registers a client whose redirect URI is under the attacker’s control and induces the authorization server to send the token there. The countermeasure is exact redirect URI matching, and where the implementation allows prefix or substring matching, a URI such as https://client.example/callback.attacker.example or one with an appended path segment passes the check. The implicit grant has no second factor to fall back on when the comparison is wrong, because the token is already in the attacker’s hands at the moment of delivery.

The second attack class is token leakage through the scripts in the page. A third-party script that reads document.location or window.location.hash reads the token. There is no isolation between the resource owner’s token and any code running in the origin, which is the same-origin model’s whole premise. This is not a flaw in OAuth; it is a consequence of putting a bearer credential into a context where arbitrary code executes.

Bearer tokens make the leak permanent until expiry

The token obtained by the implicit grant is a bearer token: possession is authority, with no proof that the presenter is the party the token was issued to. The specification’s own definition of the token type states the consequence directly, that any party in possession of a bearer token can use it, and that no additional cryptographic verification is required.

That property means the window of exposure is the token’s lifetime, and the specification’s mitigations are all variations on shortening it. Short lifetimes push the cost into refresh: a client with a fifteen-minute access token must refresh constantly, and the refresh token, if issued to the browser, has the same leakage property with a much longer window. The implicit grant does not issue refresh tokens, which is a mitigation and also a reason applications that need to stay signed in could not simply adopt it.

The comparison with a proof-of-possession token is instructive. A token that requires the presenter to hold a private key and sign a challenge is useless to an attacker who intercepts the bearer bytes. OAuth 2.0’s original specification did not define such a mechanism, and the profile that later added one requires the code exchange, because the flow needs a point at which the client can prove possession before the token is used. The implicit grant has no such point by construction.

Why the workaround arrived instead of a fix

The practical answer to the implicit grant’s weaknesses was to restore the code exchange with a proof that does not require a secret. Proof Key for Code Exchange, specified as an extension, has the client generate a random verifier, send its hash as a code challenge with the authorization request, and send the verifier with the code at the token endpoint. An attacker who intercepts the code cannot complete the exchange without the verifier, which never left the client’s memory.

The mechanism addresses redirection and observes the leak without assuming a confidential client. It does not address the browser holding the resulting token, which remains in memory and in script reach. It also does not address the fragment problem, because it brings the code back through the redirect and the code is now in the fragment or the query depending on the response mode, and the interception of the code has to be prevented by the same redirect matching.

The extension is a chain of additions to a flow whose security model was fixed at specification time. Each addition is sound and each one is separate: the code challenge, the response mode, the state parameter’s binding function, the nonce for the hybrid flow. The resulting stack works and it illustrates the cost of specifying an interface before the threat model was written down.

What the hybrid flow got right and wrong

The hybrid response type returns a code from the authorization endpoint and a token from the front channel in the same response. The rationale is that a client can use the token immediately for a user interface that needs a quick answer, and use the code to obtain a better-scoped token or a refresh token in the back channel. The original specification’s own example of the flow is straightforward and the flow does reduce perceived latency for the client.

The security property it preserves is the binding: the code is bound to the same session as the token, so a party that obtains one in a redirect does not automatically obtain the other. The attack it introduced is the exposure of the token in the redirect, which is the implicit grant’s problem retained, plus the possibility that the token leak is exploited before the code exchange completes, since the token is in the attacker’s hands immediately.

Both of those flows have since been characterized in a best-practice document that recommends against both. The document, published as an RFC after a long review period, says that clients should use the code flow with PKCE and should not use the implicit flow at all, and it names the specific reason: the access token is exposed in the browser’s URL, and the flow offers no mechanism for the client to prove that it is the intended recipient.

The detail that made the mistake visible

The reason the implicit grant could be specified with confidence is that its security argument did not include the browser’s threat model. The original framework’s text says the client is typically implemented in a browser using a scripting language and cannot keep a secret, and it treats the shortened token lifetime as sufficient compensation. A reader comparing the two flows sees one with an exchange guarded by a secret and one without, and the second one is described as being for the case where the secret is impossible.

The threat model document was published a year later and enumerates the attacks in detail. Its recommendations for the implicit grant are to use the code flow, and where that is impossible, to shorten lifetimes and to restrict the client. The specification’s flow was already deployed by then. This ordering, framework first and threat model second, is the most transferable fact in the record: a protocol’s security posture is fixed by the client classes it decides to admit, and admitting a class whose trust assumptions are not stated commits the protocol to those assumptions.

The residual problem after the replacement

The current best practice does not restore a confidential client. A browser application still cannot hold a secret, so it uses the code flow with PKCE, which makes the code theft-resistant but not the code unobservable. The token it receives still lands in the browser, and every mitigation after that operates at the application layer: storing the token in memory rather than in local storage, restricting script origins, and using a backend-for-frontend proxy that holds the token server-side and forwards only cookies.

That last one is the honest conclusion. The application that survives the implicit grant’s removal is one where the browser holds no token at all, which is the architecture the implicit grant was introduced to avoid because it assumed every client had a server. The assumption was correct for the applications being written; those applications, at the time, were increasingly written as single-page clients with no server of their own.

The mechanism that would have served better at specification time was the one that arrived as an extension: a public client is not a client without proof, it is a client whose proof cannot be a shared secret. Specifying the verifier and challenge alongside the code flow would have cost one round of review and removed the need for a flow that the framework has since had to deprecate. The comparable case at the transport layer is traced in X.400 Versus SMTP: When a Better-Designed Messaging System Loses: an interface that divides clients into classes at the moment of specification sets the security or interoperability argument for every later revision.

What the record documents is narrower than a general rule. A credential returned through a channel the recipient does not control must be a credential that is useless outside that channel. The implicit grant returned a credential usable by anyone, through a channel readable by anyone with script execution in the page, and the specification’s response was to make the credential expire sooner. Expiry shortens the exposure; it does not prevent it. For a bearer-token design the question is not how long the exposure lasts but whether a second party can use what it sees.

References

  1. RFC 6749: The OAuth 2.0 Authorization Framework — IETF
  2. RFC 6819: OAuth 2.0 Threat Model and Security Considerations — IETF
  3. RFC 7636: Proof Key for Code Exchange by OAuth Public Clients — IETF
  4. OAuth 2.0 Security Best Current Practice — IETF