Skip to content

Commit 81e2fd2

Browse files
committed
Add Type Validation
Closes gh-16672
1 parent 0c7b05a commit 81e2fd2

File tree

4 files changed

+325
-0
lines changed

4 files changed

+325
-0
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright 2002-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.oauth2.jwt;
18+
19+
import java.util.ArrayList;
20+
import java.util.Collection;
21+
import java.util.List;
22+
23+
import org.springframework.security.oauth2.core.OAuth2Error;
24+
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
25+
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
26+
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
27+
import org.springframework.util.Assert;
28+
import org.springframework.util.StringUtils;
29+
30+
/**
31+
* A validator for the {@code typ} header. Specifically for indicating the header values
32+
* that a given {@link JwtDecoder} will support.
33+
*
34+
* @author Josh Cummings
35+
* @since 6.5
36+
*/
37+
public final class JwtTypeValidator implements OAuth2TokenValidator<Jwt> {
38+
39+
private Collection<String> validTypes;
40+
41+
private boolean allowEmpty;
42+
43+
public JwtTypeValidator(Collection<String> validTypes) {
44+
Assert.notEmpty(validTypes, "validTypes cannot be empty");
45+
this.validTypes = new ArrayList<>(validTypes);
46+
}
47+
48+
/**
49+
* Require that the {@code typ} header be {@code JWT} or absent
50+
*/
51+
public static JwtTypeValidator jwt() {
52+
JwtTypeValidator validator = new JwtTypeValidator(List.of("JWT"));
53+
validator.setAllowEmpty(true);
54+
return validator;
55+
}
56+
57+
/**
58+
* Whether to allow the {@code typ} header to be empty. The default value is
59+
* {@code false}
60+
*/
61+
public void setAllowEmpty(boolean allowEmpty) {
62+
this.allowEmpty = allowEmpty;
63+
}
64+
65+
@Override
66+
public OAuth2TokenValidatorResult validate(Jwt token) {
67+
String typ = (String) token.getHeaders().get(JoseHeaderNames.TYP);
68+
if (this.allowEmpty && !StringUtils.hasText(typ)) {
69+
return OAuth2TokenValidatorResult.success();
70+
}
71+
if (this.validTypes.contains(typ)) {
72+
return OAuth2TokenValidatorResult.success();
73+
}
74+
return OAuth2TokenValidatorResult.failure(new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN,
75+
"the given typ value needs to be one of " + this.validTypes,
76+
"https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.9"));
77+
}
78+
79+
}

oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import javax.crypto.SecretKey;
3434

3535
import com.nimbusds.jose.JOSEException;
36+
import com.nimbusds.jose.JOSEObjectType;
3637
import com.nimbusds.jose.JWSAlgorithm;
3738
import com.nimbusds.jose.KeySourceException;
3839
import com.nimbusds.jose.RemoteKeySourceException;
@@ -41,6 +42,8 @@
4142
import com.nimbusds.jose.jwk.source.JWKSetSource;
4243
import com.nimbusds.jose.jwk.source.JWKSource;
4344
import com.nimbusds.jose.jwk.source.JWKSourceBuilder;
45+
import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier;
46+
import com.nimbusds.jose.proc.JOSEObjectTypeVerifier;
4447
import com.nimbusds.jose.proc.JWSKeySelector;
4548
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
4649
import com.nimbusds.jose.proc.SecurityContext;
@@ -265,11 +268,20 @@ public static SecretKeyJwtDecoderBuilder withSecretKey(SecretKey secretKey) {
265268
*/
266269
public static final class JwkSetUriJwtDecoderBuilder {
267270

271+
private static final JOSEObjectTypeVerifier<SecurityContext> JWT_TYPE_VERIFIER = new DefaultJOSEObjectTypeVerifier<>(
272+
JOSEObjectType.JWT, null);
273+
274+
private static final JOSEObjectTypeVerifier<SecurityContext> NO_TYPE_VERIFIER = (header, context) -> {
275+
};
276+
268277
private Function<RestOperations, String> jwkSetUri;
269278

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

282+
private JOSEObjectTypeVerifier<SecurityContext> typeVerifier = new DefaultJOSEObjectTypeVerifier<>(
283+
JOSEObjectType.JWT, null);
284+
273285
private Set<SignatureAlgorithm> signatureAlgorithms = new HashSet<>();
274286

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

310+
/**
311+
* Whether to use Nimbus's typ header verification. This is {@code true} by
312+
* default, however it may change to {@code false} in a future major release.
313+
*
314+
* <p>
315+
* By turning off this feature, {@link NimbusJwtDecoder} expects applications to
316+
* check the {@code typ} header themselves in order to determine what kind of
317+
* validation is needed
318+
* </p>
319+
*
320+
* <p>
321+
* This is done for you when you use {@link JwtValidators} to construct a
322+
* validator.
323+
*
324+
* <p>
325+
* That means that this: <code>
326+
* NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build();
327+
* jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer);
328+
* </code>
329+
*
330+
* <p>
331+
* Is equivalent to this: <code>
332+
* NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer)
333+
* .validateType(false)
334+
* .build();
335+
* jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer);
336+
* </code>
337+
*
338+
* <p>
339+
* The difference is that by setting this to {@code false}, it allows you to
340+
* provide validation by type, like for {@code at+jwt}:
341+
*
342+
* <code>
343+
* NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer)
344+
* .validateType(false)
345+
* .build();
346+
* jwtDecoder.setJwtValidator(new MyAtJwtValidator());
347+
* </code>
348+
* @param shouldValidateTypHeader whether Nimbus should validate the typ header or
349+
* not
350+
* @return a {@link JwkSetUriJwtDecoderBuilder} for further configurations
351+
* @since 6.5
352+
*/
353+
public JwkSetUriJwtDecoderBuilder validateType(boolean shouldValidateTypHeader) {
354+
this.typeVerifier = shouldValidateTypHeader ? JWT_TYPE_VERIFIER : NO_TYPE_VERIFIER;
355+
return this;
356+
}
357+
298358
/**
299359
* Append the given signing
300360
* <a href="https://tools.ietf.org/html/rfc7515#section-4.1.1" target=
@@ -389,6 +449,7 @@ JWKSource<SecurityContext> jwkSource() {
389449
JWTProcessor<SecurityContext> processor() {
390450
JWKSource<SecurityContext> jwkSource = jwkSource();
391451
ConfigurableJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
452+
jwtProcessor.setJWSTypeVerifier(this.typeVerifier);
392453
jwtProcessor.setJWSKeySelector(jwsKeySelector(jwkSource));
393454
// Spring Security validates the claim set independent from Nimbus
394455
jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> {
@@ -481,8 +542,17 @@ public void close() {
481542
*/
482543
public static final class PublicKeyJwtDecoderBuilder {
483544

545+
private static final JOSEObjectTypeVerifier<SecurityContext> JWT_TYPE_VERIFIER = new DefaultJOSEObjectTypeVerifier<>(
546+
JOSEObjectType.JWT, null);
547+
548+
private static final JOSEObjectTypeVerifier<SecurityContext> NO_TYPE_VERIFIER = (header, context) -> {
549+
};
550+
484551
private JWSAlgorithm jwsAlgorithm;
485552

553+
private JOSEObjectTypeVerifier<SecurityContext> typeVerifier = new DefaultJOSEObjectTypeVerifier<>(
554+
JOSEObjectType.JWT, null);
555+
486556
private RSAPublicKey key;
487557

488558
private Consumer<ConfigurableJWTProcessor<SecurityContext>> jwtProcessorCustomizer;
@@ -495,6 +565,54 @@ private PublicKeyJwtDecoderBuilder(RSAPublicKey key) {
495565
};
496566
}
497567

568+
/**
569+
* Whether to use Nimbus's typ header verification. This is {@code true} by
570+
* default, however it may change to {@code false} in a future major release.
571+
*
572+
* <p>
573+
* By turning off this feature, {@link NimbusJwtDecoder} expects applications to
574+
* check the {@code typ} header themselves in order to determine what kind of
575+
* validation is needed
576+
* </p>
577+
*
578+
* <p>
579+
* This is done for you when you use {@link JwtValidators} to construct a
580+
* validator.
581+
*
582+
* <p>
583+
* That means that this: <code>
584+
* NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build();
585+
* jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer);
586+
* </code>
587+
*
588+
* <p>
589+
* Is equivalent to this: <code>
590+
* NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer)
591+
* .validateType(false)
592+
* .build();
593+
* jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer);
594+
* </code>
595+
*
596+
* <p>
597+
* The difference is that by setting this to {@code false}, it allows you to
598+
* provide validation by type, like for {@code at+jwt}:
599+
*
600+
* <code>
601+
* NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer)
602+
* .validateType(false)
603+
* .build();
604+
* jwtDecoder.setJwtValidator(new MyAtJwtValidator());
605+
* </code>
606+
* @param shouldValidateTypHeader whether Nimbus should validate the typ header or
607+
* not
608+
* @return a {@link JwkSetUriJwtDecoderBuilder} for further configurations
609+
* @since 6.5
610+
*/
611+
public PublicKeyJwtDecoderBuilder validateType(boolean shouldValidateTypHeader) {
612+
this.typeVerifier = shouldValidateTypHeader ? JWT_TYPE_VERIFIER : NO_TYPE_VERIFIER;
613+
return this;
614+
}
615+
498616
/**
499617
* Use the given signing
500618
* <a href="https://tools.ietf.org/html/rfc7515#section-4.1.1" target=
@@ -533,6 +651,7 @@ JWTProcessor<SecurityContext> processor() {
533651
+ this.jwsAlgorithm + ". Please indicate one of RS256, RS384, or RS512.");
534652
JWSKeySelector<SecurityContext> jwsKeySelector = new SingleKeyJWSKeySelector<>(this.jwsAlgorithm, this.key);
535653
DefaultJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
654+
jwtProcessor.setJWSTypeVerifier(this.typeVerifier);
536655
jwtProcessor.setJWSKeySelector(jwsKeySelector);
537656
// Spring Security validates the claim set independent from Nimbus
538657
jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> {
@@ -557,10 +676,19 @@ public NimbusJwtDecoder build() {
557676
*/
558677
public static final class SecretKeyJwtDecoderBuilder {
559678

679+
private static final JOSEObjectTypeVerifier<SecurityContext> JWT_TYPE_VERIFIER = new DefaultJOSEObjectTypeVerifier<>(
680+
JOSEObjectType.JWT, null);
681+
682+
private static final JOSEObjectTypeVerifier<SecurityContext> NO_TYPE_VERIFIER = (header, context) -> {
683+
};
684+
560685
private final SecretKey secretKey;
561686

562687
private JWSAlgorithm jwsAlgorithm = JWSAlgorithm.HS256;
563688

689+
private JOSEObjectTypeVerifier<SecurityContext> typeVerifier = new DefaultJOSEObjectTypeVerifier<>(
690+
JOSEObjectType.JWT, null);
691+
564692
private Consumer<ConfigurableJWTProcessor<SecurityContext>> jwtProcessorCustomizer;
565693

566694
private SecretKeyJwtDecoderBuilder(SecretKey secretKey) {
@@ -570,6 +698,54 @@ private SecretKeyJwtDecoderBuilder(SecretKey secretKey) {
570698
};
571699
}
572700

701+
/**
702+
* Whether to use Nimbus's typ header verification. This is {@code true} by
703+
* default, however it may change to {@code false} in a future major release.
704+
*
705+
* <p>
706+
* By turning off this feature, {@link NimbusJwtDecoder} expects applications to
707+
* check the {@code typ} header themselves in order to determine what kind of
708+
* validation is needed
709+
* </p>
710+
*
711+
* <p>
712+
* This is done for you when you use {@link JwtValidators} to construct a
713+
* validator.
714+
*
715+
* <p>
716+
* That means that this: <code>
717+
* NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build();
718+
* jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer);
719+
* </code>
720+
*
721+
* <p>
722+
* Is equivalent to this: <code>
723+
* NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer)
724+
* .validateType(false)
725+
* .build();
726+
* jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer);
727+
* </code>
728+
*
729+
* <p>
730+
* The difference is that by setting this to {@code false}, it allows you to
731+
* provide validation by type, like for {@code at+jwt}:
732+
*
733+
* <code>
734+
* NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer)
735+
* .validateType(false)
736+
* .build();
737+
* jwtDecoder.setJwtValidator(new MyAtJwtValidator());
738+
* </code>
739+
* @param shouldValidateTypHeader whether Nimbus should validate the typ header or
740+
* not
741+
* @return a {@link JwkSetUriJwtDecoderBuilder} for further configurations
742+
* @since 6.5
743+
*/
744+
public SecretKeyJwtDecoderBuilder validateType(boolean shouldValidateTypHeader) {
745+
this.typeVerifier = shouldValidateTypHeader ? JWT_TYPE_VERIFIER : NO_TYPE_VERIFIER;
746+
return this;
747+
}
748+
573749
/**
574750
* Use the given
575751
* <a href="https://tools.ietf.org/html/rfc7515#section-4.1.1" target=
@@ -615,6 +791,7 @@ JWTProcessor<SecurityContext> processor() {
615791
this.secretKey);
616792
DefaultJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
617793
jwtProcessor.setJWSKeySelector(jwsKeySelector);
794+
jwtProcessor.setJWSTypeVerifier(this.typeVerifier);
618795
// Spring Security validates the claim set independent from Nimbus
619796
jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> {
620797
});
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2002-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.oauth2.jwt;
18+
19+
import org.junit.jupiter.api.Test;
20+
21+
import static org.assertj.core.api.Assertions.assertThat;
22+
23+
class JwtTypeValidatorTests {
24+
25+
@Test
26+
void constructorWhenJwtThenRequiresJwtOrEmpty() {
27+
Jwt.Builder jwt = TestJwts.jwt();
28+
JwtTypeValidator validator = JwtTypeValidator.jwt();
29+
assertThat(validator.validate(jwt.build()).hasErrors()).isFalse();
30+
jwt.header(JoseHeaderNames.TYP, "JWT");
31+
assertThat(validator.validate(jwt.build()).hasErrors()).isFalse();
32+
jwt.header(JoseHeaderNames.TYP, "at+jwt");
33+
assertThat(validator.validate(jwt.build()).hasErrors()).isTrue();
34+
}
35+
36+
@Test
37+
void constructorWhenCustomThenEnforces() {
38+
Jwt.Builder jwt = TestJwts.jwt();
39+
JwtTypeValidator validator = new JwtTypeValidator("JOSE");
40+
assertThat(validator.validate(jwt.build()).hasErrors()).isTrue();
41+
jwt.header(JoseHeaderNames.TYP, "JWT");
42+
assertThat(validator.validate(jwt.build()).hasErrors()).isTrue();
43+
jwt.header(JoseHeaderNames.TYP, "JOSE");
44+
assertThat(validator.validate(jwt.build()).hasErrors()).isFalse();
45+
}
46+
47+
}

0 commit comments

Comments
 (0)