For years, the username-and-password combination was the default way to log in to almost everything. It has aged badly. People reuse the same password across dozens of sites, rarely use a password manager, and pick combinations that are easy to guess. A single leaked database can therefore unlock a person’s accounts everywhere else.
This is a big reason why “Sign in with Google,” “Continue with Apple,” and similar buttons have become so common. Instead of every app storing its own pile of passwords, the app trusts a large, security-focused identity provider to confirm who you are. This approach is called federated identity, and the protocol that makes most of it work is OAuth 2.0.
OAuth is powerful, but like any protocol it has sharp edges. One of those edges is what the Authorization Code flow with PKCE was created to file down. This guide explains what that flow is, the specific weakness it addresses, and how it works step by step.
A Quick Refresher on the Authorization Code Flow
Before getting to PKCE, it helps to understand the standard Authorization Code flow it builds on, because PKCE is an addition to it, not a replacement.
In a typical setup there are three parties:
- The user — sitting in a browser or app.
- The client — the application the user wants to use.
- The authorization server — the identity provider that actually checks credentials.
The flow goes roughly like this:
- The user clicks a login button.
- The application sends the user over to the authorization server.
- The user proves who they are there — by entering a password, approving a prompt, or using whatever method the provider supports.
- On success, the authorization server sends the user back to the application carrying a short-lived value called an authorization code.
That code is not yet the key to anything useful. The application now makes a separate, direct, server-to-server request back to the authorization server, handing over the authorization code along with its own credentials — a client ID and a client secret. If everything checks out, the authorization server responds with an access token. That token is the real prize: it is what lets the application call APIs and fetch information on the user’s behalf.
The design has a clever property. The valuable access token is never exposed in the browser or in a redirect URL where it could be snooped on. It is only handed over in a back-channel request that the user’s browser never sees. For a traditional web application with a proper backend, this is genuinely secure.
Where the Standard Flow Breaks Down
The security of that flow rests on a quiet assumption: the client secret stays secret. For a web app with a server, that assumption holds, because the secret lives on the backend where users and attackers cannot reach it.
The assumption falls apart for two kinds of applications that have become enormously common:
- Native mobile apps, which can be downloaded and decompiled.
- Single-page applications (SPAs) that run entirely in the browser, where anyone can open developer tools and read the shipped JavaScript.
These are called public clients, because their code is delivered directly to the user’s device. There is simply nowhere safe to hide a client secret in either case. If you embed one, you should assume a determined attacker can pull it out.
This creates a real danger when combined with another weak point: the authorization code itself travels through the user’s device, usually as part of a redirect URL. On a mobile device in particular, a malicious app can sometimes register itself to intercept those redirects and grab the code in transit.
Put the two problems together and the picture is alarming. An attacker who has extracted the client secret from a public client, and who manages to intercept an authorization code, now has everything needed to complete the token exchange themselves. They can trade that stolen code for a valid access token and start impersonating the user. The flow that was airtight for server-backed web apps has a gaping hole for public clients.
What PKCE Adds, in One Sentence
PKCE — Proof Key for Code Exchange, often pronounced “pixy” — closes that hole by removing the dependence on a static client secret and replacing it with a fresh, single-use secret that is generated on the fly for every single login attempt.
Because this proof is created anew each time and never has to be stored inside the app, there is nothing permanent for an attacker to steal, and an intercepted authorization code becomes useless on its own.
How PKCE Works, Step by Step
The elegance of PKCE is that it adds only a small amount of work to the existing flow and requires no long-lived secret. Here is what happens.
Step 1 — The app creates a secret and a scrambled version of it
Right before starting the login, the application generates a large random string. This string is the code verifier, and it is the temporary secret for this one login attempt. The application then runs that verifier through a one-way hashing function (the recommended one is SHA-256) to produce a second value called the code challenge.
// Generate the code verifier (a high-entropy random string)
const codeVerifier = base64UrlEncode(crypto.getRandomValues(new Uint8Array(32)));
// Derive the code challenge: SHA-256, then base64url-encode the digest
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(codeVerifier));
const codeChallenge = base64UrlEncode(new Uint8Array(digest));
The key idea is asymmetry: you can easily go from the verifier to the challenge, but you cannot work backwards from the challenge to recover the verifier. The application keeps the verifier to itself and sends only the challenge.
Step 2 — The login request carries the challenge
When the application redirects the user to the authorization server to log in, it includes the code challenge and a note saying which hashing method it used.
GET /authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://app.example.com/callback
&scope=openid profile
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
The authorization server stores that challenge, tied to this particular request, and proceeds with authenticating the user as normal. Once the user has successfully proven their identity, the server sends them back to the application with an authorization code, exactly as in the standard flow.
Step 3 — The token exchange must prove possession of the original secret
Now the application goes to swap its authorization code for an access token. This time, instead of presenting a client secret, it sends along the original code verifier — the raw random string it kept from Step 1.
POST /token
grant_type=authorization_code
&code=AUTHORIZATION_CODE
&redirect_uri=https://app.example.com/callback
&client_id=YOUR_CLIENT_ID
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
The authorization server takes that verifier, hashes it with the method declared earlier, and compares the result against the code challenge it stored. If the two match, the server has cryptographic proof that whoever is asking for the token is the very same party that started the login — and it issues the access token. If they do not match, the request is rejected.
Why This Defeats the Attack
Walk back through the attacker’s position with PKCE in place:
- They decompile the mobile app. There is no client secret to find, so that avenue is gone.
- They intercept the authorization code in transit. When they try to exchange it for a token, the server demands the matching code verifier. The attacker never had it — the verifier lived only in memory inside the legitimate app for the duration of that one login and was never transmitted across the network.
- They captured the code challenge from the network. It is a one-way hash, so it cannot be reversed into the verifier.
The stolen authorization code is therefore inert. It cannot be redeemed.
In short, PKCE addresses two distinct problems at once. It neutralizes authorization code interception, because a code without its matching verifier is worthless. And it eliminates the client secret problem entirely, because public clients no longer need to store a secret they were never able to protect in the first place.
Where You Should Use It
PKCE was originally designed to protect mobile and native applications, which were its most obvious victims. Since then, security guidance has broadened considerably. The current recommendation from the OAuth working group is to use the Authorization Code flow with PKCE for essentially all clients that use the authorization code flow — including single-page apps, and even traditional web apps with backends, where it adds defense in depth at very little cost.
If you are building authentication today, treating PKCE as the default rather than the exception is the sensible posture.
Conclusion
OAuth’s popularity comes from a genuine balance of convenience and security, but that security only holds when the protocol is matched to the environment it runs in. The plain Authorization Code flow was built in an era dominated by server-backed web apps, and it quietly assumed a secret could always be kept safe. The explosion of mobile apps and browser-based applications broke that assumption.
PKCE restores the guarantee by swapping a fragile, permanent secret for a disposable proof that is generated fresh every time and never stored anywhere an attacker can reach. It is a small, elegant addition that turns a flow with a serious gap for public clients into one that is safe to use almost everywhere — which is exactly why it has become the modern default.