Skip to content

Commit

Permalink
Add Type Validation
Browse files Browse the repository at this point in the history
Closes gh-16672
  • Loading branch information
jzheaux committed Feb 27, 2025
1 parent 0c7b05a commit 81e2fd2
Show file tree
Hide file tree
Showing 4 changed files with 325 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.security.oauth2.jwt;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
* A validator for the {@code typ} header. Specifically for indicating the header values
* that a given {@link JwtDecoder} will support.
*
* @author Josh Cummings
* @since 6.5
*/
public final class JwtTypeValidator implements OAuth2TokenValidator<Jwt> {

private Collection<String> validTypes;

private boolean allowEmpty;

public JwtTypeValidator(Collection<String> validTypes) {
Assert.notEmpty(validTypes, "validTypes cannot be empty");
this.validTypes = new ArrayList<>(validTypes);
}

/**
* Require that the {@code typ} header be {@code JWT} or absent
*/
public static JwtTypeValidator jwt() {
JwtTypeValidator validator = new JwtTypeValidator(List.of("JWT"));
validator.setAllowEmpty(true);
return validator;
}

/**
* Whether to allow the {@code typ} header to be empty. The default value is
* {@code false}
*/
public void setAllowEmpty(boolean allowEmpty) {
this.allowEmpty = allowEmpty;
}

@Override
public OAuth2TokenValidatorResult validate(Jwt token) {
String typ = (String) token.getHeaders().get(JoseHeaderNames.TYP);
if (this.allowEmpty && !StringUtils.hasText(typ)) {
return OAuth2TokenValidatorResult.success();
}
if (this.validTypes.contains(typ)) {
return OAuth2TokenValidatorResult.success();
}
return OAuth2TokenValidatorResult.failure(new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN,
"the given typ value needs to be one of " + this.validTypes,
"https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.9"));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import javax.crypto.SecretKey;

import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JOSEObjectType;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.KeySourceException;
import com.nimbusds.jose.RemoteKeySourceException;
Expand All @@ -41,6 +42,8 @@
import com.nimbusds.jose.jwk.source.JWKSetSource;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.jwk.source.JWKSourceBuilder;
import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier;
import com.nimbusds.jose.proc.JOSEObjectTypeVerifier;
import com.nimbusds.jose.proc.JWSKeySelector;
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
import com.nimbusds.jose.proc.SecurityContext;
Expand Down Expand Up @@ -265,11 +268,20 @@ public static SecretKeyJwtDecoderBuilder withSecretKey(SecretKey secretKey) {
*/
public static final class JwkSetUriJwtDecoderBuilder {

private static final JOSEObjectTypeVerifier<SecurityContext> JWT_TYPE_VERIFIER = new DefaultJOSEObjectTypeVerifier<>(
JOSEObjectType.JWT, null);

private static final JOSEObjectTypeVerifier<SecurityContext> NO_TYPE_VERIFIER = (header, context) -> {
};

private Function<RestOperations, String> jwkSetUri;

private Function<JWKSource<SecurityContext>, Set<JWSAlgorithm>> defaultAlgorithms = (source) -> Set
.of(JWSAlgorithm.RS256);

private JOSEObjectTypeVerifier<SecurityContext> typeVerifier = new DefaultJOSEObjectTypeVerifier<>(
JOSEObjectType.JWT, null);

private Set<SignatureAlgorithm> signatureAlgorithms = new HashSet<>();

private RestOperations restOperations = new RestTemplate();
Expand All @@ -295,6 +307,54 @@ private JwkSetUriJwtDecoderBuilder(Function<RestOperations, String> jwkSetUri,
};
}

/**
* Whether to use Nimbus's typ header verification. This is {@code true} by
* default, however it may change to {@code false} in a future major release.
*
* <p>
* By turning off this feature, {@link NimbusJwtDecoder} expects applications to
* check the {@code typ} header themselves in order to determine what kind of
* validation is needed
* </p>
*
* <p>
* This is done for you when you use {@link JwtValidators} to construct a
* validator.
*
* <p>
* That means that this: <code>
* NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build();
* jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer);
* </code>
*
* <p>
* Is equivalent to this: <code>
* NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer)
* .validateType(false)
* .build();
* jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer);
* </code>
*
* <p>
* The difference is that by setting this to {@code false}, it allows you to
* provide validation by type, like for {@code at+jwt}:
*
* <code>
* NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer)
* .validateType(false)
* .build();
* jwtDecoder.setJwtValidator(new MyAtJwtValidator());
* </code>
* @param shouldValidateTypHeader whether Nimbus should validate the typ header or
* not
* @return a {@link JwkSetUriJwtDecoderBuilder} for further configurations
* @since 6.5
*/
public JwkSetUriJwtDecoderBuilder validateType(boolean shouldValidateTypHeader) {
this.typeVerifier = shouldValidateTypHeader ? JWT_TYPE_VERIFIER : NO_TYPE_VERIFIER;
return this;
}

/**
* Append the given signing
* <a href="https://tools.ietf.org/html/rfc7515#section-4.1.1" target=
Expand Down Expand Up @@ -389,6 +449,7 @@ JWKSource<SecurityContext> jwkSource() {
JWTProcessor<SecurityContext> processor() {
JWKSource<SecurityContext> jwkSource = jwkSource();
ConfigurableJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
jwtProcessor.setJWSTypeVerifier(this.typeVerifier);
jwtProcessor.setJWSKeySelector(jwsKeySelector(jwkSource));
// Spring Security validates the claim set independent from Nimbus
jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> {
Expand Down Expand Up @@ -481,8 +542,17 @@ public void close() {
*/
public static final class PublicKeyJwtDecoderBuilder {

private static final JOSEObjectTypeVerifier<SecurityContext> JWT_TYPE_VERIFIER = new DefaultJOSEObjectTypeVerifier<>(
JOSEObjectType.JWT, null);

private static final JOSEObjectTypeVerifier<SecurityContext> NO_TYPE_VERIFIER = (header, context) -> {
};

private JWSAlgorithm jwsAlgorithm;

private JOSEObjectTypeVerifier<SecurityContext> typeVerifier = new DefaultJOSEObjectTypeVerifier<>(
JOSEObjectType.JWT, null);

private RSAPublicKey key;

private Consumer<ConfigurableJWTProcessor<SecurityContext>> jwtProcessorCustomizer;
Expand All @@ -495,6 +565,54 @@ private PublicKeyJwtDecoderBuilder(RSAPublicKey key) {
};
}

/**
* Whether to use Nimbus's typ header verification. This is {@code true} by
* default, however it may change to {@code false} in a future major release.
*
* <p>
* By turning off this feature, {@link NimbusJwtDecoder} expects applications to
* check the {@code typ} header themselves in order to determine what kind of
* validation is needed
* </p>
*
* <p>
* This is done for you when you use {@link JwtValidators} to construct a
* validator.
*
* <p>
* That means that this: <code>
* NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build();
* jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer);
* </code>
*
* <p>
* Is equivalent to this: <code>
* NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer)
* .validateType(false)
* .build();
* jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer);
* </code>
*
* <p>
* The difference is that by setting this to {@code false}, it allows you to
* provide validation by type, like for {@code at+jwt}:
*
* <code>
* NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer)
* .validateType(false)
* .build();
* jwtDecoder.setJwtValidator(new MyAtJwtValidator());
* </code>
* @param shouldValidateTypHeader whether Nimbus should validate the typ header or
* not
* @return a {@link JwkSetUriJwtDecoderBuilder} for further configurations
* @since 6.5
*/
public PublicKeyJwtDecoderBuilder validateType(boolean shouldValidateTypHeader) {
this.typeVerifier = shouldValidateTypHeader ? JWT_TYPE_VERIFIER : NO_TYPE_VERIFIER;
return this;
}

/**
* Use the given signing
* <a href="https://tools.ietf.org/html/rfc7515#section-4.1.1" target=
Expand Down Expand Up @@ -533,6 +651,7 @@ JWTProcessor<SecurityContext> processor() {
+ this.jwsAlgorithm + ". Please indicate one of RS256, RS384, or RS512.");
JWSKeySelector<SecurityContext> jwsKeySelector = new SingleKeyJWSKeySelector<>(this.jwsAlgorithm, this.key);
DefaultJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
jwtProcessor.setJWSTypeVerifier(this.typeVerifier);
jwtProcessor.setJWSKeySelector(jwsKeySelector);
// Spring Security validates the claim set independent from Nimbus
jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> {
Expand All @@ -557,10 +676,19 @@ public NimbusJwtDecoder build() {
*/
public static final class SecretKeyJwtDecoderBuilder {

private static final JOSEObjectTypeVerifier<SecurityContext> JWT_TYPE_VERIFIER = new DefaultJOSEObjectTypeVerifier<>(
JOSEObjectType.JWT, null);

private static final JOSEObjectTypeVerifier<SecurityContext> NO_TYPE_VERIFIER = (header, context) -> {
};

private final SecretKey secretKey;

private JWSAlgorithm jwsAlgorithm = JWSAlgorithm.HS256;

private JOSEObjectTypeVerifier<SecurityContext> typeVerifier = new DefaultJOSEObjectTypeVerifier<>(
JOSEObjectType.JWT, null);

private Consumer<ConfigurableJWTProcessor<SecurityContext>> jwtProcessorCustomizer;

private SecretKeyJwtDecoderBuilder(SecretKey secretKey) {
Expand All @@ -570,6 +698,54 @@ private SecretKeyJwtDecoderBuilder(SecretKey secretKey) {
};
}

/**
* Whether to use Nimbus's typ header verification. This is {@code true} by
* default, however it may change to {@code false} in a future major release.
*
* <p>
* By turning off this feature, {@link NimbusJwtDecoder} expects applications to
* check the {@code typ} header themselves in order to determine what kind of
* validation is needed
* </p>
*
* <p>
* This is done for you when you use {@link JwtValidators} to construct a
* validator.
*
* <p>
* That means that this: <code>
* NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build();
* jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer);
* </code>
*
* <p>
* Is equivalent to this: <code>
* NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer)
* .validateType(false)
* .build();
* jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer);
* </code>
*
* <p>
* The difference is that by setting this to {@code false}, it allows you to
* provide validation by type, like for {@code at+jwt}:
*
* <code>
* NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer)
* .validateType(false)
* .build();
* jwtDecoder.setJwtValidator(new MyAtJwtValidator());
* </code>
* @param shouldValidateTypHeader whether Nimbus should validate the typ header or
* not
* @return a {@link JwkSetUriJwtDecoderBuilder} for further configurations
* @since 6.5
*/
public SecretKeyJwtDecoderBuilder validateType(boolean shouldValidateTypHeader) {
this.typeVerifier = shouldValidateTypHeader ? JWT_TYPE_VERIFIER : NO_TYPE_VERIFIER;
return this;
}

/**
* Use the given
* <a href="https://tools.ietf.org/html/rfc7515#section-4.1.1" target=
Expand Down Expand Up @@ -615,6 +791,7 @@ JWTProcessor<SecurityContext> processor() {
this.secretKey);
DefaultJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
jwtProcessor.setJWSKeySelector(jwsKeySelector);
jwtProcessor.setJWSTypeVerifier(this.typeVerifier);
// Spring Security validates the claim set independent from Nimbus
jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> {
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.security.oauth2.jwt;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class JwtTypeValidatorTests {

@Test
void constructorWhenJwtThenRequiresJwtOrEmpty() {
Jwt.Builder jwt = TestJwts.jwt();
JwtTypeValidator validator = JwtTypeValidator.jwt();
assertThat(validator.validate(jwt.build()).hasErrors()).isFalse();
jwt.header(JoseHeaderNames.TYP, "JWT");
assertThat(validator.validate(jwt.build()).hasErrors()).isFalse();
jwt.header(JoseHeaderNames.TYP, "at+jwt");
assertThat(validator.validate(jwt.build()).hasErrors()).isTrue();
}

@Test
void constructorWhenCustomThenEnforces() {
Jwt.Builder jwt = TestJwts.jwt();
JwtTypeValidator validator = new JwtTypeValidator("JOSE");
assertThat(validator.validate(jwt.build()).hasErrors()).isTrue();
jwt.header(JoseHeaderNames.TYP, "JWT");
assertThat(validator.validate(jwt.build()).hasErrors()).isTrue();
jwt.header(JoseHeaderNames.TYP, "JOSE");
assertThat(validator.validate(jwt.build()).hasErrors()).isFalse();
}

}
Loading

0 comments on commit 81e2fd2

Please sign in to comment.