OAuth 2.0 is a framework commonly used in modern web applications for authorization (the process of verifying what resources the user have access to). However it can also support authentication (the process of verifying “who you are”) for example by using OpenID which is built on top of OAuth2.
Its purpose is to allow a third-party application (the client application) to access users details and other resources of another application (the application that owns the user’s data). Using this framework the client application doesn’t need to get the users credentials which are managed only by the OAuth authorization server.
There are many workflows of OAuth2 (they are also referred as “grant types”), but the most common is called “the authorization code flow”. This is commonly used by web applications and mobile apps and it works like this:
- The user tries to access the client application that redirects the user to the OAuth authorization server
- The OAuth server shows a prompt with the required authorizations (it also handles the login if the user is not yet authenticated)
- The user approves the authorizations and it’s redirected back to the application with an authorization code (it can be in the URI query string, as a parameter or as a fragment) which is valid only once (and for a small amount of time, usually 30-60 seconds)
- The client application exchanges this authorization code with the OAuth server to get an access token
- This access token can be used by the client application to access required user’s data or resources in behalf of the user
The following image shows an high level overview of the interactions just described (in the context of a printing service which needs to access Google Drive protected resources).
The following entities are involved in OAuth2:
- the client application
- the resource owner (usually the user)
- the authorization server (the server that authorizes the user)
- the resource server (the server that holds the protected resource owned by the user)
In order to support several uses cases and due to its flexibility, OAuth2 can be complex and it’s not always properly implemented (on both client and server sides) and/or there might be misconfigurations that can lead to critical vulnerabilities (including complete account take over). In the following sections I’m going to describe the most common ones.
Missing state check (CSRF)
Let’s say we have an application that allows the users to link their account to third-party accounts (such as Google, Facebook, LinkedIn, etc..) and let them authenticate with those accounts.
When we click on “Link Your Google Account” an OAuth2 flow starts and the client application (the one requesting to connect to your Google account) will be able to login with his Google account.
However, a malicious user could craft a URL that when clicked, will force the victim to link his account to the attacker’s Google account, hence by performing a complete account takeover.
In order to prevent this vulnerability, OAuth2 specifies a state
parameter that should be validated by the requesting application (the client) before proceeding with the authorization.
To start the authorization flow, the client application constructs the following URL:
https://authorization-server.com/auth
?response_type=code
&client_id=29352915982374239857
&redirect_uri=https%3A%2F%2Fclient-app.com%2Fcallback
&scope=openid+profile+email
&state=dca35ec4-7e78-4357-a6ba-c481ebb57b6d
As you can see, there is a state
parameter which protects against CSRF attacks. When the authorization server replies with the authorization code, it will also include the state
parameter. At this point the client application can verify that the state
parameter matches the ones that he sent with the previous request.
This way, if an attacker send a malicious link, he can’t predict the state
parameter and the client application will stop the authorization process.
Open Redirect in redirect_uri parameter
During the authorization code flow, the OAuth server needs to redirect the browser to a specific callback of the client application which can handle the code and exchanges it for a valid access token with the OAuth server. This happens transparently from the user’s perspective and usually happens in a backend-to-backend communication with the usage of a client_secret
, a secret value only known by the backend server and the OAuth server so that only the allowed service can get valid tokens (these clients are also called “Confidential Clients").
In a regular flow, the redirect_uri
specifies the client application callback, as shown in the following request:
https://authorization-server.com/auth
?response_type=code
&response_mode=query
&client_id=my-app-id
&redirect_uri=https%3A%2F%2Fclient-app.com%2Fcallback
&scope=openid+profile+email
&state=dca35ec4-7e78-4357-a6ba-c481ebb57b6d
The previous request will trigger the OAuth server to redirect the browser to the following location (notice that the authorization code
is appended as a URL query parameter, together with the state
parameter):
HTTP/1.1 302 Found
...
Location: https://client-app.com/callback?state=e5886c7a-92f7-4fef-8bb1-1b71fb7b8196&code=ea7375e8-5dda-4931-121a-92b1534017e2.7c0151e2-7484-4f91-b93e-5dc77f2dcb06.286ed716-2453-49ff-b806-9bae69e43dce
...
The browser will follow the redirect and the client application will exchange the code
for a valid access token with the OAuth2 authorization server.
Now, what happens if the redirect_uri
is not validated from the OAuth2 server? In this case, an attacker can craft a link that will trigger the victim to navigate towards a malicious attacker controlled website, which will be able to steal the code and take over the client application account of the victim user. Even if the attacker doesn’t know the client_secret
he could just send the code (which is valid only once) to the client application’s legitimate callback and he will be logged in with the victim’s account.
Validating the redirect_uri
parameter is crucial to prevent complete account takeover. The new version of the standard OAuth 2.1 also requires that redirect_uri
must be compared using exact string matching (wildcards are not allowed anymore for better security).
Implicit Flow
For mobile applications and SPA (Single Page Applications) also known as “Public clients”, it’s not possible to securely store the client_secret
(because all the code runs client-side and it’s visible by the users) so OAuth 2 provided another grant type called “Implicit Flow”. Using this flow, there is no code/token exchange step and the access token is immediately returned in the redirect.
It means that using this mode, the redirect directly includes the access token, as follows (in this case in the URL fragment):
HTTP/1.1 302 Found
...
Location: https://client-app.com/#access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
...
This implies that any third party JavaScript that we load in our application could be able to read (and exfiltrate) the access token.
This of course has several security implications and it has been now deprecated in favor of a more secure flow called PKCE exchange (which is now required for all OAuth 2.1 clients).
PKCE is an extension used to ensure that if a malicious party intercepted the authorization code, he won’t be able to use it. It basically works like this:
- the client generates a secret random string called
code_verifier
- it also calculates the
sha256
of thecode_verifier
(and base64-URL-encode the resulting string) which is called thecode_challenge
- it stores this code for later use (for example in a cookie)
- the client includes this
code_challenge
to the authorization code flow request and get back thecode
token from the authorization server - the client issues a POST request to get the access token, including all the required parameters plus the
code_verifier
- the authorization server calculates the
sha256
of thecode_verifier
and checks if the resulting value matches the thecode_challenge
that was issued with the first authorization request - if it matches, the server knows that the request is coming from a legitimate client and it will respond with an access token
- if it doesn’t match, the request is denied
If you want to test and better understand the PKCE flow, you can use the following link: https://www.oauth.com/playground/authorization-code-with-pkce.html
Also notice that there are other ways for an attacker to leak the access token: for example the implicit flow send the authorization token with the URL, meaning that the token can be leaked via the referer header, via proxy or log files.
Conclusion
OAuth is a powerful and flexible framework that allows developers to delegate access control to an OAuth authorization server, however, due to its complexity, it might be easy to make mistakes or misconfiguration when dealing with it. Because of that, it’s really important to understand how it works and deeply test the application to avoid critical vulnerabilities.