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

  1. 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))).
  2. Persist the verifier client-side between the authorization request and the token exchange (session storage, keychain, secure storage) — do not regenerate it.
  3. Use a RFC 7636-compliant PKCE library/helper to generate verifier (43-128 chars, unreserved chars) and challenge consistently.
  4. 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

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

Related errors


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)