Saltar al contenido
Esta página aún no está traducida.

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 one piece of client state per login attempt: keyed, not singular, because concurrent attempts each keep their own.

  • 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) and code_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 before PKCE the code was the only thing standing between an attacker and a token.

Code injection. The attacker does not need the victim's browser at all. Having obtained a code that was issued for the victim, the attacker starts their own flow with the legitimate client and swaps their fresh code for the stolen one on the way back. The client redeems it under its own credentials, and the attacker's session at that client is now bound to the victim's identity and resources. RFC 9700, section 4.5 walks through the six steps.

The mirror image, planting a code of the attacker's own into the victim's session, is a different attack: that is the CSRF family of section 4.7, and in its PKCE-specific form the downgrade attack described further down this page.

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
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_methodcode_challenge
S256BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
plainthe verifier itself

Using the example from RFC 7636, appendix B, a verifier of

Code
dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

produces the challenge

Code
E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM

Note 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:

Code
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=S256

The server stores both values against the code it issues. The token request then presents the verifier:

Code
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_wW1gFWFOEjXk

The 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 detail on this page that costs the most when it is missed: 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 for compatibility with deployments that predate PKCE and whose request path is already protected, and for constrained environments that cannot compute SHA-256; 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, and it is usually quoted with its second half removed. The challenge must be transaction-specific and securely bound to the client and the user agent in which the transaction was started. A fresh random value satisfies only the first half.

The verifier also has to be stored where nothing but the browser that began this flow can present it, which in practice means the client's own per-browser session keyed by state, and never a slot that a second login attempt overwrites: two tabs mean two flows in the air, and one slot means the first callback dies with invalid_grant having reused nothing. Authorization servers, for their part, are encouraged to detect clients that send constant challenges.

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 FlowAuthorization Code Flow with PKCE
What the redirect carriesthe access token itselfan authorization code
Where the value landsURL fragment and browser history; the fragment never reaches a server, and Referer must omit itquery string, so the redirect handler's log and any Referer sent onward as well - but the code is useless without the verifier
Refresh tokensnot issuedissued normally
Current statusremoved from OAuth 2.1 and advised against by RFC 9700the 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 to get access tokens" no longer has a version that ends in yes. Pure authentication with response_type=id_token and form_post is the narrower case the BCP does not reach, since it puts no access token in the response; the code flow is still the better answer there.

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 RFC 9700 states all three of the server's obligations as MUSTs.

  • 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_supported element 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 matching code_verifier.
  • Refuse the downgrade. RFC 9700, section 4.8 describes the PKCE downgrade attack: the attacker strips code_challenge from 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 own code_verifier, and a server that simply ignores an unexpected verifier issues tokens for the attacker's account. The countermeasure is exact: a token request containing code_verifier is accepted only if the authorization request contained code_challenge.

PKCE in Abblix OIDC Server

Abblix OIDC Server implements the server half of the mechanism, and each of the three rules above is checked in code. The protocol layer is certified by the OpenID Foundation across all login and logout profiles, which is the external check on claims of this kind; the paragraphs below describe behaviour that certification does not cover, so they name what a reader can go and reproduce.

At the authorization endpoint, a statically configured client requires PKCE unless it opts out: a missing code_challenge fails any response_type that yields a code, while a pure implicit request is exempt because there is no code for a challenge to protect. That exemption is not a way in: the implicit flow is opt-in, so without it the token and id_token response types are refused server-wide whatever a client is registered for, and a client is limited in any case to the response types it registered. That default is stricter than RFC 9700, which makes PKCE a MUST only for public clients, and it is on by default.

A client created through Dynamic Client Registration inherits the same default: the registration request may set pkce_required explicitly, and a request that says nothing leaves the client on the server's own value. Earlier versions shipped the opposite default on that path, so a client registered dynamically did not require PKCE unless the request asked for it.

plain is refused unless the client is explicitly configured to allow it, and under a profile that pins the method, such as FAPI 2.0, anything other than S256 is refused before the per-client check, so the profile cannot be loosened one client at a time. The trap from the top of this page has a definite answer here: a request that carries a challenge but no code_challenge_method is not quietly treated as plain. It passes the authorization endpoint and then fails the exchange, because the token endpoint requires the method that the code was bound with. Read discovery with that in mind: code_challenge_methods_supported advertises what the server can do, including plain, while whether a given client may use it is per-client policy that metadata does not express.

With reuse detection enabled, a code_challenge this client has already used is refused. RFC 9700 puts the MUST on the client, which is the party that makes each challenge transaction-specific. The server it only asks to make a reasonable effort at detecting constant values, and this is that effort made explicit.

The token endpoint 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. Against a strict server the first and third fail loudly at the token exchange; the rest are client-side and no server can see them.

  • Omitting code_challenge_method. The parameter defaults to plain, so against a permissive server the login succeeds and the protection is gone, while a strict one fails with an error naming plain - a word the client never sent, which puzzles everyone for an afternoon. 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 replace state for CSRF purposes, but only once the client has confirmed the server supports PKCE, and it replaces nothing else. A client that can talk to more than one authorization server is required by RFC 9700, section 2.1 to defend against mix-up, using the iss response parameter of RFC 9207 or the iss claim of the ID token. PKCE supplies none of that, so dropping state on the strength of it can leave two protections missing instead of one.
  • 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.