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

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.

Prelude

Before stating what is missing, let us briefly look at how the OAuth2 "exchange" takes place.

  1. First is the authorization request which redirects the user to the provider:
GET /authorize
    ?response_type=code
    &client_id=...
    &redirect_uri=https%3A%2F%2Fexample.org%2Fcallback
    &...
  1. Once the user grants the scope, the callback is invoked with the authorization code. The server provided in the redirect_uri receives this code as follows.
https://example.org/callback
?code=Qcb0Orv1zh30vL1MPRsbm&...
  1. Now, at example.org's server side, the authorization code (which is basically a nonce) is exchanged for the "real" access token. The URL to exchange the authorization code for a token looks like this:
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.

Why is the `redirect_uri` sent again?

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:

  1. The attacker achieves to manipulate the URI in the first step as follows /authorize?...&redirect_uri=https://attacker.xyz/callback

  2. The attacker receives the authorization code https://attacker.xyz/callback?code=...

  3. 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?!

Here comes the trick!

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.

How likely is it to have a valid attacker 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.