Skip to content
Boston Identity

Chapter 9

OAuth 2.0: PKCE

Part II · IAM Core Concepts7 min read

OAuth 2.0 with Proof Key for Code Exchange (PKCE) is an enhanced security mechanism designed to protect against code interception attacks in authorization flows.

PKCE is an extension to the OAuth 2.0 protocol and provides an additional layer of security for native and mobile applications. The PKCE flow ensures that even if an attacker intercepts the authorization code, they cannot exchange it for an access token without possessing the original cryptographic proof.

This flow adds a dynamic secret called the code verifier to the OAuth 2.0 authorization request, which is later verified during the token exchange process. By using PKCE, developers can significantly enhance the security of their applications, mitigating the risk of authorization code interception and unauthorized access to protected resources.

Key Points

  • Client Types:
    • Confidential Clients: Can securely store client secrets.
    • Public Clients: Cannot store secrets securely (e.g., SPAs, mobile apps).
  • PKCE Motivation:
    • Prevents authorization code interception attacks in public clients.
    • Eliminates the need for client secrets in public clients.
  • PKCE Flow:
    1. Client generates a code_verifier (random string) and code_challenge (hashed code_verifier).
    2. Client includes code_challenge and code_challenge_method in the authorization request.
    3. Authorization Server stores the code_challenge and issues an authorization code.
    4. Client exchanges the code for an access token, including the code_verifier.
    5. Authorization Server verifies the code_verifier against the stored code_challenge.
    6. If verified, the Authorization Server issues an access token.
  • Benefits:
    • Enhances security for public clients.
    • Enables the use of the Authorization Code grant type without client secrets.

Authorization Code with PKCE (Proof Key for Code Exchange)

Authorization Code with PKCE (pronounced 'pixy'), or simply PKCE grant, is designed to be used with SPA (Single Page Apps) or Mobile Apps as a modern solution. Before we dive in, let's look at some pre-knowledge to better understand it.

Public Client and Confidential Client

According to OAuth specs, there are two types of Clients that can be registered with an Authorization Server: Public Client and Confidential Client

Confidential Client - Client Applications that are able to securely store Client Secret and keep it safe

Public Client - Client Applications that NOT able to store Client Secret and hence, can NOT use secret to authenticate a Client

Based on the definition, when registering a Confidential type Client, a Client Secret will be provided together with the Client ID by the Authorization Server, while in contrast, NO secret will be provided to the Public Client.

The reason for the above is that Confidential Client refers to Applications within a company, which usually have a backend using technologies like Java, .NET, NodeJS etc. These applications can store a password or secret securely e.g. in a database.

On the other hand, Public Client refers to SPA or Mobile Application. SPA is written in JavaScript and usually has NO way to store a password/secret other than source code. Native Mobile Applications can NOT store password/secret securely either. The binaries from one's cellphone can be decompiled and credentials could be compromised.

The Issue with Public Client

Remember the standard Authorization Code flow is a two-step workflow, where the first step is a browser redirect to get an Authorization Code and second step is to exchange the code for Access Token with Http POST. Specifically, the second step needs to POST Client ID and Client Secret in addition to the Authorization Code.

If the Client App is a SPA or Native Mobile App (meaning a Public Client), it can NOT store secrets and hence can NOT use Client ID and Secret to authenticate itself in the Authorization Server.

To avoid the issue of storing secrets, one option is to use Implicit Grant instead of Authorization Code grant, where only Client ID and Redirect URI are used to authenticate the client in Authorization Server (NOT a secure way). Furthermore, since Access Token is returned in the URL, it has added risk of exposing the Access Token. This is why Implicit Grant is deprecated.

Now, if we go back to the Authorization Code grant and remove the Client Secret, only relying on Authorization Code, Client Id and Redirect URI to exchange for an Access Token. It looks plausible but actually is vulnerable to a type of attack called Authorization Code Interception Attack.

To fix this, some additional parameters: Code Verifier, Code Challenge and Code Challenge Method are added to the flow and this is the PKCE flow.

How PKCE Flow Works

Let's first understand what area Code Verifier, Code Challenge and Code Challenge Method.

  • Code Verifier - A cryptographically random string generated by the Client App. This value will be different each time a PKCE flow is initialized.
  • Code Challenge - This is the hashed value from Code Verifier (You need to understand what Hashing is).
  • Code Challenge Method - The hash method/function used to hash Code Verifier to generate Code Challenge

Note the default Code Challenge Method is 'plain', which does NOT do any hashing on the original Code Verifier and it is recommended to replace it with some hashing algorithm such as S256 (SHA256).

Diagram: How PKCE Flow Works

On a high level, the flow is:

  1. Client generates a random string (Code Verifier), and then hash it with a certain hash function(Code Challenge Method). Then the Client sends the hashed value (Code Challenge) and hashing method to the Authorization server.
  2. Authorization Server stores the Code Challenge value and Code Challenge method and returns the Authorization Code to Client.
  3. In exchanging Authorization Code for Access Token, the Client sends the Authorization Code together with, this time, Code Verifier to the Authorization Server.
  4. Authorization Server runs a hashing check with the Code Verifier and the Code Challenge & Code Challenge Method received in the first step.
  5. If the check is valid, Access Token will be returned.
Diagram: How PKCE Flow Works

PKCE Flow Steps

1. Client sends (browser redirect) code_challenge and code_challenge_method to Authorization Server

https://authorization-server.com/oauth/authorize?client_id=test-client123&response_type=code&scope=read&redirect_uri=https://oauth-app.com/callback&code_challenge=bc6c6abccd6fec88f1772fa263f824208db050306562e80f3f8684a82f6b9818&code_challenge_method=S256&state=abc123

Notice that https://authorization-server.com/oauth/authorize is the Authorization Server address with the endpoint to start the flow. This is defined by the Authorization Server itself. Also some Authorization Server (such as ForgeRock) supports getting Authorization Code without Browser, meaning that Client can make a REST call to its endpoint providing the same parameters as payload and get back the authorization code. This depends on the Authorization Server.

  • client_id - The registered Client Name/ID from the Authorization Server
  • response_type=code - This tells the Authorization Server that the Client App is initializing the Authorization Code flow (PKCE is a modified Authorization Code flow)
  • scope - One or more strings (separated by space by default) indicating which permissions the application is requesting
  • redirect_uri - The URI where Authorization Server will redirect to after authenticating the user
  • code_challenge - Hashed value of original Code Verifier
  • code_challenge_method - Hash algorithm used to generate the code_challenge; 'S256' means SHA256
  • state - A random string to keep track of current OAuth 2 flow transaction; the same value will be returned by the Authorization Server; this is to prevent CSRF attacks

2. Authorization Server returns Authorization Code

Based on the Authorization Code flow, the user needs to authenticate in the Authorization Server and consent might be needed based on the server configuration.

The return of the Authorization Code happens in the URL parameter and will be an Http browser redirect.

https://oauth-app.com/callback?code=LtwzXMel17MNjrel4KIcaZoz24eGsLtQ&state=abc123

  • code - This is the authorization code generated by the Authorization Server; it is usually short-lived (between 1-10 mins) and will be invalid once used
  • state - This will be the same value from the request; if the Client App sees a different value, the flow should be terminated

3. Exchanging Authorization Code for an Access Token

Client sends out the Authorization Code and Code Verifier with an Http POST. You can see NO Client Secret is included in this request.

To send out the POST request, you can use CURL from the command line or REST Client Tool like POSTMAN. The POST request will look like below:

POST /oauth/token HTTP/1.1

Host: authorization-server.com

grant_type=authorization_code &code=LtwzXMel17MNjrel4KIcaZoz24eGsLtQ

&redirect_uri=https://oauth-app.com/callback

&client_id=test-client123

&code_verifier=LtwzXMel17MNjrel4KIcaZoz24eGsLtQ

The client_id and redirect_uri must match the previous call.

  • grant_type=authorization_code - This tells the Authorization Server that the Client app is using the Authorization Code flow(again, PKCE is still a modified Authorization Code flow); do NOT confuse with 'response_type=code' in step 2
  • code - The same as the value returned by the Authorization Server
  • redirect_uri - The same redirect URI that was used when requesting the code; this may not be required depending on the Authorization Server
  • client_id - The same client id that is used to request the code
  • code_verifier - The original string used to generate the code_challenge

4. Authorization Server Check code_verifier, code_challenge and code_challenge_method

Besides checking the code, redirect_uri and client_id, the Authorization Server will do a hashing on code_verifier with code_challenge_method and compare the generated value with the code_challenge value sent by the client. If the two values do NOT match, Authorization Server will reject the request and terminate the flow.

5. Authorization Server Returns Access Token

After validation of all parameters and hashing, Authorization Server will return the Access Token in the payload.

HTTP/1.1 200 OK

Content-Type: application/json

Cache-Control: no-store

Pragma: no-cache

{

"access_token":"eyJ0eXW13A8OZ5AbigKfDmKAPHHGTU7cUbNdrw...",

"token_type":"Bearer",

"expires_in":3599,

"scope":"read"

}

Similar to Authorization Code flow, Refresh Token can be issued by Authorization Server as well.