Proof Key for Code Exchange (PKCE) - pronounced "pixy" đ§ - is my favorite OAuth standard. It is short, sweet, and solves an important problem in an interesting way. When I am introducing new developers to OAuth, I recommend they read the PKCE RFC first before diving into others - like starting with "Of Mice and Men" before opening up "Moby Dick".
Pee Kay Cee Eee? What?
PKCE is an extension to the original OAuth standard.
OAuth 2.0 was not originally designed with secure public clients (like mobile or single-page apps) in mind. In traditional authorization code flows, the client secret is a required part of the token exchangeâbut public clients canât store secrets safely. This made the flow susceptible to interception attacks, especially in environments where the authorization code could be leaked.
During a public client flow, if an attacker gets access to an authorization code (for example, see the mess that is mobile deep linking) then the attacker can exchange that code themselves. Bad!
PKCE fixes this attack by cryptographically proving that the caller that starts the OAuth flow is the same caller that completes it.
This is done by introducing a new code_verifier parameter:
- The client generates a cryptographically random
code_verifierand stores it locally - The client computes the
SHA256hash of thecode_verifierto generate acode_challenge. This is theS256method - the RFC also defines aplainmethod that skips hashing, but in practice you should never use it. - The client passes the
code_challengeto the server as part of the standard OAuth Authorization Request, e.g.example.com/oauth2/authorize?client_id=...&code_challenge=.... - The server records the
code_challengeand proceeds with the standard OAuth flow, eventually redirecting back to the client with a?code=.... - The client sends the saved
code_verifierin thePOST /tokenrequest that exchanges thecodefor an access token (it is a request parameter, not part of the redirect) - The server computes the
SHA256hash of thecode_verifieritself- If the hash matches the saved
code_challenge, then the server knows that the client is the same one who initiated the flow and no interception occurred - If the hash does not match, the server fails the request
- If the hash matches the saved
There are some other bits and pieces around the spec - the code_verifier must be 43â128 characters of high-entropy randomness (Section 4.1),
the code_challenge is base64url-encoded (not standard base64), and clients advertise which method they used via a code_challenge_method parameter set to S256 or plain - but that's pretty much the entire spec.
Here's what that actually looks like in code - generating a code_verifier and deriving its code_challenge with WebCrypto:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const base64url = (bytes) =>
btoa(String.fromCharCode(...bytes))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
const verifierBytes = crypto.getRandomValues(new Uint8Array(32));
const code_verifier = base64url(verifierBytes);
const hashBuffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(code_verifier));
const code_challenge = base64url(new Uint8Array(hashBuffer));
console.log('code_verifier: ', code_verifier);
console.log('code_challenge:', code_challenge);Top Tier Security API
Unlike many other parts of OAuth, PKCE is designed such that it is impossible to skirt around.
Compare this with something like the nonce in OpenID Connect, which clients are expected to validate locally.
If a client forgets or skips validation, the flow still completes. But with PKCE, the server itself enforces the check.
You canât fake PKCE. You either follow the spec or you fail.
As a bonus, PKCE protects against Login CSRF attacks too.
Attackers can't trick an unsuspecting user to click a link and log in to the wrong account because the user won't have the correct code_verifier.
The code_verifier lives on the attacker's device, so when the victim's browser follows the redirect back, neither side can complete the token exchange - the attacker's session isn't involved, and the victim's browser has no code_verifier to present.
PKCE All The Things
PKCE started out as a specific extension for mobile applications, but is now generally recommended for all OAuth applications. Talk about a glow up!
PKCE doesn't need to limit itself to OAuth either! Any system that involves HTTP redirects and callbacks can make use of PKCE. For example, magic links! Magic link flows generally involve a user requesting a login link from a website, and then opening that link from their email. The link in the email redirects the user back to the website with a token that can be verified. Want to prevent users from forwarding magic links to friends and family? Or stop email scanners from clicking on links and messing things up? Add PKCE to the flow and only the device that initiated the magic link flow will be able to authenticate the token.
Note: PKCE magic links don't work well when using mobile application WebViews - when users click the link from their email, the link is opened in a default browser that doesn't have access to the code verifier. Mobile-focused magic links need to be deeplinks or universal links to work well.