OAuth2: Why should we validate the `redirect_uri` when exchanging the authorization code for an access token?

Search for a command to run...

No comments yet. Be the first to comment.
Hello folks, I'm pleased to announce the release of the v2.0 of my WebAuthn library! This library greatly simplifies the usage of passkeys by invoking the WebAuthn protocol more conveniently. It is open source, opinionated, dependency-free and minim...

The WebAuthn protocol is more than 200 pages long, it's complex and gets constantly tweaked. Moreover, the reality of browsers and authenticators have their own quirks and deviate from the official RFC. As such, all information on the web should be t...

How the Passwordless.ID development is currently in a slump.

Hello folks! Since I have little time to cater about Passwordless.ID lately, I wanted to at publish some little update. So here we go, the WebAuthn library (to enable passwordless login using passkeys) now also delivers more information about the "au...

What I like about the banner picture above is that it is a very useful analogy to our problem at hand. A database is nothing else than a glorified electronic version of it. Lots of small storage boxes containing some data, and identified by a label o...

Beware, technical article ahead!
OAuth 2 and OpenID are complex protocols. It's full of tiny details that are there for the sake of security and it's not always clear why some checks are necessary. This article is about such a detail.
Before stating what is missing, let us briefly look at how the OAuth2 "exchange" takes place.
GET /authorize
?response_type=code
&client_id=...
&redirect_uri=https%3A%2F%2Fexample.org%2Fcallback
&...
redirect_uri receives this code as follows.https://example.org/callback
?code=Qcb0Orv1zh30vL1MPRsbm&...
POST /token
?grant_type=authorization_code
&code=Qcb0Orv1zh30vL1MPRsbm
&redirect_uri=https%3A%2F%2Fexample.org%2Fcallback
Some parameters were omitted for brevity's sake, but it's sufficient for the upcoming explanation.
Sending the redirect_uri in the first step is obvious, it's to know where the authorization code should be sent to via redirect.
However, when exchanging the authorization code for a token, why send it again?
Let's assume an attacker manages to manipulate the redirect_uri of a victim. Let's also assume this manipulated redirect is accepted by the provider for whatever reason. The attack pattern may look like this:
The attacker achieves to manipulate the URI in the first step as follows /authorize?...&redirect_uri=https://attacker.xyz/callback
The attacker receives the authorization code https://attacker.xyz/callback?code=...
The attacker can now invoke /token?code=...&redirect_uri=https://attacker.xyz/callback or /token?code=...&redirect_uri=https://example.org/callback as it please.
It makes no difference since the attacker is in full control of what is sent as redirect_uri ...right?
Anyway, it's probably further protected by a client secret or PKCE. Is there a way around it?!
Let's assume Alice is the attacker and Vincent the victim.
Both Alice and Vincent have an account at example.org, which can for example use google photos to make collages of your family pictures.
Somehow Alice manages to manipulate the redirect_uri of Vincent, redirecting him to https://attacker.xyz/callback?code=ABC...
This attacker's website will probably also discreetly redirect back to example.org after grabbing the code. Vincent won't see example.org working properly, probably saying something went wrong, without suspecting anything bad.
On the other hand, Alice could go to example.org and use Vincent's code to complete the OAuth2 flow for herself, simply by pasting https://example.org/callback?code=ABC...
In other words, Alice will be able to impersonate Bob and make photo collages of Vincent's family.
This is of course just an example to illustrate the issue, but you certainly see the extent of this kind of exploit, which lets the attacker impersonate someone else.
Verifying the redirect_uri is there to prevent such "code stealing". It ensures such a triangle with an attacker in the middle did not take place by verifying that the code was indeed sent to the place the client app expected. Nothing more, nothing less.
redirect_uri?This cannot happen if a single redirect_uri is defined. It happens in scenarios where multiple callback URIs are defined (or if the server check is missing!) or where some loose matching takes place. Imagine for example a service which lets developers host their own apps behind a subdomain, providing authentication as a convenience. This could lead to a legitimate app goodapp.example.org being manipulated to leak the code to badapp.example.org, which might have a legitimate redirect_uri for example.org too. This could also be a larger organization, where some isolated app was hacked, and so on.