mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-06-22 20:12:14 +00:00
Add Type Validation
Closes gh-16672
This commit is contained in:
parent
0c7b05a0e3
commit
81e2fd2fe8
@ -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"));
|
||||
}
|
||||
|
||||
}
|
@ -33,6 +33,7 @@ import java.util.function.Function;
|
||||
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;
|
||||
@ -41,6 +42,8 @@ import com.nimbusds.jose.jwk.source.JWKSetCacheRefreshEvaluator;
|
||||
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;
|
||||
@ -265,11 +268,20 @@ public final class NimbusJwtDecoder implements JwtDecoder {
|
||||
*/
|
||||
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();
|
||||
@ -295,6 +307,54 @@ public final class NimbusJwtDecoder implements JwtDecoder {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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=
|
||||
@ -389,6 +449,7 @@ public final class NimbusJwtDecoder implements JwtDecoder {
|
||||
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) -> {
|
||||
@ -481,8 +542,17 @@ public final class NimbusJwtDecoder implements JwtDecoder {
|
||||
*/
|
||||
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;
|
||||
@ -495,6 +565,54 @@ public final class NimbusJwtDecoder implements JwtDecoder {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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=
|
||||
@ -533,6 +651,7 @@ public final class NimbusJwtDecoder implements JwtDecoder {
|
||||
+ 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) -> {
|
||||
@ -557,10 +676,19 @@ public final class NimbusJwtDecoder implements JwtDecoder {
|
||||
*/
|
||||
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) {
|
||||
@ -570,6 +698,54 @@ public final class NimbusJwtDecoder implements JwtDecoder {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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=
|
||||
@ -615,6 +791,7 @@ public final class NimbusJwtDecoder implements JwtDecoder {
|
||||
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) -> {
|
||||
});
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -832,6 +832,28 @@ public class NimbusJwtDecoderTests {
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeWhenPublicKeyValidateTypeFalseThenSkipsNimbusTypeValidation() throws Exception {
|
||||
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey(TestKeys.DEFAULT_PUBLIC_KEY)
|
||||
.validateType(false)
|
||||
.build();
|
||||
RSAPrivateKey privateKey = TestKeys.DEFAULT_PRIVATE_KEY;
|
||||
SignedJWT jwt = signedJwt(privateKey,
|
||||
new JWSHeader.Builder(JWSAlgorithm.RS256).type(JOSEObjectType.JOSE).build(),
|
||||
new JWTClaimsSet.Builder().subject("subject").build());
|
||||
jwtDecoder.decode(jwt.serialize());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeWhenSecretKeyValidateTypeFalseThenSkipsNimbusTypeValidation() throws Exception {
|
||||
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withSecretKey(TestKeys.DEFAULT_SECRET_KEY)
|
||||
.validateType(false)
|
||||
.build();
|
||||
SignedJWT jwt = signedJwt(TestKeys.DEFAULT_SECRET_KEY, MacAlgorithm.HS256,
|
||||
new JWTClaimsSet.Builder().subject("subject").build());
|
||||
jwtDecoder.decode(jwt.serialize());
|
||||
}
|
||||
|
||||
private RSAPublicKey key() throws InvalidKeySpecException {
|
||||
byte[] decoded = Base64.getDecoder().decode(VERIFY_KEY.getBytes());
|
||||
EncodedKeySpec spec = new X509EncodedKeySpec(decoded);
|
||||
|
Loading…
x
Reference in New Issue
Block a user