浏览代码

Add RFC 9068 Support

Closes gh-13185
Josh Cummings 5 月之前
父节点
当前提交
ab43a660b9

+ 172 - 0
docs/modules/ROOT/pages/migration/oauth2.adoc

@@ -0,0 +1,172 @@
+= OAuth 2.0 Changes
+
+== Validate `typ` Header with `JwtTypeValidator`
+
+`NimbusJwtDecoder` in Spring Security 7 will move `typ` header validation to `JwtTypeValidator` intsead of relying on Nimbus.
+This brings it in line with `NimbusJwtDecoder` validating claims instead of relying on Nimbus to validate them.
+
+If you are changing Nimbus's default type validation in a `jwtProcessorCustomizer` method, then you should move that to `JwtTypeValidator` or an implementation of `OAuth2TokenValidator` of your own.
+
+To check if you are prepared for this change, add the default `JwtTypeValidator` to your list of validators, as this will be included by default in 7:
+
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+@Bean
+JwtDecoder jwtDecoder() {
+	NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.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 = NimbusJwtDecoder.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` (this will be off by default in 7)
+<2> - Add the default `typ` validator (this will be included by default in 7)
+
+Note the default value verifies that the `typ` value either be `JWT` or not present, which is the same as the Nimbus default.
+It is also aligned with https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.9[RFC 7515] which states that `typ` is optional.
+
+
+=== I'm Using A `DefaultJOSEObjectTypeVerifier`
+
+If you have something like the following in your configuration:
+
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+@Bean
+JwtDecoder jwtDecoder() {
+	NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(location)
+        .jwtProcessorCustomizer((c) -> c
+            .setJWSTypeVerifier(new DefaultJOSEObjectTypeVerifier<>("JOSE"))
+        )
+        .build();
+	return jwtDecoder;
+}
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+@Bean
+fun jwtDecoder(): JwtDecoder {
+    val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(location)
+        .jwtProcessorCustomizer {
+            it.setJWSTypeVerifier(DefaultJOSEObjectTypeVerifier("JOSE"))
+        }
+        .build()
+    return jwtDecoder
+}
+----
+======
+
+Then change this to:
+
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+@Bean
+JwtDecoder jwtDecoder() {
+	NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(location)
+        .validateTypes(false)
+        .build();
+	jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithValidators(
+		new JwtIssuerValidator(location), new JwtTypeValidator("JOSE")));
+	return jwtDecoder;
+}
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+@Bean
+fun jwtDecoder(): JwtDecoder {
+    val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(location)
+        .validateTypes(false)
+        .build()
+	jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithValidators(
+		JwtIssuerValidator(location), JwtTypeValidator("JOSE")))
+    return jwtDecoder
+}
+----
+======
+
+To indicate that the `typ` header is optional, use `#setAllowEmpty(true)` (this is the equivalent of including `null` in the list of allowed types in `DefaultJOSEObjectTypeVerifier`).
+
+=== I want to opt-out
+
+If you want to keep doing things the way that you are, then the steps are similar, just in reverse:
+
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+@Bean
+JwtDecoder jwtDecoder() {
+	NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(location)
+        .validateTypes(true) <1>
+        .jwtProcessorCustomizer((c) -> c
+            .setJWSTypeVerifier(new DefaultJOSEObjectTypeVerifier<>("JOSE"))
+        )
+        .build();
+	jwtDecoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(
+		new JwtTimestampValidator(), new JwtIssuerValidator(location))); <2>
+	return jwtDecoder;
+}
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+@Bean
+fun jwtDecoder(): JwtDecoder {
+    val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(location)
+        .validateTypes(true) <1>
+        .jwtProcessorCustomizer {
+            it.setJWSTypeVerifier(DefaultJOSEObjectTypeVerifier("JOSE"))
+        }
+        .build()
+	jwtDecoder.setJwtValidator(DelegatingOAuth2TokenValidator(
+        JwtTimestampValidator(), JwtIssuerValidator(location))) <2>
+    return jwtDecoder
+}
+----
+======
+<1> - leave Nimbus type verification on
+<2> - specify the list of validators you need, excluding `JwtTypeValidator`
+
+For additional guidance, please see the xref:servlet/oauth2/resource-server/jwt.adoc#oauth2resourceserver-jwt-validation[JwtDecoder Validators] section in the reference.

+ 40 - 0
docs/modules/ROOT/pages/reactive/oauth2/resource-server/jwt.adoc

@@ -936,6 +936,46 @@ fun jwtDecoder(): ReactiveJwtDecoder {
 By default, Resource Server configures a clock skew of 60 seconds.
 ====
 
+[[webflux-oauth2resourceserver-validation-rfc9068]]
+=== Configuring RFC 9068 Validation
+
+If you need to require tokens that meet https://datatracker.ietf.org/doc/rfc9068/[RFC 9068], you can configure validation in the following way:
+
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+@Bean
+JwtDecoder jwtDecoder() {
+    NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuerUri)
+            .validateTypes(false).build();
+    jwtDecoder.setJwtValidator(JwtValidators.createAtJwtValidator()
+            .audience("https://audience.example.org")
+            .clientId("client-identifier")
+            .issuer("https://issuer.example.org").build());
+     return jwtDecoder;
+}
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+@Bean
+fun jwtDecoder(): JwtDecoder {
+    val jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuerUri)
+            .validateTypes(false).build()
+    jwtDecoder.setJwtValidator(JwtValidators.createAtJwtValidator()
+            .audience("https://audience.example.org")
+            .clientId("client-identifier")
+            .issuer("https://issuer.example.org").build())
+    return jwtDecoder
+}
+----
+======
+
 [[webflux-oauth2resourceserver-validation-custom]]
 ==== Configuring a Custom Validator
 

+ 40 - 0
docs/modules/ROOT/pages/servlet/oauth2/resource-server/jwt.adoc

@@ -1213,6 +1213,46 @@ fun jwtDecoder(): JwtDecoder {
 [NOTE]
 By default, Resource Server configures a clock skew of 60 seconds.
 
+[[oauth2resourceserver-jwt-validation-rfc9068]]
+=== Configuring RFC 9068 Validation
+
+If you need to require tokens that meet https://datatracker.ietf.org/doc/rfc9068/[RFC 9068], you can configure validation in the following way:
+
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+@Bean
+JwtDecoder jwtDecoder() {
+    NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuerUri)
+            .validateTypes(false).build();
+    jwtDecoder.setJwtValidator(JwtValidators.createAtJwtValidator()
+            .audience("https://audience.example.org")
+            .clientId("client-identifier")
+            .issuer("https://issuer.example.org").build());
+     return jwtDecoder;
+}
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+@Bean
+fun jwtDecoder(): JwtDecoder {
+    val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuerUri)
+            .validateTypes(false).build()
+    jwtDecoder.setJwtValidator(JwtValidators.createAtJwtValidator()
+            .audience("https://audience.example.org")
+            .clientId("client-identifier")
+            .issuer("https://issuer.example.org").build())
+    return jwtDecoder
+}
+----
+======
+
 [[oauth2resourceserver-jwt-validation-custom]]
 === Configuring a Custom Validator
 

+ 5 - 1
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtTypeValidator.java

@@ -36,7 +36,7 @@ import org.springframework.util.StringUtils;
  */
 public final class JwtTypeValidator implements OAuth2TokenValidator<Jwt> {
 
-	private Collection<String> validTypes;
+	private final Collection<String> validTypes;
 
 	private boolean allowEmpty;
 
@@ -45,6 +45,10 @@ public final class JwtTypeValidator implements OAuth2TokenValidator<Jwt> {
 		this.validTypes = new ArrayList<>(validTypes);
 	}
 
+	public JwtTypeValidator(String... validTypes) {
+		this(List.of(validTypes));
+	}
+
 	/**
 	 * Require that the {@code typ} header be {@code JWT} or absent
 	 */

+ 155 - 0
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtValidators.java

@@ -18,10 +18,17 @@ package org.springframework.security.oauth2.jwt;
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
 
 import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
+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.CollectionUtils;
 
@@ -116,4 +123,152 @@ public final class JwtValidators {
 		return createDefaultWithValidators(tokenValidators);
 	}
 
+	/**
+	 * Return a {@link AtJwtBuilder} for building a validator that conforms to
+	 * <a href="https://datatracker.ietf.org/doc/html/rfc9068">RFC 9068</a>.
+	 * @return the {@link AtJwtBuilder} for configuration
+	 * @since 6.5
+	 */
+	public static AtJwtBuilder createAtJwtValidator() {
+		return new AtJwtBuilder();
+	}
+
+	private static RequireClaimValidator require(String claim) {
+		return new RequireClaimValidator(claim);
+	}
+
+	/**
+	 * A class for building a validator that conforms to
+	 * <a href="https://datatracker.ietf.org/doc/html/rfc9068">RFC 9068</a>.
+	 *
+	 * <p>
+	 * To comply with this spec, this builder needs you to specify at least the
+	 * {@link #audience}, {@link #issuer}, and {@link #clientId}.
+	 *
+	 * <p>
+	 * While building, the claims are keyed by claim name to allow for simplified lookup
+	 * and replacement in {@link #validators}.
+	 *
+	 * @author Josh Cummings
+	 * @since 6.5
+	 */
+	public static final class AtJwtBuilder {
+
+		Map<String, OAuth2TokenValidator<Jwt>> validators = new LinkedHashMap<>();
+
+		private AtJwtBuilder() {
+			JwtTimestampValidator timestamps = new JwtTimestampValidator();
+			this.validators.put(JoseHeaderNames.TYP, new JwtTypeValidator(List.of("at+jwt", "application/at+jwt")));
+			this.validators.put(JwtClaimNames.EXP, require(JwtClaimNames.EXP).and(timestamps));
+			this.validators.put(JwtClaimNames.SUB, require(JwtClaimNames.SUB));
+			this.validators.put(JwtClaimNames.IAT, require(JwtClaimNames.IAT).and(timestamps));
+			this.validators.put(JwtClaimNames.JTI, require(JwtClaimNames.JTI));
+		}
+
+		/**
+		 * Validate that each token has this <a href=
+		 * "https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1">issuer</a>.
+		 * @param issuer the required issuer
+		 * @return the {@link AtJwtBuilder} for further configuration
+		 */
+		public AtJwtBuilder issuer(String issuer) {
+			return validators((v) -> v.put(JwtClaimNames.ISS, new JwtIssuerValidator(issuer)));
+		}
+
+		/**
+		 * Validate that each token has this <a href=
+		 * "https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3">audience</a>.
+		 * @param audience the required audience
+		 * @return the {@link AtJwtBuilder} for further configuration
+		 */
+		public AtJwtBuilder audience(String audience) {
+			return validators((v) -> v.put(JwtClaimNames.AUD,
+					require(JwtClaimNames.AUD).satisfies((jwt) -> jwt.getAudience().contains(audience))));
+		}
+
+		/**
+		 * Validate that each token has this <a href=
+		 * "https://datatracker.ietf.org/doc/html/rfc8693#name-client_id-client-identifier">client_id</a>.
+		 * @param clientId the client identifier to use
+		 * @return the {@link AtJwtBuilder} for further configuration
+		 */
+		public AtJwtBuilder clientId(String clientId) {
+			return validators((v) -> v.put("client_id", require("client_id").isEqualTo(clientId)));
+		}
+
+		/**
+		 * Mutate the list of validators by claim name.
+		 *
+		 * <p>
+		 * For example, to add a validator for
+		 * <a href="https://datatracker.ietf.org/doc/html/rfc9068#section-2.2.1">azp</a>
+		 * do: <code>
+		 * 	builder.validators((v) -> v.put("acr", myValidator()));
+		 * </code>
+		 *
+		 * <p>
+		 * A validator is required for all required RFC 9068 claims.
+		 * @param validators the mutator for the map of validators
+		 * @return the {@link AtJwtBuilder} for further configuration
+		 */
+		public AtJwtBuilder validators(Consumer<Map<String, OAuth2TokenValidator<Jwt>>> validators) {
+			validators.accept(this.validators);
+			return this;
+		}
+
+		/**
+		 * Build the validator
+		 * @return the RFC 9068 validator
+		 */
+		public OAuth2TokenValidator<Jwt> build() {
+			List.of(JoseHeaderNames.TYP, JwtClaimNames.EXP, JwtClaimNames.SUB, JwtClaimNames.IAT, JwtClaimNames.JTI,
+					JwtClaimNames.ISS, JwtClaimNames.AUD, "client_id")
+				.forEach((name) -> Assert.isTrue(this.validators.containsKey(name), name + " must be validated"));
+			return new DelegatingOAuth2TokenValidator<>(this.validators.values());
+		}
+
+	}
+
+	private static final class RequireClaimValidator implements OAuth2TokenValidator<Jwt> {
+
+		private final String claimName;
+
+		RequireClaimValidator(String claimName) {
+			this.claimName = claimName;
+		}
+
+		@Override
+		public OAuth2TokenValidatorResult validate(Jwt token) {
+			if (token.getClaim(this.claimName) == null) {
+				return OAuth2TokenValidatorResult
+					.failure(new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, this.claimName + " must have a value",
+							"https://datatracker.ietf.org/doc/html/rfc9068#name-data-structure"));
+			}
+			return OAuth2TokenValidatorResult.success();
+		}
+
+		OAuth2TokenValidator<Jwt> isEqualTo(String value) {
+			return and(satisfies((jwt) -> value.equals(jwt.getClaim(this.claimName))));
+		}
+
+		OAuth2TokenValidator<Jwt> satisfies(Predicate<Jwt> predicate) {
+			return and((jwt) -> {
+				OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, this.claimName + " is not valid",
+						"https://datatracker.ietf.org/doc/html/rfc9068#name-data-structure");
+				if (predicate.test(jwt)) {
+					return OAuth2TokenValidatorResult.success();
+				}
+				return OAuth2TokenValidatorResult.failure(error);
+			});
+		}
+
+		OAuth2TokenValidator<Jwt> and(OAuth2TokenValidator<Jwt> that) {
+			return (jwt) -> {
+				OAuth2TokenValidatorResult result = validate(jwt);
+				return (result.hasErrors()) ? result : that.validate(jwt);
+			};
+		}
+
+	}
+
 }

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

@@ -279,8 +279,7 @@ public final class NimbusJwtDecoder implements JwtDecoder {
 		private Function<JWKSource<SecurityContext>, Set<JWSAlgorithm>> defaultAlgorithms = (source) -> Set
 			.of(JWSAlgorithm.RS256);
 
-		private JOSEObjectTypeVerifier<SecurityContext> typeVerifier = new DefaultJOSEObjectTypeVerifier<>(
-				JOSEObjectType.JWT, null);
+		private JOSEObjectTypeVerifier<SecurityContext> typeVerifier = JWT_TYPE_VERIFIER;
 
 		private Set<SignatureAlgorithm> signatureAlgorithms = new HashSet<>();
 
@@ -332,7 +331,8 @@ public final class NimbusJwtDecoder implements JwtDecoder {
 		 *     NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer)
 		 *         .validateType(false)
 		 *         .build();
-		 *     jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer);
+		 *     jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithValidators(
+		 *     		new JwtIssuerValidator(issuer), JwtTypeValidator.jwt());
 		 * </code>
 		 *
 		 * <p>
@@ -550,8 +550,7 @@ public final class NimbusJwtDecoder implements JwtDecoder {
 
 		private JWSAlgorithm jwsAlgorithm;
 
-		private JOSEObjectTypeVerifier<SecurityContext> typeVerifier = new DefaultJOSEObjectTypeVerifier<>(
-				JOSEObjectType.JWT, null);
+		private JOSEObjectTypeVerifier<SecurityContext> typeVerifier = JWT_TYPE_VERIFIER;
 
 		private RSAPublicKey key;
 
@@ -590,7 +589,8 @@ public final class NimbusJwtDecoder implements JwtDecoder {
 		 *     NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer)
 		 *         .validateType(false)
 		 *         .build();
-		 *     jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer);
+		 *     jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithValidators(
+		 *     		new JwtIssuerValidator(issuer), JwtTypeValidator.jwt());
 		 * </code>
 		 *
 		 * <p>
@@ -686,8 +686,7 @@ public final class NimbusJwtDecoder implements JwtDecoder {
 
 		private JWSAlgorithm jwsAlgorithm = JWSAlgorithm.HS256;
 
-		private JOSEObjectTypeVerifier<SecurityContext> typeVerifier = new DefaultJOSEObjectTypeVerifier<>(
-				JOSEObjectType.JWT, null);
+		private JOSEObjectTypeVerifier<SecurityContext> typeVerifier = JWT_TYPE_VERIFIER;
 
 		private Consumer<ConfigurableJWTProcessor<SecurityContext>> jwtProcessorCustomizer;
 
@@ -723,7 +722,8 @@ public final class NimbusJwtDecoder implements JwtDecoder {
 		 *     NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer)
 		 *         .validateType(false)
 		 *         .build();
-		 *     jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer);
+		 *     jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithValidators(
+		 *     		new JwtIssuerValidator(issuer), JwtTypeValidator.jwt());
 		 * </code>
 		 *
 		 * <p>

+ 237 - 1
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java

@@ -32,6 +32,7 @@ import javax.crypto.SecretKey;
 
 import com.nimbusds.jose.Header;
 import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JOSEObjectType;
 import com.nimbusds.jose.JWSAlgorithm;
 import com.nimbusds.jose.JWSHeader;
 import com.nimbusds.jose.jwk.JWK;
@@ -39,6 +40,8 @@ import com.nimbusds.jose.jwk.JWKMatcher;
 import com.nimbusds.jose.jwk.JWKSelector;
 import com.nimbusds.jose.jwk.source.JWKSecurityContextJWKSet;
 import com.nimbusds.jose.proc.BadJOSEException;
+import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier;
+import com.nimbusds.jose.proc.JOSEObjectTypeVerifier;
 import com.nimbusds.jose.proc.JWKSecurityContext;
 import com.nimbusds.jose.proc.JWSKeySelector;
 import com.nimbusds.jose.proc.JWSVerificationKeySelector;
@@ -308,6 +311,12 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
 	 */
 	public static final class JwkSetUriReactiveJwtDecoderBuilder {
 
+		private static final JOSEObjectTypeVerifier<JWKSecurityContext> JWT_TYPE_VERIFIER = new DefaultJOSEObjectTypeVerifier<>(
+				JOSEObjectType.JWT, null);
+
+		private static final JOSEObjectTypeVerifier<JWKSecurityContext> NO_TYPE_VERIFIER = (header, context) -> {
+		};
+
 		private static final Duration FOREVER = Duration.ofMillis(Long.MAX_VALUE);
 
 		private Function<WebClient, Mono<String>> jwkSetUri;
@@ -315,6 +324,8 @@ 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 Set<SignatureAlgorithm> signatureAlgorithms = new HashSet<>();
 
 		private WebClient webClient = WebClient.create();
@@ -349,6 +360,55 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
 			return this;
 		}
 
+		/**
+		 * 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 NimbusReactiveJwtDecoder} 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>
+		 *     NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuer).build();
+		 *     jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer);
+		 * </code>
+		 *
+		 * <p>
+		 * Is equivalent to this: <code>
+		 *     NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuer)
+		 *         .validateType(false)
+		 *         .build();
+		 *     jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithValidators(
+		 *     		new JwtIssuerValidator(issuer), JwtTypeValidator.jwt());
+		 * </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>
+		 *     NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuer)
+		 *         .validateType(false)
+		 *         .build();
+		 *     jwtDecoder.setJwtValidator(new MyAtJwtValidator());
+		 * </code>
+		 * @param shouldValidateTypHeader whether Nimbus should validate the typ header or
+		 * not
+		 * @return a {@link JwkSetUriReactiveJwtDecoderBuilder} for further configurations
+		 * @since 6.5
+		 */
+		public JwkSetUriReactiveJwtDecoderBuilder validateType(boolean shouldValidateTypHeader) {
+			this.typeVerifier = shouldValidateTypHeader ? JWT_TYPE_VERIFIER : NO_TYPE_VERIFIER;
+			return this;
+		}
+
 		/**
 		 * Configure the list of
 		 * <a href="https://tools.ietf.org/html/rfc7515#section-4.1.1" target=
@@ -435,13 +495,14 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
 			Mono<Tuple2<ConfigurableJWTProcessor<JWKSecurityContext>, Function<JWSAlgorithm, Boolean>>> jwtProcessorMono = jwsKeySelector
 				.flatMap((selector) -> {
 					jwtProcessor.setJWSKeySelector(selector);
+					jwtProcessor.setJWSTypeVerifier(this.typeVerifier);
 					return this.jwtProcessorCustomizer.apply(source, jwtProcessor);
 				})
 				.map((processor) -> Tuples.of(processor, getExpectedJwsAlgorithms(processor.getJWSKeySelector())))
 				.cache((processor) -> FOREVER, (ex) -> Duration.ZERO, () -> Duration.ZERO);
 			return (jwt) -> {
 				return jwtProcessorMono.flatMap((tuple) -> {
-					JWTProcessor<JWKSecurityContext> processor = tuple.getT1();
+					ConfigurableJWTProcessor<JWKSecurityContext> processor = tuple.getT1();
 					Function<JWSAlgorithm, Boolean> expectedJwsAlgorithms = tuple.getT2();
 					JWKSelector selector = createSelector(expectedJwsAlgorithms, jwt.getHeader());
 					return source.get(selector)
@@ -476,10 +537,18 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
 	 */
 	public static final class PublicKeyReactiveJwtDecoderBuilder {
 
+		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 RSAPublicKey key;
 
 		private JWSAlgorithm jwsAlgorithm;
 
+		private JOSEObjectTypeVerifier<SecurityContext> typeVerifier = JWT_TYPE_VERIFIER;
+
 		private Consumer<ConfigurableJWTProcessor<SecurityContext>> jwtProcessorCustomizer;
 
 		private PublicKeyReactiveJwtDecoderBuilder(RSAPublicKey key) {
@@ -505,6 +574,56 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
 			return this;
 		}
 
+		/**
+		 * 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 NimbusReactiveJwtDecoder} 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>
+		 *     NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuer).build();
+		 *     jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer);
+		 * </code>
+		 *
+		 * <p>
+		 * Is equivalent to this: <code>
+		 *     NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withPublicKey(key)
+		 *         .validateType(false)
+		 *         .build();
+		 *     jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithValidators(
+		 *     		new JwtIssuerValidator(issuer), JwtTypeValidator.jwt());
+		 *     		new JwtIssuerValidator(issuer), JwtTypeValidator.jwt());
+		 * </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>
+		 *     NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withPublicKey(key)
+		 *         .validateType(false)
+		 *         .build();
+		 *     jwtDecoder.setJwtValidator(new MyAtJwtValidator());
+		 * </code>
+		 * @param shouldValidateTypHeader whether Nimbus should validate the typ header or
+		 * not
+		 * @return a {@link PublicKeyReactiveJwtDecoderBuilder} for further configurations
+		 * @since 6.5
+		 */
+		public PublicKeyReactiveJwtDecoderBuilder validateType(boolean shouldValidateTypHeader) {
+			this.typeVerifier = shouldValidateTypHeader ? JWT_TYPE_VERIFIER : NO_TYPE_VERIFIER;
+			return this;
+		}
+
 		/**
 		 * Use the given {@link Consumer} to customize the {@link JWTProcessor
 		 * ConfigurableJWTProcessor} before passing it to the build
@@ -535,6 +654,7 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
 			JWSKeySelector<SecurityContext> jwsKeySelector = new SingleKeyJWSKeySelector<>(this.jwsAlgorithm, this.key);
 			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) -> {
 			});
@@ -552,10 +672,18 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
 	 */
 	public static final class SecretKeyReactiveJwtDecoderBuilder {
 
+		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 = JWT_TYPE_VERIFIER;
+
 		private Consumer<ConfigurableJWTProcessor<SecurityContext>> jwtProcessorCustomizer;
 
 		private SecretKeyReactiveJwtDecoderBuilder(SecretKey secretKey) {
@@ -582,6 +710,55 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
 			return this;
 		}
 
+		/**
+		 * 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 NimbusReactiveJwtDecoder} 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>
+		 *     NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuer).build();
+		 *     jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer);
+		 * </code>
+		 *
+		 * <p>
+		 * Is equivalent to this: <code>
+		 *     NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withSecretKey(key)
+		 *         .validateType(false)
+		 *         .build();
+		 *     jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithValidators(
+		 *     		new JwtIssuerValidator(issuer), JwtTypeValidator.jwt());
+		 * </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>
+		 *     NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withSecretKey(key)
+		 *         .validateType(false)
+		 *         .build();
+		 *     jwtDecoder.setJwtValidator(new MyAtJwtValidator());
+		 * </code>
+		 * @param shouldValidateTypHeader whether Nimbus should validate the typ header or
+		 * not
+		 * @return a {@link PublicKeyReactiveJwtDecoderBuilder} for further configurations
+		 * @since 6.5
+		 */
+		public SecretKeyReactiveJwtDecoderBuilder validateType(boolean shouldValidateTypHeader) {
+			this.typeVerifier = shouldValidateTypHeader ? JWT_TYPE_VERIFIER : NO_TYPE_VERIFIER;
+			return this;
+		}
+
 		/**
 		 * Use the given {@link Consumer} to customize the {@link JWTProcessor
 		 * ConfigurableJWTProcessor} before passing it to the build
@@ -610,6 +787,7 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
 					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) -> {
 			});
@@ -626,10 +804,18 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
 	 */
 	public static final class JwkSourceReactiveJwtDecoderBuilder {
 
+		private static final JOSEObjectTypeVerifier<JWKSecurityContext> JWT_TYPE_VERIFIER = new DefaultJOSEObjectTypeVerifier<>(
+				JOSEObjectType.JWT, null);
+
+		private static final JOSEObjectTypeVerifier<JWKSecurityContext> NO_TYPE_VERIFIER = (header, context) -> {
+		};
+
 		private final Function<SignedJWT, Flux<JWK>> jwkSource;
 
 		private JWSAlgorithm jwsAlgorithm = JWSAlgorithm.RS256;
 
+		private JOSEObjectTypeVerifier<JWKSecurityContext> typeVerifier = JWT_TYPE_VERIFIER;
+
 		private Consumer<ConfigurableJWTProcessor<JWKSecurityContext>> jwtProcessorCustomizer;
 
 		private JwkSourceReactiveJwtDecoderBuilder(Function<SignedJWT, Flux<JWK>> jwkSource) {
@@ -652,6 +838,55 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
 			return this;
 		}
 
+		/**
+		 * 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 NimbusReactiveJwtDecoder} 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>
+		 *     NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withJwkSource(issuer).build();
+		 *     jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer);
+		 * </code>
+		 *
+		 * <p>
+		 * Is equivalent to this: <code>
+		 *     NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withJwkSource(key)
+		 *         .validateType(false)
+		 *         .build();
+		 *     jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithValidators(
+		 *     		new JwtIssuerValidator(issuer), JwtTypeValidator.jwt());
+		 * </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>
+		 *     NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withJwkSource(key)
+		 *         .validateType(false)
+		 *         .build();
+		 *     jwtDecoder.setJwtValidator(new MyAtJwtValidator());
+		 * </code>
+		 * @param shouldValidateTypHeader whether Nimbus should validate the typ header or
+		 * not
+		 * @return a {@link JwkSourceReactiveJwtDecoderBuilder} for further configurations
+		 * @since 6.5
+		 */
+		public JwkSourceReactiveJwtDecoderBuilder validateType(boolean shouldValidateTypHeader) {
+			this.typeVerifier = shouldValidateTypHeader ? JWT_TYPE_VERIFIER : NO_TYPE_VERIFIER;
+			return this;
+		}
+
 		/**
 		 * Use the given {@link Consumer} to customize the {@link JWTProcessor
 		 * ConfigurableJWTProcessor} before passing it to the build
@@ -681,6 +916,7 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
 					jwkSource);
 			DefaultJWTProcessor<JWKSecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
 			jwtProcessor.setJWSKeySelector(jwsKeySelector);
+			jwtProcessor.setJWSTypeVerifier(this.typeVerifier);
 			jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> {
 			});
 			this.jwtProcessorCustomizer.accept(jwtProcessor);

+ 36 - 0
oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtValidatorsTests.java

@@ -18,12 +18,14 @@ package org.springframework.security.oauth2.jwt;
 
 import java.util.Collection;
 import java.util.Collections;
+import java.util.List;
 import java.util.Objects;
 
 import org.junit.jupiter.api.Test;
 
 import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
 import org.springframework.security.oauth2.core.OAuth2TokenValidator;
+import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
 import org.springframework.test.util.ReflectionTestUtils;
 import org.springframework.util.CollectionUtils;
 
@@ -68,6 +70,40 @@ public class JwtValidatorsTests {
 		assertThatException().isThrownBy(() -> JwtValidators.createDefaultWithValidators(Collections.emptyList()));
 	}
 
+	@Test
+	public void createAtJwtWhenIssuerClientIdAudienceThenBuilds() {
+		Jwt.Builder builder = TestJwts.jwt();
+		OAuth2TokenValidator<Jwt> validator = JwtValidators.createAtJwtValidator()
+			.audience("audience")
+			.clientId("clientId")
+			.issuer("issuer")
+			.build();
+
+		OAuth2TokenValidatorResult result = validator.validate(builder.build());
+		assertThat(result.getErrors().toString()).contains("at+jwt")
+			.contains("aud")
+			.contains("client_id")
+			.contains("iss");
+
+		result = validator.validate(builder.header(JoseHeaderNames.TYP, "JWT").build());
+		assertThat(result.getErrors().toString()).contains("at+jwt");
+
+		result = validator.validate(builder.header(JoseHeaderNames.TYP, "at+jwt").build());
+		assertThat(result.getErrors().toString()).doesNotContain("at+jwt");
+
+		result = validator.validate(builder.header(JoseHeaderNames.TYP, "application/at+jwt").build());
+		assertThat(result.getErrors().toString()).doesNotContain("at+jwt");
+
+		result = validator.validate(builder.audience(List.of("audience")).build());
+		assertThat(result.getErrors().toString()).doesNotContain("aud");
+
+		result = validator.validate(builder.claim("client_id", "clientId").build());
+		assertThat(result.getErrors().toString()).doesNotContain("client_id");
+
+		result = validator.validate(builder.issuer("issuer").build());
+		assertThat(result.getErrors().toString()).doesNotContain("iss");
+	}
+
 	@SuppressWarnings("unchecked")
 	private boolean containsByType(OAuth2TokenValidator<Jwt> validator, Class<? extends OAuth2TokenValidator<?>> type) {
 		DelegatingOAuth2TokenValidator<Jwt> delegatingOAuth2TokenValidator = (DelegatingOAuth2TokenValidator<Jwt>) validator;

+ 2 - 1
oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderTests.java

@@ -849,7 +849,8 @@ public class NimbusJwtDecoderTests {
 		NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withSecretKey(TestKeys.DEFAULT_SECRET_KEY)
 			.validateType(false)
 			.build();
-		SignedJWT jwt = signedJwt(TestKeys.DEFAULT_SECRET_KEY, MacAlgorithm.HS256,
+		SignedJWT jwt = signedJwt(TestKeys.DEFAULT_SECRET_KEY,
+				new JWSHeader.Builder(JWSAlgorithm.HS256).type(JOSEObjectType.JOSE).build(),
 				new JWTClaimsSet.Builder().subject("subject").build());
 		jwtDecoder.decode(jwt.serialize());
 	}

+ 55 - 1
oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoderTests.java

@@ -19,6 +19,8 @@ package org.springframework.security.oauth2.jwt;
 import java.net.UnknownHostException;
 import java.security.KeyFactory;
 import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.interfaces.RSAPrivateKey;
 import java.security.interfaces.RSAPublicKey;
 import java.security.spec.EncodedKeySpec;
 import java.security.spec.InvalidKeySpecException;
@@ -38,6 +40,8 @@ import com.nimbusds.jose.JWSAlgorithm;
 import com.nimbusds.jose.JWSHeader;
 import com.nimbusds.jose.JWSSigner;
 import com.nimbusds.jose.crypto.MACSigner;
+import com.nimbusds.jose.crypto.RSASSASigner;
+import com.nimbusds.jose.jwk.JWK;
 import com.nimbusds.jose.jwk.JWKSet;
 import com.nimbusds.jose.jwk.RSAKey;
 import com.nimbusds.jose.jwk.source.JWKSecurityContextJWKSet;
@@ -658,10 +662,60 @@ public class NimbusReactiveJwtDecoderTests {
 		assertThat(jwsAlgorithmMapKeySelector.isAllowed(JWSAlgorithm.RS512)).isTrue();
 	}
 
+	@Test
+	public void decodeWhenPublicKeyValidateTypeFalseThenSkipsNimbusTypeValidation() throws Exception {
+		NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.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()).block();
+	}
+
+	@Test
+	public void decodeWhenSecretKeyValidateTypeFalseThenSkipsNimbusTypeValidation() throws Exception {
+		NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withSecretKey(TestKeys.DEFAULT_SECRET_KEY)
+			.validateType(false)
+			.build();
+		SignedJWT jwt = signedJwt(TestKeys.DEFAULT_SECRET_KEY,
+				new JWSHeader.Builder(JWSAlgorithm.HS256).type(JOSEObjectType.JOSE).build(),
+				new JWTClaimsSet.Builder().subject("subject").build());
+		jwtDecoder.decode(jwt.serialize()).block();
+	}
+
+	@Test
+	public void decodeWhenJwkSourceValidateTypeFalseThenSkipsNimbusTypeValidation() throws Exception {
+		JWK jwk = new RSAKey.Builder(TestKeys.DEFAULT_PUBLIC_KEY).privateKey(TestKeys.DEFAULT_PRIVATE_KEY)
+			.algorithm(JWSAlgorithm.RS256)
+			.build();
+		NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withJwkSource((jwt) -> Flux.just(jwk))
+			.validateType(false)
+			.build();
+		SignedJWT jwt = signedJwt(TestKeys.DEFAULT_PRIVATE_KEY,
+				new JWSHeader.Builder(JWSAlgorithm.RS256).type(JOSEObjectType.JOSE).build(),
+				new JWTClaimsSet.Builder().subject("subject").build());
+		jwtDecoder.decode(jwt.serialize()).block();
+	}
+
 	private SignedJWT signedJwt(SecretKey secretKey, MacAlgorithm jwsAlgorithm, JWTClaimsSet claimsSet)
 			throws Exception {
-		SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.parse(jwsAlgorithm.getName())), claimsSet);
+		return signedJwt(secretKey, new JWSHeader(JWSAlgorithm.parse(jwsAlgorithm.getName())), claimsSet);
+	}
+
+	private SignedJWT signedJwt(SecretKey secretKey, JWSHeader header, JWTClaimsSet claimsSet) throws Exception {
 		JWSSigner signer = new MACSigner(secretKey);
+		return signedJwt(signer, header, claimsSet);
+	}
+
+	private SignedJWT signedJwt(PrivateKey privateKey, JWSHeader header, JWTClaimsSet claimsSet) throws Exception {
+		JWSSigner signer = new RSASSASigner(privateKey);
+		return signedJwt(signer, header, claimsSet);
+	}
+
+	private SignedJWT signedJwt(JWSSigner signer, JWSHeader header, JWTClaimsSet claimsSet) throws Exception {
+		SignedJWT signedJWT = new SignedJWT(header, claimsSet);
 		signedJWT.sign(signer);
 		return signedJWT;
 	}