Describe the bug

When creating a custom JwtDecoder for an application with multiple OAuth2TokenValidators, if one of the validators fails and returns an error a ClassCastException is thrown when the DelegatingOAuth2TokenValidator tries to add the failure to it's collection of errors.

java.lang.ClassCastException: class java.util.ArrayList cannot be cast to class java.lang.String (java.util.ArrayList and java.lang.String are in module java.base of loader 'bootstrap')
    at org.springframework.security.oauth2.jwt.JwtClaimValidator.validate(JwtClaimValidator.java:65) ~[spring-security-oauth2-jose-6.5.3.jar:6.5.3]
    at org.springframework.security.oauth2.jwt.JwtClaimValidator.validate(JwtClaimValidator.java:37) ~[spring-security-oauth2-jose-6.5.3.jar:6.5.3]
    at org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator.validate(DelegatingOAuth2TokenValidator.java:59) ~[spring-security-oauth2-core-6.5.3.jar:6.5.3]
    at org.springframework.security.oauth2.jwt.NimbusJwtDecoder.validateJwt(NimbusJwtDecoder.java:193) ~[spring-security-oauth2-jose-6.5.3.jar:6.5.3]
    at org.springframework.security.oauth2.jwt.NimbusJwtDecoder.decode(NimbusJwtDecoder.java:143) ~[spring-security-oauth2-jose-6.5.3.jar:6.5.3]
    at org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider.getJwt(JwtAuthenticationProvider.java:99) ~[spring-security-oauth2-resource-server-6.5.3.jar:6.5.3]
    at org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider.authenticate(JwtAuthenticationProvider.java:88) ~[spring-security-oauth2-resource-server-6.5.3.jar:6.5.3]
    at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:182) ~[spring-security-core-6.5.3.jar:6.5.3]
    at org.springframework.security.authentication.ObservationAuthenticationManager.lambda$authenticate$1(ObservationAuthenticationManager.java:54) ~[spring-security-core-6.5.3.jar:6.5.3]
    at io.micrometer.observation.Observation.observe(Observation.java:564) ~[micrometer-observation-1.15.3.jar:1.15.3]

To Reproduce

Create a JwtDecoder as below and send a request with JWT that fails validation (example: invalid issuerUri)

Image

@Configuration
class LegacyJwtSecretDecoderConfig(private val supabaseProperties: SupabaseProperties) {

    // Supabase Legacy JWT keys uses HS256: This is a symmetric algorithm.
    // It uses a single secret key (your Supabase JWT secret) to both sign and verify the token.
    // This is deprecated and not recommended for production use.

    @Bean
    fun jwtDecoder(): JwtDecoder {
        // The secret key needs to be strong enough for HS256, so Supabase secrets are long
        val secretKey = SecretKeySpec(supabaseProperties.jwtSecret?.toByteArray() ?: ByteArray(0), "HmacSHA256")

        // Create the NimbusJwtDecoder with the secret key
        val jwtDecoder = NimbusJwtDecoder.withSecretKey(secretKey)
            .macAlgorithm(MacAlgorithm.HS256)
            .build()

        // --- Add Issuer and Audience Validators ---

        // Validator for the issuer claim
        val issuerValidator: OAuth2TokenValidator<Jwt?> =
            JwtValidators.createDefaultWithIssuer(supabaseProperties.issuerUri)

        // Validator for the audience claim
        val audienceValidator: OAuth2TokenValidator<Jwt?> = JwtClaimValidator("aud") { aud: String? ->
            aud == "authenticated"
        }

        // Combine all validators
        val withAllValidators: OAuth2TokenValidator<Jwt?> = DelegatingOAuth2TokenValidator(
            issuerValidator,
            audienceValidator
        )

        jwtDecoder.setJwtValidator(withAllValidators)

        return jwtDecoder
    }
}

Expected behavior

Reason for the failure (i.e. token expired) should be returned/thrown as an exception