Proof Key for Code Exchange (PKCE)
In the Code Flow tutorial, we explained:
- How this popular message exchange pattern works;
- The kinds of client applications that use it;
- Variations that can be used to get different types of tokens (e.g., ID Tokens and Refresh Tokens); and
- How the user is redirected back to the client application once authentication and delegation are complete.
This last point about redirection is an important one. As mentioned many times in the OAuth 2.0 Threat Model (RFC 6819), the use of redirects is one of the primary attack vectors of OAuth. There are many ways that an attacker may try to take advantage of the code flow's redirection back to the client. These danger are largely mitigated by:
- Requiring the client to authenticate itself at the token endpoint (when redeeming the authorization code);
- Using TLS on the client's redirect URI;
- Having a unique redirect URI for each client which it has total control over.
For Web applications, these safeguards are usually in place. They may not be, however, for mobile applications. In such environments, an attacker may be able to obtain the client ID and secret used by the app when it authenticates itself to the OAuth server's token endpoint. Also, the redirection may use a custom scheme that is not confidential and may be possible for an attacker to also register to handle. This situation is shown in the following figure:
This danger can be mitigated though using Proof Key for Code Exchange (PKCE), or "pixie" as it is commonly referred to, which is defined in RFC 7636.
Explanation of PKCE
The reason that such a vulnerability exists is because the OAuth server (i.e., Curity) doesn't know the difference between the legitimate app that starts the flow in step one and the malicious app that redeems the authorization code in step five. To close this hole, the OAuth server needs a way of determining that the app that starts the flow is the same one that finishes it. This is what PKCE does. The way it accomplishes this is by specifying that:
- The legitimate client app creates a secret key that only it knows.
- The request in step one above includes a hash of this secret key and info about which hashing algorithm was used.
- Step five in the previous figure includes the actual key.
- Before the OAuth server returns the tokens in step six, it uses the secret key to determine if it can arrive at the same hash that was previously given (using the early-provided hashing algorithm).
These additional steps are shown in the following figure:
This is the code flow using a proof key for the code exchange (hence the name). The key proves that the one who requested the code is the same entity that redeems it. This makes the authorization code a "proof of possession" token because the client must prove that it posses the secret key used when the authorization flow is initiated. Because the malicious app does not have access to the secret key, it will not be able to redeem the code. Conversely, the legitimate one does, so only it will receive tokens when redeeming the authorization code:
Pseudo Example Code
To implement this, the client would use pseudocode such as the following when it starts the authorization request:
code_verifier = generate_random_string(length = 100) # (1) session["code_verifier"] = code_verifier # (2) hash = sha256_hash(code_verifier) # (3) code_challenge = base64_url_encode(hash) # (4) redirect_to_authorization_endpoint_using_pkce( # (5) code_challenge = code_challenge, code_challenge_method = "S256")
(1), the client generates a random key or string of bytes that only it knows. It saves this in some persistent session store
(2). It then hashes these bytes
(3) and encodes them for use in a URL
(4). When redirecting to the OAuth server, it includes the hash of the key and the algorithm used to compute that hash
Later, when the client receives the call back from the OAuth server, it would redeem the authorization code using pseudocode like this:
post_code_to_token_endpoint_using_pkce( # (6) code = request["code"], code_verifier = session["code_verifier"])
In this sample code, the client would include the key generated when it started the authorization request (as shown in step one in the figures above). Then, Curity would be able to verify that the client exchanging the code (step five in the previous diagram) is the original one. It would do this by using the signing algorithm included in
(5) and the code verifier in
(6). If the result was the same as the hash in
(3), then the request initiator and the code exchanger are the same entity, and tokens are issued; otherwise, an error results.
For an actual code example of this, refer to the Python code sample on GitHub.
Implementing PKCE in Curity
Using PKCE in Curity is very simple. It is automatically enabled for any client that has the code flow enabled:
While it is not possible to disable PKCE (thus making that client less secure), it is possible to require it (making it more secure). To do this in the Admin UI:
- Navigate to the
Client Application Settingssection of a client for which you would like to require PKCE.
- Expand the
- Toggle on
Require Proof Key.
- Optionally and recommended, disallow the client from using the
plainproof key challenge method.
Of course, these settings can also be configured using the other managment interfaces (e.g., the CLI, REST API, etc.). The XML for these settings is shown in the following listing:
<client> <id>my-good-client</id> <proof-key> <require-proof-key>true</require-proof-key> <disallowed-proof-key-challenge-methods>plain</disallowed-proof-key-challenge-methods> </proof-key> <!-- ... --> </client>
Aside from this, there is nothing more to configure in Curity. Setting up PKCE is that easy :-)
Conclusion and More Info
Pixie is an auxiliary OAuth specification designed to close a security vulnerability that is common on mobile devices. It specifies the use of a proof key to ensure that the application that starts the code flow is the same one that finishes it. This challenge is very easy to implement as shown in the pseudocode of this tutorial. It is enabled by default in Curity and requiring it can be done with just a few clicks. Because it is so simple to use, it is recommended for all clients, mobile and otherwise, that are using the code flow. It is also suggested to use SHA-256 as the hashing algorithm used to compute the challenge and to disallow an unhashed challenge. Using PKCE is common with dynamically registered clients. To learn more about this, watch this video which digs into that protocol. Another easy safeguard that you can use with the code flow and PKCE is Pairwise Pseudonymous IDs (PPIDs) which is covered in another tutorial.
If you have questions about anything in this tutorial, you can also contact us. We're happy to help!