{"record":{"id":"31617c2dc8e2e182","repo":"apereo/cas","slug":"code-verification-does-not-match-the-challenge-ass","errorCode":null,"errorMessage":"Code verification does not match the challenge assigned to: ","messagePattern":"Code verification does not match the challenge assigned to: ","errorType":"exception","errorClass":"CredentialsException","httpStatus":null,"severity":"error","filePath":"support/cas-server-support-oauth-core-api/src/main/java/org/apereo/cas/support/oauth/authenticator/OAuth20ProofKeyCodeExchangeAuthenticator.java","lineNumber":97,"sourceCode":"        val codeVerifier = getRequestParameterResolver()\n            .resolveRequestParameter(callContext.webContext(), OAuth20Constants.CODE_VERIFIER)\n            .map(String::valueOf).orElse(StringUtils.EMPTY);\n        val code = getRequestParameterResolver()\n            .resolveRequestParameter(callContext.webContext(), OAuth20Constants.CODE)\n            .map(String::valueOf).orElse(StringUtils.EMPTY);\n\n        LOGGER.debug(\"Received PKCE code verifier [{}] along with code [{}]\", codeVerifier, code);\n        val token = getTicketRegistry().getTicket(code, OAuth20Code.class);\n        if (token == null || token.isExpired()) {\n            LOGGER.error(\"Provided code [{}] is either not found in the ticket registry or has expired\", code);\n            throw new CredentialsException(\"Invalid token: \" + code);\n        }\n\n        val method = StringUtils.defaultIfEmpty(token.getCodeChallengeMethod(), \"plain\");\n        val hash = calculateCodeVerifierHash(method, codeVerifier);\n        if (!hash.equalsIgnoreCase(token.getCodeChallenge())) {\n            LOGGER.error(\"Code verifier [{}] does not match the challenge [{}]\", hash, token.getCodeChallenge());\n            throw new CredentialsException(\"Code verification does not match the challenge assigned to: \" + token.getId());\n        }\n        LOGGER.debug(\"Validated code verifier using verification method [{}]\", method);\n    }\n}\n","sourceCodeStart":79,"sourceCodeEnd":102,"githubUrl":"https://github.com/apereo/cas/blob/e7288fc434b4f4505b8452e1a57e8fb3111bb863/support/cas-server-support-oauth-core-api/src/main/java/org/apereo/cas/support/oauth/authenticator/OAuth20ProofKeyCodeExchangeAuthenticator.java#L79-L102","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)"],"exampleFix":"// before\ncodeChallenge = Base64.getEncoder().encodeToString(digest);\n// after\ncodeChallenge = Base64.getUrlEncoder().withoutPadding().encodeToString(digest);","handlingStrategy":"validation","validationCode":"const digest = crypto.createHash('sha256').update(codeVerifier).digest('base64url');\nif (digest !== storedCodeChallenge) throw new Error('PKCE verifier does not match challenge before sending');","typeGuard":null,"tryCatchPattern":"try {\n  await requestToken({ code, codeVerifier });\n} catch (e) {\n  if (String(e.message).includes('Code verification does not match')) {\n    // restart full PKCE flow: new verifier + challenge\n  }\n}","preventionTips":["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"],"tags":["oauth2","pkce","credentials"],"backgroundTag":"invalid-argument-value","analyzedSha":"e7288fc434b4f4505b8452e1a57e8fb3111bb863","analyzedAt":"2026-09-08T15:39:16.015Z","contentChangedAt":"2026-09-08T15:39:16.015Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}