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

  1. Register a RegisteredClient with a clientId exactly equal to the JWT assertion's iss/sub claim.
  2. Verify the client is building its assertion with the correct client_id for the target environment.
  3. If using a database-backed RegisteredClientRepository, confirm the client row exists and is loaded (check findByClientId query/tenant filters).
  4. 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

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

Related errors


AI-assisted analysis of spring-projects/spring-security@96852e8860 (2026-09-10). Data as JSON: /api/errors/2ae10db2be1b8310. Report an issue: GitHub.