OAuth2 requires clients to register allowed values for redirect_uri.

Otherwise, an attacker could receive the authorization code. For example:


However, isn't this attack mitigated by requiring clients to use client_secret when using the authorization code to retrieve the access token?

Without client_secret, evil.org couldn't actually do anything harmful. Right?


1 Answer 1


OAuth is a somewhat overengineered family of protocols, suitable for many scenarios and rarely relying on only one security measure where it can instead have several. This is usually a good thing, but it does make it hard to understand and hard to implement correctly.

In this case, there is a way to exploit the incorrect redirect even when client secrets are used and not known to the attacker. Imagine you own a domain, pauldraper.com. It is an OAuth2 client, with a client secret for a third-party OAuth authorization server, oauthserver.com. Your webserver listens at the endpoint https://pauldraper.com/oauth/callback for OAuth2 Authorization Code redirections from oauthserver.com.

Oauthserver.com has the usual set of endpoints. To initiate an OAuth flow, you direct a user's browser to send a request to https://oauthserver.com/authorize (expects parameters response_type=code, client_id=paul_draper_com, redirect_uri=https://pauldraper.com/oauth/callback, and a scope and a state). Note that those are all public values that could be seen by anybody who monitors what their browser does when using oauthserver.com. The browser request to /authorize will initiate the process of first ensuring the user is logged into oauthserver.com, and then that they approve the oauth authorization and scope.

After that (if both are true), the user's browser is redirected to the redirect URI. Normally this is supposed to be a fixed set of allowed URIs, but we're discussing what happens if it's not. The redirection of the user's browser from oauthserver.com to the supplied redirect URI also involves a couple of parameters: code (the authorization code for this user, an unpredictable security token that should be single-use and short-lived) and state (the exact same string that was passed in the request to /authorize, used for CSRF protection).

Normally, those parameters go to your OAuth redirect listener at https://pauldraper.com/oauth/callback (because normally that's the specified redirect_uri parameter value in the request, and normally the OAuth server wouldn't accept anything else). Your server then makes a POST request directly (not via the user's browser) to the other main OAuth endpoint on oauthserver.com, at https://oauthserver.com/token. This request contains a bunch of parameters: grant_type=authorization_code, client_id=paul_draper_com, client_secret (value is your client secret, generally a high-entropy random string), and code (value is the authorization code supplied in the redirect). This is the only place your client_secret is used, and it doesn't go through the user's browser, so it stays secret.

The OAuth server responds to this direct POST request (the one at https://oauthserver.com/token with a response that contains an access token and some metadata. You can then use that access token to do various things, but the usual uses are either to access some resources that the user owns on another site (the resource server, which may or may not be the authorization server), or to get the user's identity so that your site, pauldraper.com, knows who the user is and presumably grants them access to an account or other resources based on that authentication.

OK, so that's the happy path, where things work as expected. Now, let's imagine that the redirect_uri parameter in the initial request to the /authorize endpoint is not restricted. An attack would look like this:

  • There are two users, EvilUser and VictimUser. VictimUser has an account on oauthserver.com, which is used to access their account on pauldraper.com. EvilUser wants to break into VictimUser's account on pauldraper.com.
  • EvilUser sets up a honeypot site, notevil.com, which claims to offer free cryptocurrency if you sign up. It allows users to sign up via their oauthserver.com accounts.
  • However, notevil.com doesn't actually have its own OAuth client ID and client secret. Instead, it redirects users to https://oauthserver.com/authorize?response_type=code&client_id=paul_draper_com&redirect_uri=https%3A%2F%notevil.com%2Fcallback&state=12345&scope=identity. That is, EvilUser's site is using YOUR client_id, but redirecting to THEIR OWN server!
  • VictimUser thinks that some free cryptocurrency sounds cool, so they click the button to register via oauthserver.com account, and their browser launches into the OAuth flow at the URL above.
  • VictimUser gets redirected back to notevil.com, to the following URL: https://notevil.com/callback?code=7i1GRmUQ0xkm4FfmbChPc1rI1nCiSCoyXZTsJ88pXqN9VOvd&state=12345. That code is only useful when combined with your (that is, pauldraper.com's) client_secret, which notevil.com doesn't have.
  • The notevil.com site shows VictimUser a "working..." message so that they don't realize the problem yet.
  • EvilUser immediately makes their own attempt to log in to pauldraper.com via oauthserver.com. Their full OAuth request URL looks like this: https://oauthserver.com/authorize?response_type=code&client_id=paul_draper_com&redirect_uri=https%3A%2F%pauldraper.com%2Foauth%2Fcallback&state=qw2YXTvEuEDEdD3O&scope=identity`. Note that this one passes the expected callback_URI, and also a "real" state parameter, rather than the trivially predictable one above.
  • EvilUser doesn't actually want to log into their own account, though, and they don't have to anyhow; they already have a valid code! EvilUser doesn't bother to interact with oauthserver.com at all.
  • Instead, EvilUser generates a request to pauldraper.com that looks like it's a redirect from oauthserver.com, but is actually crafted using the code from VictimUser's redirection to notevil.com! The URL is https://pauldraper.com/oauth/callback?code=7i1GRmUQ0xkm4FfmbChPc1rI1nCiSCoyXZTsJ88pXqN9VOvd&state=qw2YXTvEuEDEdD3O. Note that this combines a valid state (for EvilUser's session on pauldraper.com, as it should be) with a valid code from oauthserver.com for use with pauldraper.com.
  • Your server at pauldraper.com takes this valid-looking request that seems to be an OAuth redirect, and sends a POST request directly to https://oauthserver.com/token with the parameters grant_type=authorization_code &client_id=paul_draper_site &client_secret=ECGrHn2DZwgugrLuwGR8fBUNY30PDfKYJzFxae1ff9FKWeuR &code=7i1GRmUQ0xkm4FfmbChPc1rI1nCiSCoyXZTsJ88pXqN9VOvd. Your site does this itself, sending the victim's code and its own client_secret on behalf of the attacker!
  • The response from oauthserver.com contains a valid access token, which pauldraper.com uses to get the identity of the oauthserver.com user associated with that code - that is, the identity of VictimUser - and gives the current user session - i.e. EvilUser's session - access to VictimUser's account!
  • VictimUser eventually gets an error message saying all the free cryptocurrency has been handed out right now, but try again next week! After all, EvilUser can't actually log into oauthserver.com as them directly; so to maintain access to VictimUser's account long term, they want the victim to keep coming back...

There you have it! Using nothing but a bad redirect_uri to steal a user's code, and then using the real redirect_uri with the stolen code to gain access to the victim's account without ever knowing the client_secret!

There are some potential mitigations here. One is that the code could include a way to specify which redirect_uri it was used with, such that the legitimate site (pauldraper.com in the example) can detect the code has been transferred from another site. This would require the authorization server (oauthserver.com) to implement a non-standard feature, though, which is way more work than just restricting the allowed value of redirect_uri correctly! Alternatively, the OAuth client (pauldraper.com) could pass along the redirect_uri that it thinks was used when doing the POST to /token, and trust that the authorization server will in fact only issue tokens if the code was sent to that redirect_uri and not a different one.

At the end of the day, the best solution is for the authorization server to restrict the allowable redirect URIs to ones that you (the client) own. An authorization server that doesn't implement this restriction is unsafe and must not be trusted!

  • OK, I thought it was clear that the attacker doesn't know (and doesn't need to know) the client secret, but apparently not. I've edited the answer to be really explicit about how the attack works, and who knows what. You can see that the attacker uses the actual OAuth client - the one that knows the client_secret - to exchange the (stolen) authorization code for (somebody else's) token.
    – CBHacking
    Mar 13, 2023 at 12:09
  • thank you for the answer! In summary, if the attacker obtain the victim's authorization code, it can trick the OAuth client accessing to the victim's resource. The attack never obtains direct access to the resource but can has indirect access via influence/control of the OAuth client. It seems the new PKCE extension would prevent this sort of attack. Apr 26, 2023 at 2:48
  • Yes it does, but it is - as you say - new. Also, like I said above, OAuth is prone to having multiple layers of security wherever practical.
    – CBHacking
    Apr 26, 2023 at 5:33
  • The redirect of the authorization code that is bound with the valid client ID should not have happened to begin with. It seems the authorization server can simply verify the redirect URL parameter against those registered by the client whose ID is presented.
    – eel ghEEz
    May 15 at 21:24
  • @eelghEEz Did you even read the thing you're commenting on? "Normally this is supposed to be a fixed set of allowed URIs, but we're discussing what happens if it's not." "At the end of the day, the best solution is for the authorization server to restrict the allowable redirect URIs to ones that you (the client) own. An authorization server that doesn't implement this restriction is unsafe and must not be trusted!"
    – CBHacking
    May 16 at 2:10

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .