OAuth 2.0 and PKCE are surprisingly adept at securing user authentication flows, even without relying on traditional shared secrets between the client and authorization server.

Let’s see this in action. Imagine a mobile app (the "public client") wanting to access a user’s data on a service like Google Photos.

  1. User Initiates Login: The app redirects the user’s browser to Google’s authorization server with a request for specific permissions (scopes). Crucially, this request includes a code_challenge and code_challenge_method.

    • https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=YOUR_REDIRECT_URI&scope=https://www.googleapis.com/auth/photos.readonly&code_challenge=E9MelmQwNuooianZ4qjQ95S0N_Vw39p_m_9H-2XbCj0&code_challenge_method=S256
  2. User Grants Consent: The user logs into Google and approves the app’s access. Google’s authorization server then redirects the user’s browser back to the app’s redirect_uri, but this time it includes an authorization code.

    • YOUR_REDIRECT_URI?code=AUTH_CODE_FROM_GOOGLE&state=SOME_STATE_VALUE
  3. App Exchanges Code for Token: The app, running on the user’s device (where it can’t securely store a client secret), takes this AUTH_CODE_FROM_GOOGLE and makes a direct request to Google’s token endpoint. This request must include the original code_verifier that was used to generate the code_challenge.

    • POST https://oauth2.googleapis.com/token
    • grant_type=authorization_code
    • code=AUTH_CODE_FROM_GOOGLE
    • redirect_uri=YOUR_REDIRECT_URI
    • client_id=YOUR_CLIENT_ID
    • code_verifier=YOUR_ORIGINAL_CODE_VERIFIER
  4. Token Issued: If the code_verifier matches the code_challenge (SHA256-hashed and Base64URL-encoded), Google’s authorization server issues an access_token and potentially a refresh_token directly to the app.

The problem PKCE (Proof Key for Code Exchange) solves is the "authorization code interception attack." Without PKCE, an attacker could intercept the authorization code during the redirect from the authorization server back to the client. If the attacker also had the client’s client_secret, they could then exchange that code for tokens, impersonating the legitimate user.

PKCE makes this impossible for public clients (like mobile apps or single-page web apps) because they don’t have a client_secret. Instead, PKCE introduces a dynamic, per-request secret (code_verifier) and a transformed version of it (code_challenge).

Here’s the mental model:

  • Authorization Server: The gatekeeper, verifying identities and issuing access tokens. It trusts the client to prove it’s the same client that initiated the request.
  • Public Client: The application requesting access on behalf of the user. It cannot securely store secrets.
  • code_verifier: A randomly generated, high-entropy string (e.g., 128 bytes) created by the public client before starting the OAuth flow. This is the "secret" the client holds.
  • code_challenge: A transformed version of the code_verifier. The most common transformation is S256, which means the client takes the code_verifier, hashes it using SHA-256, and then Base64URL-encodes the result. This is what’s sent in the initial authorization request.
  • code_challenge_method: Indicates how the code_challenge was generated (e.g., S256).
  • Authorization code: A temporary credential issued by the authorization server after the user approves the request.
  • Token Endpoint: Where the client exchanges the authorization code for an access_token. This is where the code_verifier is finally presented.

The authorization server stores the code_challenge and code_challenge_method associated with the authorization code it issues. When the client requests tokens, it sends the code_verifier. The authorization server then re-calculates the code_challenge from the provided code_verifier using the specified code_challenge_method. If the re-calculated challenge matches the one it stored, it knows the client making the token request is the same client that initiated the authorization request, and it proceeds to issue tokens. An attacker who intercepts the code but doesn’t have the original code_verifier cannot complete this step.

The state parameter, often used alongside PKCE, prevents Cross-Site Request Forgery (CSRF) attacks by ensuring the user who initiated the request is the one completing it. The client generates a random string for state, sends it in the initial request, and verifies that the state returned by the authorization server matches the one it sent.

The most surprising thing is how effectively PKCE transforms a potentially insecure flow for public clients into a robust one, all by adding just two parameters (code_challenge and code_verifier) and a simple cryptographic transformation. It doesn’t require the client to store long-lived secrets, sidestepping the fundamental insecurity of public clients in traditional OAuth.

The next step is understanding how refresh tokens are used and the security implications of their use.

Want structured learning?

Take the full Cryptography course →