spring-projects/spring-security · error · OAuth2AuthenticationException
invalid_grant
invalid_grant
Error message
Client authentication failed: code_verifier
What it means
CodeVerifierAuthenticator.authenticateRequired verifies the PKCE code_verifier when the authorization grant requires it (public clients using authorization_code + PKCE). If the client authentication does not include/validate a code_verifier matching the stored code_challenge, invalid_grant with the 'code_verifier' detail is thrown on the token endpoint.
Solutions
- Include code_verifier in the token request body and ensure it is the exact original string used to derive the code_challenge (for S256: BASE64URL(SHA256(verifier))).
- Persist the verifier client-side between the authorization request and the token exchange (session storage, keychain, secure storage) — do not regenerate it.
- Use a RFC 7636-compliant PKCE library/helper to generate verifier (43-128 chars, unreserved chars) and challenge consistently.
- Confirm the same client_id is used at both endpoints and the authorization code has not expired or been consumed.
Example fix
// before // token request without PKCE params POST /oauth2/token grant_type=authorization_code&code=abc&client_id=spa&redirect_uri=... // after POST /oauth2/token grant_type=authorization_code&code=abc&client_id=spa&redirect_uri=...&code_verifier=<original-verifier>
Defensive patterns
Strategy: validation
Validate before calling
// client side: verify verifier before token exchange
String verifier = session.getAttribute("pkce_verifier");
if (verifier == null || verifier.length() < 43 || verifier.length() > 128) {
throw new IllegalStateException("PKCE code_verifier missing or invalid length");
}
// ensure it is appended to the token request body Try / catch
try {
exchangeCodeForToken(code, verifier);
} catch (OAuth2ErrorResponseException e) {
if ("invalid_grant".equals(e.getError())) {
// restart the full PKCE flow: new authorization request with fresh verifier/challenge
}
} Prevention
- Persist the code_verifier for the lifetime of the authorization flow (session/secure storage).
- Generate challenges with the same S256 transformation the server expects.
- Use a maintained OAuth2/OIDC client library that handles PKCE automatically.
- Never reuse authorization codes or verifiers across flows.
When it happens
Trigger: Token request for a PKCE authorization_code grant (public client) processed by CodeVerifierAuthenticator.authenticateRequired where authenticate() returns false — missing code_verifier parameter, or the provided verifier does not hash to the stored code_challenge.
Common situations: SPA/mobile clients omitting code_verifier in the token request; verifier generated with a different transformation than the challenge (S256 vs plain); code_challenge stored at authorization time but verifier lost across redirect; parameters sent in the wrong request (auth request vs token request).
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- invalid_client
- Invalid Client Registration: + fieldName
- invalid_request
- Invalidated authorization code used by registered client
- Invalidated authorization token(s) previously issued to…
AI-assisted analysis of spring-projects/spring-security@96852e8860 (2026-09-10).
Data as JSON: /api/errors/1037d607bed8fc49.
Report an issue: GitHub.
Appendix: source
Thrown at oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/CodeVerifierAuthenticator.java:69
* @see OAuth2ClientAuthenticationToken
* @see OAuth2AuthorizationService
*/
final class CodeVerifierAuthenticator {
private static final OAuth2TokenType AUTHORIZATION_CODE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.CODE);
private final Log logger = LogFactory.getLog(getClass());
private final OAuth2AuthorizationService authorizationService;
CodeVerifierAuthenticator(OAuth2AuthorizationService authorizationService) {
Assert.notNull(authorizationService, "authorizationService cannot be null");
this.authorizationService = authorizationService;
}
void authenticateRequired(OAuth2ClientAuthenticationToken clientAuthentication, RegisteredClient registeredClient) {
if (!authenticate(clientAuthentication, registeredClient)) {
throw invalidGrantException(PkceParameterNames.CODE_VERIFIER);
}
}
void authenticateIfAvailable(OAuth2ClientAuthenticationToken clientAuthentication,
RegisteredClient registeredClient) {
authenticate(clientAuthentication, registeredClient);
}
private boolean authenticate(OAuth2ClientAuthenticationToken clientAuthentication,
RegisteredClient registeredClient) {
Map<String, Object> parameters = clientAuthentication.getAdditionalParameters();
if (!authorizationCodeGrant(parameters)) {
return false;
}
String code = (String) parameters.get(OAuth2ParameterNames.CODE);
Assert.hasText(code, "code cannot be empty");View on GitHub (pinned to 96852e8860)