spring-projects/spring-security · error · OAuth2AuthenticationException

INVALID_CLIENT

INVALID_CLIENT

Error message

Failed to find a Signature Verifier for Client: '<registeredClient.getId()>'. Check to ensure you have configured the JWK Set URL.

What it means

In the OAuth2 Authorization Server, when a client authenticates with a JWT client assertion signed with an asymmetric algorithm (e.g. RS256), the server builds a JwtDecoder that fetches the client's public keys via a JWK Set URL. JwtClientAssertionDecoderFactory.buildDecoder throws this INVALID_CLIENT error when the client's JwkSetUrl client setting is missing or blank, so no signature verifier can be constructed.

Source

Thrown at oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/JwtClientAssertionDecoderFactory.java:144

	 * @param jwtValidatorFactory the factory that provides an
	 * {@link OAuth2TokenValidator} for the specified {@link RegisteredClient}
	 */
	public void setJwtValidatorFactory(Function<RegisteredClient, OAuth2TokenValidator<Jwt>> jwtValidatorFactory) {
		Assert.notNull(jwtValidatorFactory, "jwtValidatorFactory cannot be null");
		this.jwtValidatorFactory = jwtValidatorFactory;
	}

	private static NimbusJwtDecoder buildDecoder(RegisteredClient registeredClient) {
		JwsAlgorithm jwsAlgorithm = registeredClient.getClientSettings()
			.getTokenEndpointAuthenticationSigningAlgorithm();
		if (jwsAlgorithm instanceof SignatureAlgorithm) {
			String jwkSetUrl = registeredClient.getClientSettings().getJwkSetUrl();
			if (!StringUtils.hasText(jwkSetUrl)) {
				OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
						"Failed to find a Signature Verifier for Client: '" + registeredClient.getId()
								+ "'. Check to ensure you have configured the JWK Set URL.",
						JWT_CLIENT_AUTHENTICATION_ERROR_URI);
				throw new OAuth2AuthenticationException(oauth2Error);
			}
			return NimbusJwtDecoder.withJwkSetUri(jwkSetUrl)
				.jwsAlgorithm((SignatureAlgorithm) jwsAlgorithm)
				.restOperations(restTemplate)
				.build();
		}
		if (jwsAlgorithm instanceof MacAlgorithm) {
			String clientSecret = registeredClient.getClientSecret();
			if (!StringUtils.hasText(clientSecret)) {
				OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
						"Failed to find a Signature Verifier for Client: '" + registeredClient.getId()
								+ "'. Check to ensure you have configured the client secret.",
						JWT_CLIENT_AUTHENTICATION_ERROR_URI);
				throw new OAuth2AuthenticationException(oauth2Error);
			}
			SecretKeySpec secretKeySpec = new SecretKeySpec(clientSecret.getBytes(StandardCharsets.UTF_8),
					JCA_ALGORITHM_MAPPINGS.get(jwsAlgorithm));
			return NimbusJwtDecoder.withSecretKey(secretKeySpec).macAlgorithm((MacAlgorithm) jwsAlgorithm).build();

View on GitHub (pinned to 96852e8860)

Solutions

  1. Set the JWK Set URL on the client: RegisteredClient.withClient(id).clientSettings(ClientSettings.builder().jwkSetUrl("https://client.example.com/jwks").build()).build()
  2. Verify the stored client record actually contains a non-empty jwkSetUrl (check DB/claim source if clients are loaded dynamically).
  3. Alternatively configure the client's JWK Set URI through your RegisteredClientRepository registration code path used at authorization time.
  4. Confirm the client is actually using private_key_jwt and that the intended auth method matches its registration (token_endpoint_authentication_method).

Example fix

// before
RegisteredClient client = RegisteredClient.withId(UUID.randomUUID().toString())
    .clientId("client-a")
    .clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT)
    .build();

// after
RegisteredClient client = RegisteredClient.withId(UUID.randomUUID().toString())
    .clientId("client-a")
    .clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT)
    .clientSettings(ClientSettings.builder()
        .jwkSetUrl("https://client-a.example.com/jwks")
        .build())
    .build();
Defensive patterns

Strategy: validation

Validate before calling

if (client.getClientSettings() == null ||
    !StringUtils.hasText(client.getClientSettings().getJwkSetUrl())) {
    throw new IllegalStateException(
        "Client " + client.getId() + " uses private_key_jwt but has no jwkSetUrl configured");
}

Try / catch

try {
    // token request with private_key_jwt
} catch (OAuth2AuthenticationException ex) {
    if (OAuth2ErrorCodes.INVALID_CLIENT.equals(ex.getError().getErrorCode())) {
        log.error("Client assertion rejected: {}. Verify jwkSetUrl registration.",
            ex.getError().getDescription());
    }
    throw ex;
}

Prevention

When it happens

Trigger: A client sends a private_key_jwt client assertion, but RegisteredClient.getClientSettings().getJwkSetUrl() was never set (or is empty string/whitespace) on the server for that client, so buildDecoder fails before decoding the assertion.

Common situations: Registering a client for client_secret_jwt/private_key_jwt auth without the jwk-set-url client setting; loading clients from a database where the JWK Set URL column is null; copying a client registration from a symmetric-secret example; typos in settings builder calls.

Understand the failure class

Background: "is required", "must be set", "missing required field": configuration validation errors across open-source libraries — this error's family across 36 libraries.

Related errors


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