spring-projects/spring-security · error · OAuth2AuthenticationException
invalid_client
invalid_client
Error message
Client authentication failed: client_id
What it means
JwtClientAssertionAuthenticationProvider authenticates clients via a JWT client assertion (private_key_jwt / client_secret_jwt). After validating the assertion, it resolves the client via RegisteredClientRepository.findByClientId using the `iss`/principal of the assertion. If no registered client matches, it throws OAuth2AuthenticationException with error code invalid_client and message 'Client authentication failed: client_id'.
Source
Thrown at oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/JwtClientAssertionAuthenticationProvider.java:96
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
Assert.notNull(authorizationService, "authorizationService cannot be null");
this.registeredClientRepository = registeredClientRepository;
this.codeVerifierAuthenticator = new CodeVerifierAuthenticator(authorizationService);
this.jwtDecoderFactory = new JwtClientAssertionDecoderFactory();
}
@Override
public @Nullable Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2ClientAuthenticationToken clientAuthentication = (OAuth2ClientAuthenticationToken) authentication;
if (!JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD.equals(clientAuthentication.getClientAuthenticationMethod())) {
return null;
}
String clientId = clientAuthentication.getPrincipal().toString();
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
if (registeredClient == null) {
throw invalidClientException(OAuth2ParameterNames.CLIENT_ID);
}
if (this.logger.isTraceEnabled()) {
this.logger.trace("Retrieved registered client");
}
// @formatter:off
if (!registeredClient.getClientAuthenticationMethods().contains(ClientAuthenticationMethod.PRIVATE_KEY_JWT) &&
!registeredClient.getClientAuthenticationMethods().contains(ClientAuthenticationMethod.CLIENT_SECRET_JWT)) {
throw invalidClientException("authentication_method");
}
// @formatter:on
Object credentials = clientAuthentication.getCredentials();
if (credentials == null) {
throw invalidClientException("credentials");
}
View on GitHub (pinned to 96852e8860)
Solutions
- Register a RegisteredClient with a clientId exactly equal to the JWT assertion's iss/sub claim.
- Verify the client is building its assertion with the correct client_id for the target environment.
- If using a database-backed RegisteredClientRepository, confirm the client row exists and is loaded (check findByClientId query/tenant filters).
- Compare the decoded JWT's iss and sub claims against server-side registered client IDs, and keep the aud claim pointed at the token endpoint /issuer/oauth2/token.
Example fix
// before: assertion built with wrong/stale client id
JwtClientAssertion.builder().clientId("old-client")...
// after: iss/sub must match a RegisteredClient registered on the server
RegisteredClient client = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("my-client")
.clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT)
...build();
registeredClientRepository.save(client); // iss/sub of assertion = "my-client" Defensive patterns
Strategy: try-catch
Validate before calling
// decode assertion payload and verify iss/sub are registered client ids
String[] parts = clientAssertion.split("\\.");
String iss = json(parse(Base64.getUrlDecoder().decode(parts[1]))).iss;
if (!registeredClientIds.contains(iss)) {
throw new IllegalStateException("Client not registered: " + iss);
} Try / catch
try {
TokenResponse resp = tokenClient.withClientAssertion(jwt).getToken();
} catch (OAuth2AuthenticationException e) {
if ("invalid_client".equals(e.getError().getErrorCode())) {
// verify client registration / assertion iss+sub before retrying
throw new ClientRegistrationException("Unknown or unregistered client_id", e);
} else { throw e; }
} Prevention
- Keep the RegisteredClientRepository and client configs in sync (same client_id as assertion iss/sub).
- Separate credentials per environment; never reuse a client_id across staging/production.
- Seed database-backed client registrations during deployment.
- Log the assertion's iss/sub (never the signature) when debugging client authentication.
When it happens
Trigger: Token request with a signed JWT client_assertion (client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer) whose `iss` (and typically `sub`) claim names a client_id not present in the RegisteredClientRepository — unknown client, wrong environment, or typo in the assertion's iss/sub claims.
Common situations: Client not yet registered on the server (missing RegisteredClient with that clientId); assertion issued for a staging client_id used against production; iss/sub mismatch after rotating client configuration; multi-tenant setup where the repository only holds tenants' clients from a different realm; deployment forgot to seed the client into a JDBC-backed RegisteredClientRepository.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
AI-assisted analysis of spring-projects/spring-security@96852e8860 (2026-09-10).
Data as JSON: /api/errors/2ae10db2be1b8310.
Report an issue: GitHub.