apereo/cas · error · CredentialsException
Code verification does not match the challenge assigned to:
Error message
Code verification does not match the challenge assigned to:
What it means
This CredentialsException is thrown by the OAuth 2.0 PKCE authenticator when the client-supplied code_verifier, after hashing with the challenge method stored on the token (plain or S256), does not equal the code_challenge bound to the authorization code/token. It protects against authorization-code interception: only the client that ran the original PKCE challenge can redeem the code.
Solutions
- Regenerate the code_verifier and restart the PKCE flow so the code_challenge sent to /authorize is derived (S256: BASE64URL(SHA256(verifier))) from that exact verifier
- Verify the code_challenge_method stored on the token matches the client's hashing implementation and that the digest is base64url-encoded without padding
- Ensure the same verifier string (no trimming, no re-encoding) is sent in the token request that produced the challenge
- Check for proxies/gateways altering query/body parameters (e.g. plus-sign handling corrupting base64url values)
Example fix
// before codeChallenge = Base64.getEncoder().encodeToString(digest); // after codeChallenge = Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
Defensive patterns
Strategy: validation
Validate before calling
const digest = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
if (digest !== storedCodeChallenge) throw new Error('PKCE verifier does not match challenge before sending'); Try / catch
try {
await requestToken({ code, codeVerifier });
} catch (e) {
if (String(e.message).includes('Code verification does not match')) {
// restart full PKCE flow: new verifier + challenge
}
} Prevention
- Generate the challenge as BASE64URL(SHA256(verifier)) with no padding when using S256
- Keep the verifier immutable from /authorize through the token request
- Use a proven OAuth client library instead of hand-rolled PKCE hashing
When it happens
Trigger: Redeeming an authorization code (or token request requiring PKCE) where the code_verifier parameter is missing, was generated for a different authorization request, the code_challenge_method used at authorize time differs from the hashing applied now (e.g. server expects S256 but client sent a plain verifier), or the challenge/verifier pair was corrupted in transit.
Common situations: Client library misconfiguration switching between plain and S256; regenerating the verifier after the authorize redirect; custom mobile/SPA clients hashing the verifier with the wrong algorithm (e.g. base64 vs base64url, including padding in the S256 digest); replaying an old code_verifier with a new code.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- Code verification method is unrecognized:
- Client Credentials provided is not valid for service:
- Invalid token:
- Resolved credentials for this transaction are empty
- No credentials can be extracted to authenticate the REST…
AI-assisted analysis of apereo/cas@e7288fc434 (2026-09-08).
Data as JSON: /api/errors/31617c2dc8e2e182.
Report an issue: GitHub.
Appendix: source
Thrown at support/cas-server-support-oauth-core-api/src/main/java/org/apereo/cas/support/oauth/authenticator/OAuth20ProofKeyCodeExchangeAuthenticator.java:97
val codeVerifier = getRequestParameterResolver()
.resolveRequestParameter(callContext.webContext(), OAuth20Constants.CODE_VERIFIER)
.map(String::valueOf).orElse(StringUtils.EMPTY);
val code = getRequestParameterResolver()
.resolveRequestParameter(callContext.webContext(), OAuth20Constants.CODE)
.map(String::valueOf).orElse(StringUtils.EMPTY);
LOGGER.debug("Received PKCE code verifier [{}] along with code [{}]", codeVerifier, code);
val token = getTicketRegistry().getTicket(code, OAuth20Code.class);
if (token == null || token.isExpired()) {
LOGGER.error("Provided code [{}] is either not found in the ticket registry or has expired", code);
throw new CredentialsException("Invalid token: " + code);
}
val method = StringUtils.defaultIfEmpty(token.getCodeChallengeMethod(), "plain");
val hash = calculateCodeVerifierHash(method, codeVerifier);
if (!hash.equalsIgnoreCase(token.getCodeChallenge())) {
LOGGER.error("Code verifier [{}] does not match the challenge [{}]", hash, token.getCodeChallenge());
throw new CredentialsException("Code verification does not match the challenge assigned to: " + token.getId());
}
LOGGER.debug("Validated code verifier using verification method [{}]", method);
}
}
View on GitHub (pinned to e7288fc434)