Testing for OAuth Authorization Server Weaknesses
Summary
OAuth stores the identities of users and their corresponding access rights with the Authorization Server (AS). The AS plays a crucial role during the OAuth flow as it grants clients access to resources. To be able to do that securely, it must properly validate parameters that are part of the OAuth flow.
Failure to validate the parameters may lead to account takeover, unauthorized resource access and the elevation of privileges.
Test Objectives
- Identify weaknesses in the Authorization Server.
How to Test
In order to test for AS weaknesses, you will aim to:
- Retrieve credentials used for authorization.
- Grant yourself access to arbitrary resources through forceful browsing.
- Bypass the authorization.
Testing for Insufficient Redirect URI Validation
If the redirect_uri
is not properly validated, a link can be crafted that contains a URL pointing to a server controlled by an attacker. This can be used to trick the AS into sending an authorization code to the attacker. In the following example, client.evil.com
is used as the forged redirect_uri
.
https://as.example.com/authorize?client_id=example-client&redirect_uri=http%3A%2F%client.evil.com%2F&state=example&response_mode=fragment&response_type=code&scope=openid&nonce=example
If a user opens this link in the user agent, the AS will redirect the user agent to the malicious URL.
An attacker can capture the code
value passed in the spoofed URL and then submit it to the AS token endpoint.
The following request illustrates an authorization request that sends the redirect_uri
to the authorization server. The client client.example.com
sends an authorization request to the AS as.example.com
with the URL-encoded redirect URI http%3A%2F%2Fclient.example.com%2F
.
GET /authorize
?redirect_uri=http%3A%2F%2Fclient.example.com%2F
&client_id=example-client
&errorPath=%2Ferror
&scope=openid%20profile%20email
&response_type=code
&response_mode=query
&state=example
&nonce=example
&code_challenge=example
&code_challenge_method=S256 HTTP/1.1
Host: as.example.com
The AS responds with a redirect containing the authorization code. This can be exchanged with an access token in the token request. As shown below, the URL in the Location
header is the URI given in the previous redirect_uri
parameter.
HTTP/1.1 302 Found
Date: Mon, 18 Oct 2021 20:46:44 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 340
Location: http://client.example.com/?code=example&state=example
To test if the AS is vulnerable to insufficient redirect URI validation, capture the traffic with an HTTP intercepting proxy such as OWASP ZAP.
- Start the OAuth flow and pause it at the authorization request.
- Change the value of the
redirect_uri
and observe the response. - Investigate the response and identify if the arbitrary
redirect_uri
parameter was accepted by the AS.
If the AS redirects the user agent to the redirect_uri
you specified, the AS does not properly validate the redirect_uri
.
Additionally, see the Common Filter Bypass
section in Testing for Server-Side Request Forgery to identity common bypasses for redirect URI validation.
Testing for Authorization Code Injection
During the Authorization Code flow code exchange, a code is issued by the AS to the client and later exchanged against the token endpoint to retrieve an authorization token and a refresh token.
Conduct the following tests against the AS:
- Send a valid code for another
client_id
. - Send a valid code for another resource owner.
- Send a valid code for another
redirect_uri
. - Resend the code more than once (code replay).
Test Public Clients
The request sent to the token endpoint contains the authorization code. It is exchanged against the token. Capture this request with an HTTP intercepting proxy like OWASP ZAP and resend the request with modified values.
POST /oauth/token HTTP/1.1
Host: as.example.com
[...]
{
"errorPath":"/error",
"client_id":"example-client",
"code":"INJECT_CODE_HERE",
"grant_type":"authorization_code",
"redirect_uri":"http://client.example.com"
}
If the AS responds with an access_token
, the code was successfully injected.
Test Confidential Clients
As the OAuth flow for confidential clients is additionally protected by a client secret, it is not possible to directly submit an authorization code to the token endpoint. Instead, inject the authorization code into the client. This injected code will then be sent in the token request, issued by the confidential client together with the client secret.
First, capture an authorization code from the AS:
- Start the authorization code flow with user Alice. Pause when you receive a code from the AS.
- Do not submit the code to the client and keep note of the code and corresponding state.
Then, inject the code:
- Start the authorization code flow with user Mallory and inject the previously gathered code and state values for user Alice into the process.
- When the attack is successful, the client should now be in possession of an
authorization_token
that grants access to resources owned by user Alice.
GET /callback?code=INJECT_CODE_HERE&state=example HTTP/1.1
Host: client.example.com
[...]
Testing for PKCE Downgrade Attack
Under certain circumstances the PKCE extension can be removed from the authorization code flow. This has the potential to leave public clients vulnerable to attacks mitigated by the PKCE extension.
This can happen when:
- The AS does not support PKCE.
- The AS does not properly validate PKCE.
Both can be tested with an HTTP intercepting proxy like OWASP ZAP. Conduct the following tests:
- Send the authorization request without the
code_challenge=sha256(xyz)
andcode_challenge_method
parameter. - Send the authorization request with an empty value for the
code_challenge=sha256(xyz)
parameter. - Send the authorization request with a forged value for the
code_challenge=sha256(xyz)
parameter
The example below highlights the values to modify:
GET /authorize
?redirect_uri=http%3A%2F%client.example.com
&client_id=example-client
&errorPath=%2Ferror
&scope=openid%20profile%20email
&response_type=code
&response_mode=web_message
&state=example-state
&nonce=example-nonce
&code_challenge=MODIFY_OR_OMIT_THIS
&code_challenge_method=MODIFY_OR_OMIT_THIS
&prompt=none HTTP/1.1
Host: as.example.com
[...]
The AS should verify the code_verifier
value in the token exchange. To test:
- Send the token request without the
code_verifier
. - Send the token request with an empty
code_verifier
. - Send the token request with a valid
code_verifier
for a different authorization code.
POST /oauth/token HTTP/1.1
Host: as.example.com
[...]
{
"client_id":"example-client",
"code_verifier":"MODIFY_OR_OMIT_THIS",
"code":"example",
"grant_type":"authorization_code",
"redirect_uri":"http://client.example.com"
}
Testing for Consent Page Cross-Site Request Forgery
CSRF attacks are described in CSRF. OAuth can be attacked with CSRF.
To prevent CSRF attacks OAuth, leverages the state
parameter as an anti-CSRF token.
Other measures can prevent CSRF attacks as well. The PKCE flow mitigates CSRF. A nonce
value may act as an anti-CSRF token as well.
Test every request that contains one of the anti-CSRF parameters used by OAuth according to the tests described in the CSRF test cases.
The consent page is displayed to a user to verify that this user consents in the client accessing the resource on the users behalf. Attacking the consent page with CSRF may grant an arbitrary client access to a resource on behalf of the user. The steps of this flow are:
- The Client generates a state parameter and sends it with the consent request.
- The User Agent displays the consent page.
- The Resource Owner grants access to the Client.
- The consent is sent to the AS together with the acknowledged scopes.
Use an HTTP intercepting proxy like OWASP ZAP to test whether the state parameter is properly validated.
POST /u/consent?state=Tampered_State HTTP/1.1
Host: as.example.com
[...]
state=MODIFY_OR_OMIT_THIS
&audience=https%3A%2F%2Fas.example.com%2Fuserinfo
&scope%5B%5D=profile
&scope%5B%5D=email
&action=accept
Testing for Clickjacking
Clickjacking is described in Testing for Clickjacking. When the consent page is prone to clickjacking and the attacker is in possession of the client_id
(for public clients, or the client secret for confidential clients), the attacker can forge the user’s consent and gain access to the requested resource through a rogue client.
How to Test
For this attack to be successful, the attacker needs to load the authorization page in an iframe.
The following HTML page can be used to load the authorization page in an iframe:
<html>
<head>
<title>Clickjack test page</title>
</head>
<body>
<iframe src="http://as.example.com/auth/realms/example/login-actions/required-action?execution=OAUTH_GRANT&client_id=example-client" width="500" height="500"></iframe>
</body>
</html>
If successfully loaded, the site is vulnerable to clickjacking.
See Testing for Clickjacking for a detailed description of how such an attack can be conducted.
Testing Token Lifetime
OAuth has two types of tokens: the access token and the refresh token. An access token should be limited in the duration of its validity. That means it is short-lived: a good duration depends on the application and may be 5 to 15 minutes.
The refresh token should be valid for a longer duration. It should be a one-time token that gets replaced each time it has been used.
Test Access Token Lifetime Validation
When a JSON Web Token (JWT) is used as the access token, it is possible to retrieve the validity of the access token from the decoded JWT. This is described in Testing JSON Web Tokens. It is possible that the AS does not properly validate the lifetime of the JWT.
To test the lifetime of the access token, use an HTTP intercepting proxy such as OWASP ZAP. Intercept a request to an endpoint that contains an access token. Put this request in the repeater and let the targeted time pass. The validity of an access token should be between 5 and 15 minutes, depending on the sensitivity of the resources.
Such requests may look like the following example. The token could also be transported in other ways, for example, in a cookie.
GET /userinfo HTTP/1.1
Host: as.example.com
[...]
Authorization: Bearer eyJhbGciOiJkaXIiL[...]
Test for lifetime validation by sending the request after varying lengths of time have passed, for example, after 5 minutes, 10 minutes, and 30 minutes.
This process can be optimized by automating the steps and logging of the server’s response. When a response of HTTP status 403 (instead of HTTP status 200) is received, this can indicate that the access token is no longer valid.
Test Refresh Token Lifetime Validation
Refresh tokens have a longer validity period than access tokens. Due to their long validity, they should be invalidated after they are used in an exchange against an access token.
Refresh tokens are issued in the same token request where the access token is handed out to the client.
Use an HTTP intercepting proxy such as OWASP ZAP. Set up the test by doing the following:
- Retrieve a valid refresh token.
- Capture the request that is used to exchange the refresh token against a new access token.
- Send the captured request to the request repeater.
In the following example, the refresh token is sent as part of the POST body.
POST /token HTTP/1.1
Host: as.example.com
Cookie: [...]
[...]
grant_type=refresh_token
&refresh_token=eyJhbGciOiJIUz[...]
&client_id=example-client
Conduct the following tests:
- Send the refresh token and determine if the AS hands out an access token.
- Repeat the steps with the same refresh token to evaluate how often a single refresh token is accepted.
When a JWT is used as the refresh token, it is possible to retrieve the validity of the refresh token from the decoded JWT. This is described in Testing JSON Web Tokens. The refresh token may be valid for a longer period of time, but should have an expiry date.
Additional security can be gained with a theft detection mechanism. If a refresh token is used in a token exchange beyond its validity (or lifetime), the AS invalidates all refresh tokens. To test this mechanism:
- Send the refresh token and determine if the AS hands out an access token.
- Repeat the steps with the same refresh token until it is invalidated.
- Use the refresh token from the last token response
If all refresh tokens that were issued to the client for this resource owner are invalidated, the AS has token theft detection.
Related Test Cases
- Testing for Cross Site Request Forgery
- Testing for Client-side URL Redirect
- Testing for Server-Side Request Forgery
- Testing JSON Web Tokens
- Testing for Clickjacking
- Testing Cross Origin Resource Sharing
Remediation
Most of the attacks against OAuth AS can be mitigated by validating the existence and content of parameters during the authorization code and token exchange.
Restrict the time span and allowed usage for credentials such as the authorization code and refresh token. This can mitigate some types of attacks and also limits the use of such credentials for an attacker, if they are gained.
Proper configuration of security mitigation like CORS, anti-CSRF tokens, and anti-clickjacking headers can mitigate or limit the impact of attacks.
- Always validate if all parameters are present, and validate their values.
- Use the PKCE extension to properly secure the authorization code and token exchange.
- Do not allow fallback for security features like the PKCE extension.
- Restrict the lifetime of credentials.
- Use credentials only once where possible, e.g. the authorization code.
- Configure available security mitigation like CORS, anti-CSRF tokens, and anti-clickjacking headers.