Change Type Validation Default

NimbusJwtDecoder and NimbusReactiveJwtDecoder now use
Spring Security's JwtTypeValidator by default instead
of Nimbus's type validator.

Closes gh-17181
This commit is contained in:
Josh Cummings 2025-05-27 13:13:54 -06:00
parent 37a814bc29
commit 6d3b54df21
No known key found for this signature in database
GPG Key ID: 869B37A20E876129
9 changed files with 111 additions and 37 deletions

View File

@ -18,11 +18,6 @@ package org.springframework.security.config.annotation.web.configurers.oauth2.cl
import java.util.function.Function;
import com.nimbusds.jose.JOSEObjectType;
import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier;
import com.nimbusds.jose.proc.JOSEObjectTypeVerifier;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
@ -38,6 +33,7 @@ import org.springframework.security.oauth2.jwt.BadJwtException;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
import org.springframework.security.oauth2.jwt.JwtTypeValidator;
import org.springframework.security.oauth2.jwt.JwtValidators;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.util.Assert;
@ -67,8 +63,10 @@ final class OidcBackChannelLogoutAuthenticationProvider implements Authenticatio
* Construct an {@link OidcBackChannelLogoutAuthenticationProvider}
*/
OidcBackChannelLogoutAuthenticationProvider() {
JwtTypeValidator type = new JwtTypeValidator("JWT", "logout+jwt");
type.setAllowEmpty(true);
Function<ClientRegistration, OAuth2TokenValidator<Jwt>> jwtValidator = (clientRegistration) -> JwtValidators
.createDefaultWithValidators(new OidcBackChannelLogoutTokenValidator(clientRegistration));
.createDefaultWithValidators(type, new OidcBackChannelLogoutTokenValidator(clientRegistration));
this.logoutTokenDecoderFactory = (clientRegistration) -> {
String jwkSetUri = clientRegistration.getProviderDetails().getJwkSetUri();
if (!StringUtils.hasText(jwkSetUri)) {
@ -79,11 +77,7 @@ final class OidcBackChannelLogoutAuthenticationProvider implements Authenticatio
null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
JOSEObjectTypeVerifier<SecurityContext> typeVerifier = new DefaultJOSEObjectTypeVerifier<>(null,
JOSEObjectType.JWT, new JOSEObjectType("logout+jwt"));
NimbusJwtDecoder decoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri)
.jwtProcessorCustomizer((processor) -> processor.setJWSTypeVerifier(typeVerifier))
.build();
NimbusJwtDecoder decoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
decoder.setJwtValidator(jwtValidator.apply(clientRegistration));
decoder.setClaimSetConverter(OidcIdTokenDecoderFactory.createDefaultClaimTypeConverter());
return decoder;

View File

@ -18,10 +18,6 @@ package org.springframework.security.config.web.server;
import java.util.function.Function;
import com.nimbusds.jose.JOSEObjectType;
import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier;
import com.nimbusds.jose.proc.JOSEObjectTypeVerifier;
import com.nimbusds.jose.proc.JWKSecurityContext;
import reactor.core.publisher.Mono;
import org.springframework.security.authentication.AuthenticationProvider;
@ -41,6 +37,7 @@ import org.springframework.security.oauth2.jwt.BadJwtException;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
import org.springframework.security.oauth2.jwt.JwtTypeValidator;
import org.springframework.security.oauth2.jwt.JwtValidators;
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
@ -72,8 +69,10 @@ final class OidcBackChannelLogoutReactiveAuthenticationManager implements Reacti
* Construct an {@link OidcBackChannelLogoutReactiveAuthenticationManager}
*/
OidcBackChannelLogoutReactiveAuthenticationManager() {
JwtTypeValidator type = new JwtTypeValidator("JWT", "logout+jwt");
type.setAllowEmpty(true);
Function<ClientRegistration, OAuth2TokenValidator<Jwt>> jwtValidator = (clientRegistration) -> JwtValidators
.createDefaultWithValidators(new OidcBackChannelLogoutTokenValidator(clientRegistration));
.createDefaultWithValidators(type, new OidcBackChannelLogoutTokenValidator(clientRegistration));
this.logoutTokenDecoderFactory = (clientRegistration) -> {
String jwkSetUri = clientRegistration.getProviderDetails().getJwkSetUri();
if (!StringUtils.hasText(jwkSetUri)) {
@ -84,11 +83,7 @@ final class OidcBackChannelLogoutReactiveAuthenticationManager implements Reacti
null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
JOSEObjectTypeVerifier<JWKSecurityContext> typeVerifier = new DefaultJOSEObjectTypeVerifier<>(null,
JOSEObjectType.JWT, new JOSEObjectType("logout+jwt"));
NimbusReactiveJwtDecoder decoder = NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri)
.jwtProcessorCustomizer((processor) -> processor.setJWSTypeVerifier(typeVerifier))
.build();
NimbusReactiveJwtDecoder decoder = NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).build();
decoder.setJwtValidator(jwtValidator.apply(clientRegistration));
decoder.setClaimSetConverter(
new ClaimTypeConverter(OidcIdTokenDecoderFactory.createDefaultClaimTypeConverters()));

View File

@ -1,3 +1,82 @@
= Reactive
If you have already performed the xref:migration/index.adoc[initial migration steps] for your Reactive application, you're now ready to perform steps specific to Reactive applications.
== Validate `typ` Header with `JwtTypeValidator`
If when following the 6.5 preparatory steps you set `validateTypes` to `false`, you can now remove it.
You can also remove explicitly adding `JwtTypeValidator` to the list of defaults.
For example, change this:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
JwtDecoder jwtDecoder() {
NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(location)
.validateTypes(false) <1>
// ... your remaining configuration
.build();
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithValidators(
new JwtIssuerValidator(location), JwtTypeValidator.jwt())); <2>
return jwtDecoder;
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun jwtDecoder(): JwtDecoder {
val jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(location)
.validateTypes(false) <1>
// ... your remaining configuration
.build()
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithValidators(
JwtIssuerValidator(location), JwtTypeValidator.jwt())) <2>
return jwtDecoder
}
----
======
<1> - Switch off Nimbus verifying the `typ`
<2> - Add the default `typ` validator
to this:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
NimbusReactiveJwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(location)
// ... your remaining configuration <1>
.build();
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(location)); <2>
return jwtDecoder;
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun jwtDecoder(): NimbusReactiveJwtDecoder {
val jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(location)
// ... your remaining configuration
.build()
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(location)) <2>
return jwtDecoder
}
----
======
<1> - `validateTypes` now defaults to `false`
<2> - `JwtTypeValidator#jwt` is added by all `createDefaultXXX` methods

View File

@ -75,8 +75,8 @@ public final class JwtValidators {
* supplied
*/
public static OAuth2TokenValidator<Jwt> createDefault() {
return new DelegatingOAuth2TokenValidator<>(
Arrays.asList(new JwtTimestampValidator(), new X509CertificateThumbprintValidator(
return new DelegatingOAuth2TokenValidator<>(Arrays.asList(JwtTypeValidator.jwt(), new JwtTimestampValidator(),
new X509CertificateThumbprintValidator(
X509CertificateThumbprintValidator.DEFAULT_X509_CERTIFICATE_SUPPLIER)));
}
@ -104,6 +104,10 @@ public final class JwtValidators {
if (jwtTimestampValidator == null) {
tokenValidators.add(0, new JwtTimestampValidator());
}
JwtTypeValidator typeValidator = CollectionUtils.findValueOfType(tokenValidators, JwtTypeValidator.class);
if (typeValidator == null) {
tokenValidators.add(0, JwtTypeValidator.jwt());
}
return new DelegatingOAuth2TokenValidator<>(tokenValidators);
}

View File

@ -279,7 +279,7 @@ public final class NimbusJwtDecoder implements JwtDecoder {
private Function<JWKSource<SecurityContext>, Set<JWSAlgorithm>> defaultAlgorithms = (source) -> Set
.of(JWSAlgorithm.RS256);
private JOSEObjectTypeVerifier<SecurityContext> typeVerifier = JWT_TYPE_VERIFIER;
private JOSEObjectTypeVerifier<SecurityContext> typeVerifier = NO_TYPE_VERIFIER;
private final Set<SignatureAlgorithm> signatureAlgorithms = new HashSet<>();
@ -548,7 +548,7 @@ public final class NimbusJwtDecoder implements JwtDecoder {
private JWSAlgorithm jwsAlgorithm;
private JOSEObjectTypeVerifier<SecurityContext> typeVerifier = JWT_TYPE_VERIFIER;
private JOSEObjectTypeVerifier<SecurityContext> typeVerifier = NO_TYPE_VERIFIER;
private final RSAPublicKey key;
@ -680,7 +680,7 @@ public final class NimbusJwtDecoder implements JwtDecoder {
private JWSAlgorithm jwsAlgorithm = JWSAlgorithm.HS256;
private JOSEObjectTypeVerifier<SecurityContext> typeVerifier = JWT_TYPE_VERIFIER;
private JOSEObjectTypeVerifier<SecurityContext> typeVerifier = NO_TYPE_VERIFIER;
private Consumer<ConfigurableJWTProcessor<SecurityContext>> jwtProcessorCustomizer;

View File

@ -324,7 +324,7 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
private Function<ReactiveRemoteJWKSource, Mono<Set<JWSAlgorithm>>> defaultAlgorithms = (source) -> Mono
.just(Set.of(JWSAlgorithm.RS256));
private JOSEObjectTypeVerifier<JWKSecurityContext> typeVerifier = JWT_TYPE_VERIFIER;
private JOSEObjectTypeVerifier<JWKSecurityContext> typeVerifier = NO_TYPE_VERIFIER;
private Set<SignatureAlgorithm> signatureAlgorithms = new HashSet<>();
@ -547,7 +547,7 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
private JWSAlgorithm jwsAlgorithm;
private JOSEObjectTypeVerifier<SecurityContext> typeVerifier = JWT_TYPE_VERIFIER;
private JOSEObjectTypeVerifier<SecurityContext> typeVerifier = NO_TYPE_VERIFIER;
private Consumer<ConfigurableJWTProcessor<SecurityContext>> jwtProcessorCustomizer;
@ -682,7 +682,7 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
private JWSAlgorithm jwsAlgorithm = JWSAlgorithm.HS256;
private JOSEObjectTypeVerifier<SecurityContext> typeVerifier = JWT_TYPE_VERIFIER;
private JOSEObjectTypeVerifier<SecurityContext> typeVerifier = NO_TYPE_VERIFIER;
private Consumer<ConfigurableJWTProcessor<SecurityContext>> jwtProcessorCustomizer;
@ -814,7 +814,7 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
private JWSAlgorithm jwsAlgorithm = JWSAlgorithm.RS256;
private JOSEObjectTypeVerifier<JWKSecurityContext> typeVerifier = JWT_TYPE_VERIFIER;
private JOSEObjectTypeVerifier<JWKSecurityContext> typeVerifier = NO_TYPE_VERIFIER;
private Consumer<ConfigurableJWTProcessor<JWKSecurityContext>> jwtProcessorCustomizer;

View File

@ -62,7 +62,8 @@ public class JwtValidatorsTests {
assertThat(containsByType(validator, JwtTimestampValidator.class)).isTrue();
assertThat(containsByType(validator, X509CertificateThumbprintValidator.class)).isTrue();
assertThat(Objects.requireNonNull(tokenValidators).size()).isEqualTo(2);
assertThat(containsByType(validator, JwtTypeValidator.class)).isTrue();
assertThat(Objects.requireNonNull(tokenValidators).size()).isEqualTo(3);
}
@Test

View File

@ -458,10 +458,8 @@ public class NimbusJwtDecoderTests {
// @formatter:off
NimbusJwtDecoder decoder = NimbusJwtDecoder.withPublicKey(publicKey)
.signatureAlgorithm(SignatureAlgorithm.RS256)
.jwtProcessorCustomizer((p) -> p
.setJWSTypeVerifier(new DefaultJOSEObjectTypeVerifier<>(new JOSEObjectType("JWS")))
)
.build();
decoder.setJwtValidator(JwtValidators.createDefaultWithValidators(new JwtTypeValidator("JWS")));
// @formatter:on
assertThat(decoder.decode(signedJwt.serialize()).hasClaim(JwtClaimNames.EXP)).isNotNull();
}
@ -575,10 +573,8 @@ public class NimbusJwtDecoderTests {
// @formatter:off
NimbusJwtDecoder decoder = NimbusJwtDecoder.withSecretKey(secretKey)
.macAlgorithm(MacAlgorithm.HS256)
.jwtProcessorCustomizer((p) -> p
.setJWSTypeVerifier(new DefaultJOSEObjectTypeVerifier<>(new JOSEObjectType("JWS")))
)
.build();
decoder.setJwtValidator(JwtValidators.createDefaultWithValidators(new JwtTypeValidator("JWS")));
// @formatter:on
assertThat(decoder.decode(signedJwt.serialize()).hasClaim(JwtClaimNames.EXP)).isNotNull();
}
@ -837,6 +833,7 @@ public class NimbusJwtDecoderTests {
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey(TestKeys.DEFAULT_PUBLIC_KEY)
.validateType(false)
.build();
jwtDecoder.setJwtValidator((jwt) -> OAuth2TokenValidatorResult.success());
RSAPrivateKey privateKey = TestKeys.DEFAULT_PRIVATE_KEY;
SignedJWT jwt = signedJwt(privateKey,
new JWSHeader.Builder(JWSAlgorithm.RS256).type(JOSEObjectType.JOSE).build(),
@ -849,6 +846,7 @@ public class NimbusJwtDecoderTests {
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withSecretKey(TestKeys.DEFAULT_SECRET_KEY)
.validateType(false)
.build();
jwtDecoder.setJwtValidator((jwt) -> OAuth2TokenValidatorResult.success());
SignedJWT jwt = signedJwt(TestKeys.DEFAULT_SECRET_KEY,
new JWSHeader.Builder(JWSAlgorithm.HS256).type(JOSEObjectType.JOSE).build(),
new JWTClaimsSet.Builder().subject("subject").build());

View File

@ -667,6 +667,7 @@ public class NimbusReactiveJwtDecoderTests {
NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withPublicKey(TestKeys.DEFAULT_PUBLIC_KEY)
.validateType(false)
.build();
jwtDecoder.setJwtValidator((jwt) -> OAuth2TokenValidatorResult.success());
RSAPrivateKey privateKey = TestKeys.DEFAULT_PRIVATE_KEY;
SignedJWT jwt = signedJwt(privateKey,
new JWSHeader.Builder(JWSAlgorithm.RS256).type(JOSEObjectType.JOSE).build(),
@ -679,6 +680,7 @@ public class NimbusReactiveJwtDecoderTests {
NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withSecretKey(TestKeys.DEFAULT_SECRET_KEY)
.validateType(false)
.build();
jwtDecoder.setJwtValidator((jwt) -> OAuth2TokenValidatorResult.success());
SignedJWT jwt = signedJwt(TestKeys.DEFAULT_SECRET_KEY,
new JWSHeader.Builder(JWSAlgorithm.HS256).type(JOSEObjectType.JOSE).build(),
new JWTClaimsSet.Builder().subject("subject").build());
@ -693,6 +695,7 @@ public class NimbusReactiveJwtDecoderTests {
NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withJwkSource((jwt) -> Flux.just(jwk))
.validateType(false)
.build();
jwtDecoder.setJwtValidator((jwt) -> OAuth2TokenValidatorResult.success());
SignedJWT jwt = signedJwt(TestKeys.DEFAULT_PRIVATE_KEY,
new JWSHeader.Builder(JWSAlgorithm.RS256).type(JOSEObjectType.JOSE).build(),
new JWTClaimsSet.Builder().subject("subject").build());