In a previous article written by Condatis senior developer Mikko Vuorinen, we’ve seen the importance of using PKCE (Proof Key for Code Exchange) for browser-based single page applications (SPA).
This article further highlights the importance of using PKCE for confidential clients and describes a possible attack demonstrated here by the senior security haxor Micah Silverman.
OAuth Client Types
The OAuth 2.0 RFC specifies two client types: public and confidential.
A public client is incapable of maintaining the confidentiality of its credentials, in other words, it’s not able to keep secret the client_secret that we use in the authorization code flow when the code is exchanged for the tokens. Such clients are SPAs and also native applications such as mobile applications. This is why PKCE is mandatory for public clients: for the authorization server, this is the only way to ensure that.
A confidential client, on the other hand, can safely maintain the confidentiality of its credentials. These are the clients implemented on a secure server where the access to the client_secret can be restricted. The client_secret is then passed by the client to the token endpoint along with the client_id and the Authorization Server can authenticate the client.
At first glance, it might seem that PKCE is not required for confidential clients. However, this is not true as we’ll see in detail below. The problem is that under certain circumstances, an attacker is still able to steal and use the victim’s code and use it to get the victim’s associated tokens.
Let’s start the attack description introducing the main actors and the components involved:
- Alice: A legitimate user who signs into the Auth Server and authorizes the Relying Party to access her protected resources. The victim of the attack.
- Eve: The attacker who wants to steal the authorization_code and obtain Alice’s session access_token.
- Auth Server: The authorization server responsible for authenticating Alice and issuing an authorization_code after her authorization.
- Relying Party: The web application that Alice authorizes to access her protected resources.
- Malicious browser extension: A malicious browser extension that Eve Eve tricks Alice and makes her install it (beyond the scope of this article). The extension is responsible for intercepting the requests between Alice’s browser and the Relying Party.
- Malicious API Server: An HTTP API hosted by Eve’s controlled server. It’s responsible for storing stolen authentication codes and provide them to Eve’s browser extension.
- Eve’s browser extension: The counterpart of Alice’s extension installed in Eve’s browser. It uses the Malicious API Server to retrieve and inject the stolen authorization_code in Eve’s session.
The first part of the attack: authorization code interception
From the above diagram, we can see that the attack starts when Alice’s browser follows the redirect to the Relying Party with the authorization_code. The malicious browser extension pauses and prevents the request to the Relying Party. It then posts the intercepted authorization_code to the malicious API server, and it finally redirects to a random Relying Party page, typically the home page. At this point, the Relying Party is not authorized to access any protected resources on behalf of Alice; therefore, she cannot perform any privileged operation in the web application.
However, she might think there was just a temporary issue and the extension can be implemented in such a way that the next time Alice tries, the redirect to the Relying Party will not be blocked and everything works as expected.
The second part of the attack: authorization code injection
The second part of the attack is focused on Eve, who first needs to go through a valid authentication flow with the Authorization Server to establish a valid session state. This time Eve’s extension browser pauses and prevents the request to the Relying Party. The extension retrieves Alice’s authorization_code by calling the malicious API server. It then replaces Eve’s authorization_code with Alice’s one in the redirect request. The forged redirect request gets sent to the Relying Party to successfully exchange Alice’s authorization_code for a valid access token that can be used to access Alice’s protected resources. The control is then returned to Eve’s browser, and she can now use the web application as if she was Alice.
How PKCE protects from the attack
The above diagram shows how the use of PKCE prevents the authorization code injection when Eve tries to perform the same flow described in the second part of the attack. This time the Relying Party generates a code_challenge and a related code_verifier. The code_challenge is included in the authorization request to the Authorization Server whereas the code_verifier is kept secret. After generating the authorization_code, the Authorization Server associates it with the received code_challenge that will be used later. The flow continues as per the second part until the Relying Party tries to exchange Alice’s stolen authorization_code for an access token. This time, the the Authorization Server’s request also contains the secret code_verifier bounded to Eve’s original authorization_code. When the Authorization Server tries to validate the code_verifier it fails because there’s no correlation between the code_challange, code_verifier and Alice’s authorization_code. Finally, an error is returned to the Relying Party.
OpenID Connect considerations
Until now we have described an oauth-pure flow, and we haven’t mentioned OpenID Connect. However, all the considerations are also valid for an authorization code flow in OpenID Connect. The difference is that not only will Eve be able to authorize the Relying Party as if she was Alice, but she would also be authenticated within the Relying Party as Alice. In fact, the Relying Party will have Alice’s identity token alongside the access token.
It’s worth noticing OpenID Connect provides an alternative mechanism to protect from this attack: the nonce parameter. The Relying Party generates the nonce value and includes it in the authorization request. The Authorization Server adds the nonce claim in the identity token, and the Relying Party validates it against the original nonce to correlate the authorization request with the token received. If the two values do not match the Relying Party aborts the authorization grant. Although the nonce is a valid alternative, there are two important differences between PKCE:
- While PKCE is a mechanism enforced by the Authorization Server, the validation of the nonce is a Relying Party responsibility.
- The second difference is the step of the authorization grant in which the protection becomes effective. With PKCE the Authorization Server performs the validation before issuing the tokens, while the Relying Party validates the nonce after the tokens have already been issued. Hence the Authorization Server will not be aware of any attack that has occurred, and the tokens could be leaked at a later point.
Our suggestion for OpenID Connect is to use both PKCE and the nonce parameter.
We would like to wrap-up this article with some suggestions for users, developers and/or architects interacting with systems using OAuth 2.0 or OpenID Connect.
- Never install any browser extension that is not very well-known and trusted. Unfortunately, browser stores seem to lack strong checks for published extensions. Therefore it is up to the single user to determine the legitimate purpose of an extension. We have already seen other situations where malicious extensions have caused trouble, for example, with cryptocurrency wallets. It is also esssential to be careful when during the installation an extension asks for suspicious permissions such as webRequest and webRequestBlocking in case of a Chromium-based browser.
- Always make use of PKCE for confidential clients. Many authentication providers such as Azure AD B2C offer the option to use PKCE also for confidential clients.
- Prefer the use of well-known OAuth/OpenID client libraries rather than re-implementing any security-related protocol or algorithm. Many open-source options are available for almost all the major programming languages and platforms.