Awwwards Nominee
← Back to all posts6 min read

OAuth 2.0 Proof Key for Code Exchange (PKCE) vs Private Key JWT


When it comes to OAuth 2.0, there are two popular ways to secure client credentials - Proof Key for Code Exchange (PKCE) and Client Authentication Private Key JWT. Although both methods are used to authenticate, they differ in some significant ways. In this blog post, we'll discuss the differences between PKCE (pronounced pixie) and Client Authentication Private Key JWT.

tldr

  • PKCE
    • Symmetric encryption (via SHA256)
    • Used for "web, native, and mobile" client applications
    • Full OAuth 2.0 flow (/authorize, /token, /refresh)
    • Use for any client that can't protect a client secret
  • Private Key JWT
    • Asymmetric encryption (via RS256)
    • Used for "machine-to-machine" client applications like (CLIs, API-to-API, etc)
    • Partial OAuth 2.0 flow (just /token and /refresh)
    • Use for "maximum security" over just a "client secret"

What is PKCE?

Proof Key for Code Exchange, or PKCE, is an extension of OAuth 2.0, which is designed to secure native, mobile, and web applications (basically anything that has a "keyboard & mouse"). PKCE adds additional security to the Authorization Code flow by binding the access token to a specific device and user. PKCE accomplishes this by using state, a code verifier and a code challenge to prevent Authorization Code interception (also known as Man-in-the-Middle MITM) attacks.

How does PKCE work?

To use PKCE, the client application generates a state, code_verified, and a code_challenge.

  • The code_verifier is a one-time random secret string (minimum of 64 characters long)
  • The state is a one-time random string, (minimum of 28 characters long)
  • The code_challenge, a hashed value (usually SHA256) of the code_verifier.

The client application generates both the code_verifier and state and saves it in storage (either Local Storage or Session Storage). It then sends the code_challenge to the authorization server when requesting an Authorization Code.

/* OAuth 2.0 Authorize (/authorize) with PKCE */
// Generate a state
const state = generateRandomString(28);
localStorage.setItem("state", state);

// Generate a code verifier
const codeVerifier = generateRandomString(64);
localStorage.setItem("code_verifier", codeVerifier);

// Generate a code challenge
const codeChallenge = await sha256(codeVerifier);

const authorizeUrl = `
  <authorization-server-url>/authorize/
    ?client_id=client_requesting_auth
    &response_type=code
    &state=982407f7db5a35f6bd8bdcd83264476ce1d2f7d170246dbbcb7086b4
    &code_challenge=sCHXzXpnKE5V9z_upOqcxhBx1ihW_L4qqwGPxMcE9TY
    &code_challenge_method=S256
`;

The authorization server verifies the code_challenge and responds with the Authorization Code code and the initial state. The client application (aka "the browser") then checks to see if the state is the same as the original request and requests to exchange the authorization code for an Access Token, which is then used to access protected resources.

/* OAuth 2.0 Token Exchange (/token) with PKCE */
// User lands on a callback URL that has `state` and `code`
window.location = `<client-url>/callback/
  ?code=319311a3bb3f13f98f8f3ace3fa23j
  &state=982407f7db5a35f6bd8bdcd83264476ce1d2f7d170246dbbcb7086b4`;

const savedLocalStorageState = localStorage.getItem("state");
const queryParameterState = new URL(window.location).searchParams.get("state");

/*
  Compare state saved in LocalStorage (during initial `/authorize`)
  with the `/callback/?state=<should_be_same_state>
*/
if (savedLocalStorageState === queryParameterState) {
  // if state matches, request code for an access token
  const url = `
    <authorization-server-url>/token/
      ?client_id=client_requesting_auth
      &grant_type=authorization_code
      &code=319311a3bb3f13f98f8f3ace3fa23j
      &code_verifier=175618f6eb0acc38891b3037b37df7de20f901ade08ad9124804...
  `;

  const response = await fetch(url); // request for token
  const data = await response.json(); // keep access tokens SECURE!
} else {
  throw new Error("state did not match request");
}

What is Client Authentication Private Key JWT?

Client Authentication Private Key JWT, also known as OAuth JWT, allows clients to authenticate with the authorization server using a private key. This method is typically used by server-side applications that require high levels of security.

How does Client Authentication Private Key JWT work?

To use OAuth JWT, the client application generates a private/public key pair. The client application sends a request to the authorization server to register the public key. When the client application requests an access token, it signs the request with its private key. The authorization server verifies the signature using the registered public key and responds with an access token.

const jwt = require('jsonwebtoken');
const axios = require('axios');

// Generate a private/public key pair
const { privateKey, publicKey } = jwt.generateKeyPairSync();

// Request an access token signed with the private key
const jwtPayload = {
  iss: 'client-id',
  sub: 'client-id',
  aud: '<https://auth.example.com/token>',
  iat: Math.floor(Date.now() / 1000),
  exp: Math.floor(Date.now() / 1000) + 60,
};

// Sign the JWT token with the generated private key
const token = jwt.sign(jwtPayload, privateKey, { algorithm: 'RS256' });

// Create request for `/token` endpoint with the following options
const response = axios.post('<https://auth.example.com/token>', {
  // REQUIRED
  grant_type: 'client_credentials',
  // Optional, client_id already in signed JWT
  client_id: 'client-id',
  client_assertion_type: 'urn:ietf:params:oauth:grant-type:saml2-bearer'
  client_assertion: token, //PHnbzW0...ZT
});

// Use access token
const accessToken = await response.data;

When to use PKCE or Private Key JWT?

The main differences between PKCE and Client Authentication Private Key JWT are:

  • PKCE is used by web, native, and mobile applications, while Client Authentication Private Key JWT is used by server-side applications.
  • PKCE uses code_verifier and code_challenge to secure the Authorization Code flow, while Client Authentication Private Key JWT uses a private/public key pair.
  • PKCE is designed to prevent Authorization Code interception attacks, while Client Authentication Private Key JWT is designed to provide high levels of security for server-side applications.

PKCE vs Private Key JWT Table

PKCE Private Key JWT
Encryption Symmetric usually (HMAC) SHA-256 Asymmetric (RSA Signature) usually with SHA-256
Client Type Web (single-page applications), native, and mobile Machine-to-machine or server-side applications (CLI, API-to-API, etc)
Secret One-time random string code_verifier and SHA-256 hashed, base64-encoded code_challenge Encrypted JWT with Private Key (Public Key is exposed to Authorization Server)

In conclusion, both PKCE and Client Authentication Private Key JWT are effective methods for securing client credentials in OAuth 2.1. However, the method you choose will depend on the type of application you are building and the level of security you require.

If you want to learn more about these topics, check out oauth.net, oauth.com, or IETF OAuth RFCs

Hopefully some of you found that useful. Cheers! πŸŽ‰

If you enjoyed this article please feel free to connect with me on Dev.to or on LinkedIn