PKCE Explained: S256, Client Types and Error Checks
Understand PKCE S256, verifier and challenge lengths, public and confidential clients, standards status, and practical callback diagnostics.
PKCE binds an authorization code to a fresh secret created for one login attempt. Send the S256 challenge with the authorization request. Present the matching verifier at the token endpoint.
NamoID's current authorization-code flow requires PKCE S256 for both public and confidential clients. A confidential client must also satisfy its registered client-authentication requirements.
Understand the code-interception problem
An attacker who obtains an authorization code may try to redeem it first. A public client cannot protect a reusable secret embedded in a browser or installed application. PKCE adds a per-attempt verifier that the client keeps until the exchange. PKCE specification.
PKCE does not authenticate a legal organization or authorize application actions. It also does not protect a verifier that an attacker has already stolen. Keep callback validation, request correlation and secure application handling in place.
Distinguish the published rules from the draft
| Reference | Role |
|---|---|
| RFC 7636 | Defines the verifier, challenge and exchange |
| RFC 9700 | Published OAuth security BCP, including PKCE deployment guidance |
| OAuth 2.1 draft 17 | Consolidates a revised authorization framework and remains a draft |
| NamoID implementation | Requires S256 for the authorization-code flow across client types |
RFC 9700 requires public clients to use PKCE and recommends it for confidential clients. The OAuth 2.1 document checked on 12 November 2026 is draft 17. Keep that status distinct from an issued RFC. OAuth security BCP, OAuth 2.1 draft 17.
Generate the verifier and S256 challenge
The verifier uses 43–128 characters from the specified unreserved character set. The S256 challenge is the unpadded base64url encoding of the SHA-256 digest. A correctly encoded S256 challenge has 43 characters.
function base64url(bytes) {
return btoa(String.fromCharCode(...bytes))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
}
const verifier = base64url(crypto.getRandomValues(new Uint8Array(32)));
const digest = await crypto.subtle.digest(
"SHA-256", new TextEncoder().encode(verifier),
);
const challenge = base64url(new Uint8Array(digest));Generate a new verifier for every attempt. Associate it with the pending request through protected transaction state. Retain it only as long as needed for that exchange.
Follow one attempt through the exchange
Client stores verifier
|
+--> authorization request with S256 challenge
| |
| v
| user authentication
| |
| v
+<----- callback with code
|
+--> token request with original verifier
|
v
server checks challenge bindingUse the registered redirect URI and the provider's exact request contract. Handle the callback once. Correlate it with the pending transaction and validate the expected issuer and applicable identity claims.
For server-rendered applications, keep the verifier in the server-side transaction. For a public client, choose storage appropriate to its architecture and threat model. Browser storage readable by script is not secret from injected script. Browser application architecture.
Diagnose length and encoding errors
| Symptom | First check |
|---|---|
| Verifier length rejected | Generate sufficient random bytes and use the permitted character set |
| S256 challenge has padding | Remove = padding after base64url encoding |
Challenge contains + or / | Use URL-safe substitutions |
| Token exchange reports a mismatch | Retrieve the verifier for that exact pending attempt |
| Failure only after navigation | Confirm the chosen storage survives the redirect |
| Failure with two browser tabs | Keep separate state and verifier records for each attempt |
Do not repair a mismatch by skipping PKCE. Begin a fresh attempt after diagnosing the cause. Avoid copying real codes, state or verifiers into support messages.
Test with a published vector
The downloadable S256 example checks the RFC 7636 test vector, generated lengths and URL-safe encoding. Run it with a current Node.js version.
node pkce-s256-example.mjsThen test your actual callback, client authentication and token exchange in the approved environment. The local hash test does not establish that the complete integration works.
Review your OAuth flow with NamoID
NamoID engineers can help review client type, callback binding and S256 failures. Share your framework and a synthetic request shape in Slack for an early response. Contact the NamoID team