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)
@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