PKCE
Proof Key for Code Exchange (RFC 7636) defends the authorization code grant against code interception. The client picks a high-entropy secret (code_verifier), sends only its SHA-256 hash (code_challenge) with the authorization request, and supplies the original verifier when redeeming the code at the token endpoint. ThunderID rejects the exchange unless the hashes match.
OAuth 2.1 makes PKCE mandatory for public clients and recommends it for every authorization code flow. ThunderID follows this baseline.
How It Works
How ThunderID Implements It
| Aspect | Behavior |
|---|---|
| Supported challenge methods | S256 only: plain is rejected with invalid_request |
| Public clients | PKCE is mandatory for public clients (token_endpoint_auth_method=none) |
| Confidential clients | pkceRequired defaults to false but can be enabled per application |
| Verifier rules | 43 to 128 characters; ASCII unreserved set per RFC 7636 §4.1 |
| Challenge encoding | Base64url-encoded SHA-256 of the verifier; 43 characters; no padding |
| Storage | The challenge is bound to the issued authorization code; the code is single-use |
| Interaction with PAR | The code_challenge is supplied at PAR push time; it cannot be added or changed at the /oauth2/authorize redirect |
| Failure mode | Missing verifier, wrong verifier, or mismatched challenge → invalid_grant at the token endpoint |
Try It in ThunderID
- Console
- Dynamic Client Registration
- Open Applications or Agents in the ThunderID Console and select your client.
- Open the Advanced Settings tab.
- Toggle Require PKCE on for confidential clients (already on for public clients).
- Save.
Public clients receive pkceRequired automatically. To enable it on a confidential client:
POST /oauth2/dcr/register
Content-Type: application/json
{
"client_name": "My App",
"redirect_uris": ["https://app.example.com/callback"],
"grant_types": ["authorization_code"],
"response_types": ["code"],
"token_endpoint_auth_method": "client_secret_basic",
"require_pkce": true
}
Generate the Verifier and Challenge
// Browser / Node
function base64url(buf) {
return btoa(String.fromCharCode(...new Uint8Array(buf)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
const verifierBytes = crypto.getRandomValues(new Uint8Array(64));
const code_verifier = base64url(verifierBytes);
const hash = await crypto.subtle.digest('SHA-256',
new TextEncoder().encode(code_verifier));
const code_challenge = base64url(hash);
Send code_challenge on the authorization request:
GET /oauth2/authorize
?response_type=code
&client_id=$CLIENT_ID
&redirect_uri=https://app.example.com/callback
&scope=openid%20profile
&state=xyz
&code_challenge=$CODE_CHALLENGE
&code_challenge_method=S256
Send code_verifier on the token exchange:
curl -X POST https://thunderid.example.com/oauth2/token \
-d "grant_type=authorization_code" \
-d "code=$CODE" \
-d "redirect_uri=https://app.example.com/callback" \
-d "client_id=$CLIENT_ID" \
-d "code_verifier=$CODE_VERIFIER"
Related Guides
- Authorization Code, the flow PKCE protects
- Pushed Authorization Requests, PAR + PKCE together is the recommended high-security pattern
- Client Authentication Methods,
nonerequires PKCE