PKCE in OpenID Connect
An authorization code travels back to the client through the user's browser, and that trip is the weak point of the Authorization Code Flow. A redirect can be observed, logged, or captured by another application registered for the same URI scheme. Whoever ends up holding the code can present it at the token endpoint and receive tokens, because the code alone used to be enough.
Proof Key for Code Exchange closes that gap. The client invents a fresh secret before the flow starts, sends only a hash of it in the authorization request, and must produce the original when redeeming the code. A stolen code is then worthless: the thief has the code but not the secret it was bound to.
TL;DR
PKCE adds three parameters to a flow you already run, and no state on the client beyond one string per login attempt.
- Before redirecting, the client generates a
code_verifier: 43 to 128 unreserved characters, from a cryptographic random source. - The authorization request carries
code_challenge(the base64url-encoded SHA-256 of the verifier) andcode_challenge_method=S256. - The token request carries the original
code_verifier. The server recomputes the hash and compares.
RFC 9700, the OAuth 2.0 Security Best Current Practice, makes PKCE a MUST for public clients and a RECOMMENDED for confidential ones, and requires every authorization server to support it. The mechanism itself is specified in RFC 7636.
The attack PKCE stops
Two related attacks share one root cause: the authorization code is a bearer value in transit.
Code interception. The code arrives as a query parameter on a redirect. On a mobile device, another installed application can claim the same custom URI scheme and receive the redirect instead of the legitimate client. In a browser, the code lands in the address bar, the history, the server log of whatever handled the redirect, and any Referer header sent onward. A public client has no client secret to fall back on, so the code is the only thing standing between an attacker and a token.
Code injection. The attacker does not steal a code, but plants one. Having obtained a code bound to their own account, the attacker gets the victim's client to redeem it, so the victim's session ends up holding tokens for the attacker's resources. RFC 9700, section 4.5 describes the full sequence.
PKCE answers both, because it stops treating the code as sufficient on its own. The code becomes usable only together with a secret that never left the client that started the flow.
How it works
The code verifier
The client creates one code_verifier per authorization request, not per session and not per installation. RFC 7636, section 4.1 fixes the alphabet and the length:
code-verifier = 43*128unreserved
unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"The recommended construction is 32 octets from a cryptographic random number generator, base64url-encoded, which lands exactly on the 43-character minimum. That is a floor, not a target: the value must be unguessable, and a counter, a timestamp, or a session identifier padded to 43 characters satisfies the ABNF while defeating the purpose.
The code challenge
The challenge is derived from the verifier by one of two transformations:
code_challenge_method | code_challenge |
|---|---|
S256 | BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) |
plain | the verifier itself |
Using the example from RFC 7636, appendix B, a verifier of
dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXkproduces the challenge
E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cMNote that base64url here carries no = padding, and the comparison at the token endpoint is case-sensitive.
The two requests
The authorization request adds the challenge and names the method:
GET /connect/authorize
?response_type=code
&client_id=s6BhdRkqt3
&redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb
&scope=openid%20profile
&state=af0ifjsldkj
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256The server stores both values against the code it issues. The token request then presents the verifier:
POST /connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb
&client_id=s6BhdRkqt3
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXkThe server recomputes the challenge from the presented verifier using the method it recorded, and issues tokens only on an exact match.
Why S256 and not plain
code_challenge_method is optional in the wire format and defaults to plain when it is absent (RFC 7636, section 4.3). That default is the single most consequential detail on this page: a client that sends a challenge and forgets the method has silently asked for the weakest variant, and the flow still succeeds.
Under plain the challenge is the verifier, so anyone who reads the authorization request learns the secret and can redeem an intercepted code. The transformation exists only for constrained environments that cannot compute SHA-256, and RFC 7636, section 7.2 states that it SHOULD NOT be used in new implementations. Clients capable of S256 MUST use it, and MUST NOT fall back to plain after trying S256: a server that rejects S256 is either broken or being impersonated, and retrying in the clear hands the attacker exactly what the retry was meant to recover from.
Which clients need it
The original framing was that PKCE protects public clients: native applications and single-page applications, which cannot keep a client secret. That framing is out of date. RFC 9700, section 2.1.1 sets the current rule:
- Public clients MUST use PKCE.
- For confidential clients PKCE is RECOMMENDED, because it protects against code injection and, as a side effect, prevents CSRF even against an attacker strong enough to defeat
state. - A confidential OpenID Connect client MAY use the nonce parameter and the matching ID token claim instead, with the additional precautions the same document sets out.
The specification adds the point explicitly: although PKCE was designed to protect native apps, the advice applies to every kind of OAuth client, web applications included.
One requirement is easy to miss. The challenge must be transaction-specific: a value reused across authorization requests is no better than a constant, and authorization servers are encouraged to detect clients that do it.
PKCE compared with the implicit flow
Both were answers to the same question, which is how a browser-based application obtains tokens without a backend. They answer it differently, and only one of them survived.
| Implicit Flow | Authorization Code Flow with PKCE | |
|---|---|---|
| What the redirect carries | the access token itself | an authorization code |
| Where the value lands | URL fragment, browser history, Referer | same places, but the code is useless without the verifier |
| Refresh tokens | not issued | issued normally |
| Current status | removed from OAuth 2.1 and advised against by RFC 9700 | the recommended flow for public clients |
The implicit flow put the payload itself in the address bar and had no way to bind it to the client that asked for it. PKCE keeps the same redirect shape and makes what travels through it worthless on its own. That is why the migration path from implicit is PKCE and not the other way round, and why "should we still use implicit" no longer has a version that ends in yes.
For the full sequence of flows and how each one closed the previous one's hole, see From Implicit to Authorization Code with PKCE & BFF. Where the client does have a backend, the Backend-For-Frontend pattern keeps tokens out of the browser entirely, and PKCE still applies to the flow that backend runs.
What the authorization server has to enforce
PKCE is not a client-side feature. Half of it lives on the server, and a server that implements only the happy path leaves the mechanism defeatable.
- Support it at all. Authorization servers MUST support PKCE, and MUST provide a way for clients to detect that support. The recommended way is the
code_challenge_methods_supportedelement of the authorization server metadata. - Enforce the verifier once a challenge was sent. If the authorization request carried a valid
code_challenge, the token endpoint MUST require the matchingcode_verifier. - Refuse the downgrade. RFC 9700, section 4.8 describes the PKCE downgrade attack: the attacker strips
code_challengefrom an authorization request on their own device, obtains a code bound to no challenge, and injects it into the victim's session. The client duly sends its owncode_verifier, and a server that simply ignores an unexpected verifier issues tokens for the attacker's account. The countermeasure is exact: a token request containingcode_verifieris accepted only if the authorization request containedcode_challenge.
PKCE in Abblix OIDC Server
Abblix OIDC Server implements the server half of the mechanism, and all three rules above are enforced rather than optional.
PkceValidator handles the authorization request. PKCE is required by default for any response_type that yields a code, and the absence of code_challenge fails the request; a pure implicit request is exempt, because there is no code for a challenge to protect. When a client presents plain, the request is rejected unless that client is explicitly configured to allow it. Under a security profile that pins the method, such as FAPI 2.0, anything other than S256 is rejected before the per-client check, so the profile cannot be loosened per client. With reuse detection enabled, a code_challenge this client has already used is refused, which is the transaction-specific requirement made enforceable rather than advisory.
AuthorizationCodeGrantHandler handles the token request. It recomputes the challenge from the presented verifier and compares ordinally, since base64url and the plain verifier are both case-sensitive. A code issued with a challenge and redeemed without a verifier is rejected, and so is the mirror image: a verifier presented for a code that was issued without a challenge is treated as a downgrade attempt rather than silently ignored.
Beyond the registered methods, the library also computes S512. It is a non-standard extension, not present in the IANA registry, and interoperable only where both ends agree on it; S256 remains the method to use.
Mistakes that survive testing
Each of these produces a flow that works, which is why they reach production.
- Omitting
code_challenge_method. The parameter defaults toplain, so the login succeeds and the protection is gone. Send it explicitly, always. - Deriving the verifier from something. A hash of the session id, a UUID from a non-cryptographic source, a value the client can be made to repeat: all pass the length check and none of them are unguessable.
- Reusing one verifier for a whole session. The specification requires a fresh value per authorization request, and a reused challenge undoes the binding between this login attempt and this code.
- Treating PKCE as a substitute for
state. It can replacestatefor CSRF purposes, but only once the client has confirmed the server supports PKCE. Droppingstateagainst a server that silently ignores the challenge leaves neither protection in place. - Skipping PKCE because the client is confidential. The client secret proves which client is redeeming the code. It does not prove that this is the code that client asked for, which is exactly what code injection exploits.