# 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](https://www.abblix.com/en/docs/glossary-overview#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](https://www.abblix.com/en/docs/glossary-overview#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) and `code_challenge_method=S256`.
- The token request carries the original `code_verifier`. The server recomputes the hash and compares.

[RFC 9700](https://datatracker.ietf.org/doc/html/rfc9700), 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](https://datatracker.ietf.org/doc/html/rfc7636).

## 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](https://www.abblix.com/en/docs/glossary-overview#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](https://datatracker.ietf.org/doc/html/rfc9700#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](https://datatracker.ietf.org/doc/html/rfc7636#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](https://datatracker.ietf.org/doc/html/rfc7636#appendix-B), a verifier of

```
dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
```

produces the challenge

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

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

```
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](https://datatracker.ietf.org/doc/html/rfc7636#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](https://datatracker.ietf.org/doc/html/rfc7636#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](https://datatracker.ietf.org/doc/html/rfc9700#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](https://www.abblix.com/en/docs/glossary-overview#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](https://www.abblix.com/en/docs/glossary-overview#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](https://www.abblix.com/en/docs/openid-connect-flows-explained-simply-from-implicit-flows-to-authorization-code-flows-with-pkce-and-bff). Where the client does have a backend, the [Backend-For-Frontend pattern](https://www.abblix.com/en/docs/bff-pattern-in-oauth-and-openid-connect) 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_supported` element of the [authorization server metadata](https://www.abblix.com/en/docs/glossary-overview#openid-provider-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](https://datatracker.ietf.org/doc/html/rfc9700#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 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 to `plain`, 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 replace `state` for CSRF purposes, but only once the client has confirmed the server supports PKCE. Dropping `state` against 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.
