Skip to content
Customer Identity · available nowBuild your first Test integration.Create a Test project
NamoID
All posts
NamoID Blog

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

ReferenceRole
RFC 7636Defines the verifier, challenge and exchange
RFC 9700Published OAuth security BCP, including PKCE deployment guidance
OAuth 2.1 draft 17Consolidates a revised authorization framework and remains a draft
NamoID implementationRequires 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 binding

Use 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

SymptomFirst check
Verifier length rejectedGenerate sufficient random bytes and use the permitted character set
S256 challenge has paddingRemove = padding after base64url encoding
Challenge contains + or /Use URL-safe substitutions
Token exchange reports a mismatchRetrieve the verifier for that exact pending attempt
Failure only after navigationConfirm the chosen storage survives the redirect
Failure with two browser tabsKeep 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.mjs

Then 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