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.
-
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_challengeandcode_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
-
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 authorizationcode.YOUR_REDIRECT_URI?code=AUTH_CODE_FROM_GOOGLE&state=SOME_STATE_VALUE
-
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_GOOGLEand makes a direct request to Google’s token endpoint. This request must include the originalcode_verifierthat was used to generate thecode_challenge.POST https://oauth2.googleapis.com/tokengrant_type=authorization_codecode=AUTH_CODE_FROM_GOOGLEredirect_uri=YOUR_REDIRECT_URIclient_id=YOUR_CLIENT_IDcode_verifier=YOUR_ORIGINAL_CODE_VERIFIER
-
Token Issued: If the
code_verifiermatches thecode_challenge(SHA256-hashed and Base64URL-encoded), Google’s authorization server issues anaccess_tokenand potentially arefresh_tokendirectly 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 thecode_verifier. The most common transformation isS256, which means the client takes thecode_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 thecode_challengewas 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
codefor anaccess_token. This is where thecode_verifieris 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.