DEV Community

Aviral Srivastava
Aviral Srivastava

Posted on

OAuth 2.0 Flows: Authorization Code with PKCE

The Dynamic Duo: Demystifying OAuth 2.0 Authorization Code with PKCE

Hey there, digital adventurers! Ever found yourself signing into a cool new app using your Google or Facebook account? That seamless, "don't-have-to-remember-another-password" feeling? Well, chances are, you've been a passenger on the magic carpet ride of OAuth 2.0, and more specifically, a really smart version of it called Authorization Code with Proof Key for Code Exchange (PKCE).

Think of OAuth 2.0 as the friendly bouncer at your digital club. It allows one application (the "client") to get permission to access resources on behalf of a user, without actually needing the user's credentials (username and password). It's all about delegated authority, keeping your sensitive information safe and sound. And PKCE? It's like giving that bouncer a special, secret handshake that makes him even more vigilant and secure.

Today, we're going to dive deep into this dynamic duo, unpack its workings, highlight its superpowers, and even acknowledge its occasional kryptonite. So, grab your favorite beverage, settle in, and let's embark on this journey to understand the nitty-gritty of Authorization Code with PKCE.

Introduction: Why Bother with Another Flow?

Imagine you're building a snazzy new mobile app that lets users track their workout progress across different fitness platforms. You want them to be able to connect their Strava, Garmin, or even their Apple Health data, without forcing them to create a whole new profile just for your app. This is where OAuth 2.0 swoops in like a superhero.

Traditionally, the Authorization Code Grant was the go-to for web applications and even some mobile apps. The idea is simple: the client app asks the authorization server (e.g., Google's login page) for permission. If the user grants it, the authorization server gives the client app a one-time-use "authorization code." The client app then trades this code, along with its own secret (a client secret), for an "access token" and a "refresh token." This access token is what the client app uses to fetch data from the resource server (e.g., Strava's API).

However, a wrinkle appeared in the fabric of this elegant system when it came to public clients, especially mobile apps and single-page web applications (SPAs). These clients can't securely store a "client secret" because their code is exposed to the end-user. If a malicious actor gets hold of that secret, they could impersonate the legitimate client app and steal access tokens.

Enter PKCE. It's not a completely new flow, but rather an extension to the Authorization Code Grant. It adds an extra layer of security that's specifically designed for these public clients, effectively eliminating the need for a client secret while maintaining a robust security posture. It's like saying, "Even if you don't have a secret handshake with the bouncer, you can still prove you're legit by performing a specific, unique action."

Prerequisites: What You'll Need to Play Along

Before we get our hands dirty, let's make sure we have our virtual toolkit ready. To understand and implement OAuth 2.0 Authorization Code with PKCE, you'll need a basic grasp of:

  • HTTP Basics: Understanding requests, responses, URLs, and parameters is fundamental.
  • APIs: Knowing how applications communicate with each other is key.
  • Security Concepts: A general understanding of concepts like authentication and authorization will be helpful.
  • Client Applications: You'll need to think about the type of application you're building (mobile, SPA, etc.) as this dictates whether PKCE is a must-have.
  • Authorization Server: You'll be interacting with an authorization server (like Google, GitHub, Auth0, Okta, etc.) that supports OAuth 2.0 and PKCE.

The Players in Our Digital Game

Let's meet the characters involved in our OAuth 2.0 Authorization Code with PKCE story:

  • Resource Owner: This is the user who owns the data you want to access (e.g., your fitness data).
  • Client Application: This is your application that wants to access the user's data (e.g., your workout tracking app).
  • Authorization Server: This is the trusted server that authenticates the Resource Owner and issues access tokens after receiving authorization (e.g., Strava's login and authorization service).
  • Resource Server: This is the server that hosts the protected resources (e.g., Strava's API that contains your workout data).

The Grand Unveiling: How Authorization Code with PKCE Works (The Step-by-Step Dance)

This is where the magic truly happens. Let's break down the Authorization Code Grant with PKCE into a series of steps, like a carefully choreographed dance:

Step 1: The Client's Secret Preparations (PKCE Magic Begins!)

Before the client app even asks for permission, it performs a clever trick:

  1. Generate code_verifier: The client application generates a cryptographically random string. This is called the code_verifier. It needs to be at least 43 characters long and at most 128 characters, using unreserved characters (alphanumeric, -, ., _, ~).
  2. Generate code_challenge: The client then creates a code_challenge from the code_verifier. This is done by hashing the code_verifier using the SHA256 algorithm and then Base64-URL encoding the resulting hash.

Think of it like this: The code_verifier is your secret handshake phrase. The code_challenge is a coded message derived from that phrase, which you can safely share with others without revealing the actual phrase.

Step 2: The Authorization Request (Asking for Permission)

The client application redirects the user's browser to the authorization server's /authorize endpoint. This request includes:

  • response_type=code: This tells the authorization server we want an authorization code.
  • client_id: Your application's unique identifier.
  • redirect_uri: The URL on your client app where the authorization server should send the user back after they grant or deny permission.
  • scope: The specific permissions your app is requesting (e.g., read_workouts).
  • state: A random string generated by the client to maintain state between the request and callback, and to prevent CSRF attacks.
  • code_challenge: The hashed and encoded code_verifier you generated in Step 1.
  • code_challenge_method=S256: This tells the authorization server that the code_challenge was generated using SHA256.

Example URL:

https://authorization-server.com/authorize?
  response_type=code
  &client_id=YOUR_CLIENT_ID
  &redirect_uri=https://your-app.com/callback
  &scope=read_workouts
  &state=A_RANDOM_STRING
  &code_challenge=E9Melhoa2OwvFrEMTJeZdSxFPவாக6tLgL5KfY9nS_g
  &code_challenge_method=S256
Enter fullscreen mode Exit fullscreen mode

Step 3: The User's Decision (The "Allow" or "Deny" Moment)

The user is presented with a consent screen by the authorization server. They can choose to grant or deny the requested permissions.

Step 4: The Authorization Code is Issued (The First Token)

If the user grants permission, the authorization server redirects the user's browser back to the redirect_uri specified by the client. This redirect URL includes:

  • code: A one-time-use authorization code.
  • state: The same state value that was sent in the initial request (for verification).

Example Callback URL:

https://your-app.com/callback?code=AUTHORIZATION_CODE_FROM_SERVER&state=A_RANDOM_STRING
Enter fullscreen mode Exit fullscreen mode

Step 5: The Code Exchange (The Grand Finale!)

Now, the client application (running on the user's device or server) receives the authorization code. It then makes a direct, back-channel request to the authorization server's /token endpoint. This request includes:

  • grant_type=authorization_code: Indicating we're exchanging an authorization code.
  • client_id: Your application's unique identifier.
  • code: The authorization code received in the previous step.
  • redirect_uri: The same redirect_uri used in the authorization request.
  • code_verifier: This is the crucial part! The client sends the original, unhashed code_verifier that it generated in Step 1.

Example Token Request Body (POST to /token):

{
  "grant_type": "authorization_code",
  "client_id": "YOUR_CLIENT_ID",
  "code": "AUTHORIZATION_CODE_FROM_SERVER",
  "redirect_uri": "https://your-app.com/callback",
  "code_verifier": "YOUR_ORIGINAL_CODE_VERIFIER"
}
Enter fullscreen mode Exit fullscreen mode

Step 6: The Server's Verification and Token Issuance

The authorization server receives the token request. It then performs its own magic:

  1. It hashes and Base64-URL encodes the code_verifier it received from the client using the code_challenge_method (which it knows was S256).
  2. It compares this newly generated code_challenge with the code_challenge it received in Step 2.
  3. If they match, it knows that the client application is legitimate and possesses the original code_verifier.
  4. It then issues an access token (used to access protected resources) and optionally a refresh token (used to obtain new access tokens without user re-authentication).

The Response from /token:

{
  "access_token": "YOUR_ACCESS_TOKEN",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "YOUR_REFRESH_TOKEN",
  "scope": "read_workouts"
}
Enter fullscreen mode Exit fullscreen mode

Step 7: Accessing Protected Resources

The client application can now use the access_token to make requests to the resource server's API, fetching the user's workout data. Each request includes the access_token in the Authorization header.

Example API Request:

GET /api/v1/workouts
Host: strava.com
Authorization: Bearer YOUR_ACCESS_TOKEN
Enter fullscreen mode Exit fullscreen mode

Advantages: The Superpowers of PKCE

Why is this extended flow so popular, especially for modern applications? Let's look at its superpowers:

  • Enhanced Security for Public Clients: This is the absolute killer feature. By requiring the code_verifier, PKCE effectively mitigates the "authorization code interception attack." Even if an attacker manages to intercept the authorization code, they cannot exchange it for an access token because they don't have the original code_verifier. This is a game-changer for mobile apps and SPAs.
  • No Client Secret Required: Public clients can't securely store client secrets. PKCE allows them to participate in the Authorization Code Grant without exposing any sensitive credentials.
  • Simplicity of Implementation: While it adds a couple of extra steps, the core concepts of generating a verifier and challenge are relatively straightforward to implement in most programming languages.
  • Interoperability: PKCE is becoming a de facto standard for authorization code grants in many OAuth 2.0 implementations.

Disadvantages: Even Superheroes Have Their Kryptonite

While PKCE is fantastic, it's not a silver bullet. Here are a few things to keep in mind:

  • Increased Complexity (Slightly): Compared to a basic Authorization Code Grant (where a client secret is used), PKCE introduces a few more steps. This means slightly more code to write and manage.
  • Browser-Based Flows Can Still Be Vulnerable to Code Interception: While PKCE protects against an attacker using the code without the verifier, if an attacker can compromise the client after it has received the code and before it exchanges it, they might still be able to gain access. This is more of a device-level compromise rather than an OAuth vulnerability.
  • Not Suitable for Confidential Clients: If your client application is a secure, server-side web application that can reliably store a client secret, the standard Authorization Code Grant might be sufficient and slightly simpler to implement. PKCE is primarily designed for public clients.

Key Features: The Hallmarks of PKCE

Let's summarize the defining characteristics of this flow:

  • code_verifier: A secret generated by the client.
  • code_challenge: A transformation of the code_verifier (typically SHA256 hash, then Base64-URL encoded).
  • code_challenge_method: Specifies the algorithm used to generate the code_challenge.
  • Authorization Code Interception Attack Mitigation: The primary security benefit.
  • No Client Secret for Public Clients: Enables secure OAuth 2.0 for mobile and SPAs.
  • Two-Step Token Acquisition: Involves obtaining an authorization code first, then exchanging it for tokens.

Code Snippets: Bringing it to Life (Conceptual)

Let's look at some pseudo-code to illustrate how you might implement the PKCE generation.

Generating code_verifier and code_challenge in JavaScript:

// Step 1: Generate code_verifier
function generateCodeVerifier() {
  const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
  let result = '';
  for (let i = 0; i < 128; i++) { // Aim for a good length
    result += characters.charAt(Math.floor(Math.random() * characters.length));
  }
  return result;
}

// Step 1: Generate code_challenge from code_verifier
async function generateCodeChallenge(codeVerifier) {
  const encoder = new TextEncoder();
  const data = encoder.encode(codeVerifier);
  const hash = await crypto.subtle.digest('SHA-256', data);
  // Convert ArrayBuffer to Base64 URL string
  let binary = '';
  const bytes = new Uint8Array(hash);
  const len = bytes.byteLength;
  for (let i = 0; i < len; i++) {
    binary += String.fromCharCode(bytes[i]);
  }
  return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

// Example usage:
async function setupPKCE() {
  const codeVerifier = generateCodeVerifier();
  const codeChallenge = await generateCodeChallenge(codeVerifier);

  console.log('codeVerifier:', codeVerifier);
  console.log('codeChallenge:', codeChallenge);

  // Store codeVerifier for later use (e.g., in session storage or localStorage)
  localStorage.setItem('pkce_code_verifier', codeVerifier);

  // Construct the authorization URL with code_challenge and code_challenge_method
  const authUrl = `https://authorization-server.com/authorize?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=https://your-app.com/callback&scope=read_workouts&state=some_state&code_challenge=${codeChallenge}&code_challenge_method=S256`;

  // Redirect the user to authUrl
  window.location.href = authUrl;
}
Enter fullscreen mode Exit fullscreen mode

Exchanging the Code for Tokens in JavaScript (on callback):

// On your callback page (e.g., https://your-app.com/callback)
async function handleCallback() {
  const urlParams = new URLSearchParams(window.location.search);
  const code = urlParams.get('code');
  const state = urlParams.get('state'); // Verify this state matches your initial state

  if (code) {
    const storedCodeVerifier = localStorage.getItem('pkce_code_verifier');
    if (!storedCodeVerifier) {
      console.error('PKCE code verifier not found!');
      return;
    }

    // Step 5: Exchange code for tokens
    const tokenRequestBody = new URLSearchParams();
    tokenRequestBody.append('grant_type', 'authorization_code');
    tokenRequestBody.append('client_id', 'YOUR_CLIENT_ID');
    tokenRequestBody.append('code', code);
    tokenRequestBody.append('redirect_uri', 'https://your-app.com/callback');
    tokenRequestBody.append('code_verifier', storedCodeVerifier);

    try {
      const response = await fetch('https://authorization-server.com/token', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
        },
        body: tokenRequestBody.toString(),
      });

      if (!response.ok) {
        const errorData = await response.json();
        throw new Error(`Token exchange failed: ${response.status} ${response.statusText} - ${JSON.stringify(errorData)}`);
      }

      const tokenData = await response.json();
      console.log('Tokens received:', tokenData);

      // Store tokens securely and use access_token to make API calls
      localStorage.setItem('access_token', tokenData.access_token);
      localStorage.setItem('refresh_token', tokenData.refresh_token);

    } catch (error) {
      console.error('Error during token exchange:', error);
    } finally {
      // Clean up stored verifier
      localStorage.removeItem('pkce_code_verifier');
    }
  }
}

// Call handleCallback when the page loads
window.onload = handleCallback;
Enter fullscreen mode Exit fullscreen mode

These snippets are illustrative and might need adjustments based on the specific OAuth 2.0 provider you are using and your application's framework.

Conclusion: The Secure Path Forward

OAuth 2.0 Authorization Code with PKCE has truly revolutionized how we handle authentication and authorization for modern applications, especially those running in less secure environments like mobile devices and web browsers. It strikes an elegant balance between usability and security, allowing users to seamlessly connect their accounts without compromising their sensitive credentials.

By understanding the underlying mechanics – the generation of code_verifier and code_challenge, the redirection dance, and the crucial code exchange – developers can confidently implement this robust flow. It's a testament to the continuous evolution of security protocols, ensuring that our digital interactions remain safe and trustworthy.

So, the next time you "Log in with Google" or authorize an app to access your social media, remember the silent, secure dance of OAuth 2.0 Authorization Code with PKCE that's making it all possible. It's the unsung hero of your seamless digital experiences, and now, you're in on its secret! Happy coding and stay secure!

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.