What is PKCE and why every OAuth app should use it
A developer-friendly guide to Proof Key for Code Exchange (PKCE): how it works, the problems it solves, and why it’s essential for secure OAuth flows, regardless of the application type.
OAuth 2.0 introduced a flexible framework for delegated authorization, but over time, best practices have shifted as new attack vectors emerged. One of the most important evolutions was the introduction of Proof Key for Code Exchange (PKCE, pronounced “pixie”), an enhancement to the popular Authorization Code Flow, designed to secure public clients, such as single-page applications (SPAs) and mobile apps.
With the release of OAuth 2.1, PKCE is no longer just a best practice for public clients; it’s now mandatory for all applications, including confidential clients like server-based apps. This article revisits how PKCE works, why it’s so important, and what developers need to know when upgrading their OAuth implementations to align with OAuth 2.1.
What is PKCE?
The most popular OAuth 2.0 flow is the Authorization Code Grant, which allows applications to get tokens securely by exchanging an authorization_code. But this flow has a limitation: it relies on the client authenticating with a client_secret during the code exchange. That works for server-based (confidential) apps, which can safely store secrets, but not for public clients like single-page apps or mobile apps, which cannot keep secrets safe.
Native apps can be decompiled, and single-page apps have their entire source code available to the browser, so they cannot be trusted with secrets. Without additional protection, these apps are exposed to code interception attacks, where an attacker steals the authorization code and redeems it for an access token.
To solve this, OAuth 2.0 introduced the Proof Key for Code Exchange (PKCE) extension. PKCE secures the Authorization Code flow for public clients by adding three new parameters:
- Code verifier: a cryptographically random string that the application creates when a user requests to log in.
- Code challenge: a hash of the code verifier.
- Code challenge method: the hashing algorithm used to generate the code challenge (typically
SHA-256
).
How PKCE works
Here’s how the PKCE flow works in practice:
- Generate a code verifier: When a user requests to log in, the app creates a cryptographically random string called the
code_verifier
. The app stores this value locally. - Derive a code challenge: The app hashes the code verifier in order to produce the
code_challenge
. The hash is produced using the hashing algorithm specified by thecode_challenge_method
. - Send the code challenge: During the request to the Authorization Server, the client includes the
code_challenge
and thecode_challenge_method
. - Authenticate the user: The Authorization Server stores the code challenge and proceeds with user authentication. Once the user authenticates, the authorization server generates an
authorization_code
and returns it to the calling application. - Exchange the code with the verifier: The application can now exchange the authorization code for an access token. To do so, it sends a request to the Authorization Server that includes the
authorization_code
and thecode_verifier
generated in the first step. - Validation: The Authorization Server hashes the
code_verifier
, using thecode_challenge_method
it received in the 3rd step, and compares it to the storedcode_challenge
. If they match, an access token is issued; otherwise, an error occurs. The access token can then be used for secure API calls.

This effectively solves the code interception attack problem, as the malicious application would need not only the authorization_code
but also the code_verifier
to obtain an access token. Intercepting the code challenge won’t help either, since SHA-256
is a one-way hashing algorithm that cannot be decrypted.
Why use PKCE with server-side apps?
If server-side (confidential) apps can store a client secret, you might wonder: isn’t the Authorization Code flow already secure enough?
The short answer: PKCE adds protection against attacks that the client secret alone doesn’t cover.
- Defense against code injection: Even in server-side apps, an attacker could attempt to inject or swap authorization codes during the redirect. With PKCE, the attacker would also need the original
code_verifier
to succeed, something they don’t have. - Safer in multi-device and proxy scenarios: Network intermediaries, compromised browsers, or malicious extensions can sometimes expose codes in ways that a client secret doesn’t protect against. PKCE ensures only the app that initiated the login can redeem the code.
- Consistent, universal security model: By requiring PKCE everywhere, OAuth 2.1 eliminates confusion about when to use it. Developers no longer need to reason about whether their app is “public” or “confidential”. The guidance is simple: always use PKCE.
- Future-proofing: As OAuth continues to evolve, flows without PKCE will be considered outdated. Adopting it now keeps your implementation aligned with the direction of the spec.
In other words, PKCE isn’t just a workaround for apps without secrets; it’s an additional safeguard that strengthens the Authorization Code flow for every client.
How to implement PKCE
If you’ve implemented PKCE before, the steps haven’t changed with OAuth 2.1. Here’s a simple example:
Common pitfalls
Even with PKCE in place, there are a few common mistakes that can weaken security or break your implementation:
- Reusing code verifiers: It’s important not to hard-code the code verifier and to generate a new one for each authorization request. If not, you are exposing yourself to replay attacks.
- Using the
plain
method: OAuth 2.1 strongly recommendsS256
as the only safe option. Don’t useplain
as it doesn’t offer any protection. - Not storing the verifier securely during the flow: The client must keep the
code_verifier
until the code exchange is complete. Losing or overwriting it will cause the flow to fail. - Assuming PKCE is optional: In OAuth 2.1, it’s required for all clients. Don’t rely on old documentation that still says “PKCE is only for public clients.”
- Inconsistent implementation across libraries: Some OAuth libraries implement PKCE automatically, while others require you to enable it explicitly. Make sure you understand how your SDK or framework handles PKCE so you don’t accidentally skip it.
- Mixing PKCE with deprecated flows: PKCE only applies to the Authorization Code Grant. If you’re still using implicit or password grant flows, it’s time to migrate; OAuth 2.1 removes those for good reason.
OAuth 2.1 today: Draft, but ready to use
It’s worth noting that OAuth 2.1 is still officially a draft specification, and OAuth 2.0 servers aren’t going away any time soon. The ecosystem will continue to support OAuth 2.0 for years to come.
But here’s the important part: OAuth 2.1 doesn’t reinvent OAuth. Instead, it formalizes existing best practices and removes outdated or risky flows that security experts have already advised against for years. This means there’s no downside to adopting OAuth 2.1 guidance today, your implementation will still be compatible with current servers, while also being more secure by design.
If you’re starting a new project, you should follow OAuth 2.1 from the beginning. You’ll be protecting your users against known vulnerabilities, aligning with the future direction of the standard, and ensuring your application is secure from day one.
!!Want to dive deeper into what changed in OAuth 2.1 beyond PKCE? Check out our guide to OAuth 2.1 updates and how to migrate.!!
How WorkOS can help
WorkOS simplifies OAuth 2.1 adoption by providing a turnkey, enterprise-ready OAuth 2.0 platform that already aligns with modern security best practices. With built-in support for Authorization Code + PKCE, exact redirect URI enforcement (we only allow wildcards in staging envs), refresh token rotation, and secure token storage patterns, WorkOS helps you accelerate compliance while eliminating boilerplate.
Whether you're upgrading an existing app or building new integrations, WorkOS makes implementing secure, standards-compliant auth flows effortless, so you can focus on building great user experiences, not protocol minutiae.