瀏覽代碼

Add withIssuerLocation

Closes gh-10309
Josh Cummings 2 年之前
父節點
當前提交
76eba9bd0c

+ 52 - 7
docs/modules/ROOT/pages/reactive/oauth2/resource-server/jwt.adoc

@@ -327,6 +327,7 @@ This is handy when you need deeper configuration, such as <<webflux-oauth2resour
 ==== Exposing a `ReactiveJwtDecoder` `@Bean`
 ==== Exposing a `ReactiveJwtDecoder` `@Bean`
 
 
 Alternately, exposing a `ReactiveJwtDecoder` `@Bean` has the same effect as `decoder()`:
 Alternately, exposing a `ReactiveJwtDecoder` `@Bean` has the same effect as `decoder()`:
+You can construct one with a `jwkSetUri` like so:
 
 
 ====
 ====
 .Java
 .Java
@@ -343,7 +344,51 @@ public ReactiveJwtDecoder jwtDecoder() {
 ----
 ----
 @Bean
 @Bean
 fun jwtDecoder(): ReactiveJwtDecoder {
 fun jwtDecoder(): ReactiveJwtDecoder {
-    return ReactiveJwtDecoders.fromIssuerLocation(issuerUri)
+    return NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).build()
+}
+----
+====
+
+or you can use the issuer and have `NimbusReactiveJwtDecoder` look up the `jwkSetUri` when `build()` is invoked, like the following:
+
+====
+.Java
+[source,java,role="primary"]
+----
+@Bean
+public ReactiveJwtDecoder jwtDecoder() {
+    return NimbusReactiveJwtDecoder.withIssuerLocation(issuer).build();
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@Bean
+fun jwtDecoder(): ReactiveJwtDecoder {
+    return NimbusReactiveJwtDecoder.withIssuerLocation(issuer).build()
+}
+----
+====
+
+Or, if the defaults work for you, you can also use `JwtDecoders`, which does the above in addition to configuring the decoder's validator:
+
+====
+.Java
+[source,java,role="primary"]
+----
+@Bean
+public ReactiveJwtDecoder jwtDecoder() {
+    return ReactiveJwtDecoders.fromIssuerLocation(issuer);
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@Bean
+fun jwtDecoder(): ReactiveJwtDecoder {
+    return ReactiveJwtDecoders.fromIssuerLocation(issuer)
 }
 }
 ----
 ----
 ====
 ====
@@ -384,7 +429,7 @@ For greater power, though, we can use a builder that ships with `NimbusReactiveJ
 ----
 ----
 @Bean
 @Bean
 ReactiveJwtDecoder jwtDecoder() {
 ReactiveJwtDecoder jwtDecoder() {
-    return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
+    return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
             .jwsAlgorithm(RS512).build();
             .jwsAlgorithm(RS512).build();
 }
 }
 ----
 ----
@@ -394,7 +439,7 @@ ReactiveJwtDecoder jwtDecoder() {
 ----
 ----
 @Bean
 @Bean
 fun jwtDecoder(): ReactiveJwtDecoder {
 fun jwtDecoder(): ReactiveJwtDecoder {
-    return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
+    return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
             .jwsAlgorithm(RS512).build()
             .jwsAlgorithm(RS512).build()
 }
 }
 ----
 ----
@@ -408,7 +453,7 @@ Calling `jwsAlgorithm` more than once configures `NimbusReactiveJwtDecoder` to t
 ----
 ----
 @Bean
 @Bean
 ReactiveJwtDecoder jwtDecoder() {
 ReactiveJwtDecoder jwtDecoder() {
-    return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
+    return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
             .jwsAlgorithm(RS512).jwsAlgorithm(ES512).build();
             .jwsAlgorithm(RS512).jwsAlgorithm(ES512).build();
 }
 }
 ----
 ----
@@ -418,7 +463,7 @@ ReactiveJwtDecoder jwtDecoder() {
 ----
 ----
 @Bean
 @Bean
 fun jwtDecoder(): ReactiveJwtDecoder {
 fun jwtDecoder(): ReactiveJwtDecoder {
-    return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
+    return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
             .jwsAlgorithm(RS512).jwsAlgorithm(ES512).build()
             .jwsAlgorithm(RS512).jwsAlgorithm(ES512).build()
 }
 }
 ----
 ----
@@ -432,7 +477,7 @@ Alternately, you can call `jwsAlgorithms`:
 ----
 ----
 @Bean
 @Bean
 ReactiveJwtDecoder jwtDecoder() {
 ReactiveJwtDecoder jwtDecoder() {
-    return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
+    return NimbusReactiveJwtDecoder.withIssuerLocation(this.jwkSetUri)
             .jwsAlgorithms(algorithms -> {
             .jwsAlgorithms(algorithms -> {
                     algorithms.add(RS512);
                     algorithms.add(RS512);
                     algorithms.add(ES512);
                     algorithms.add(ES512);
@@ -445,7 +490,7 @@ ReactiveJwtDecoder jwtDecoder() {
 ----
 ----
 @Bean
 @Bean
 fun jwtDecoder(): ReactiveJwtDecoder {
 fun jwtDecoder(): ReactiveJwtDecoder {
-    return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
+    return NimbusReactiveJwtDecoder.withIssuerLocation(this.jwkSetUri)
             .jwsAlgorithms {
             .jwsAlgorithms {
                 it.add(RS512)
                 it.add(RS512)
                 it.add(ES512)
                 it.add(ES512)

+ 60 - 15
docs/modules/ROOT/pages/servlet/oauth2/resource-server/jwt.adoc

@@ -430,7 +430,8 @@ This is handy when deeper configuration, like <<oauth2resourceserver-jwt-validat
 [[oauth2resourceserver-jwt-decoder-bean]]
 [[oauth2resourceserver-jwt-decoder-bean]]
 === Exposing a `JwtDecoder` `@Bean`
 === Exposing a `JwtDecoder` `@Bean`
 
 
-Or, exposing a <<oauth2resourceserver-jwt-architecture-jwtdecoder,`JwtDecoder`>> `@Bean` has the same effect as `decoder()`:
+Or, exposing a <<oauth2resourceserver-jwt-architecture-jwtdecoder,`JwtDecoder`>> `@Bean` has the same effect as `decoder()`.
+You can construct one with a `jwkSetUri` like so:
 
 
 ====
 ====
 .Java
 .Java
@@ -452,6 +453,50 @@ fun jwtDecoder(): JwtDecoder {
 ----
 ----
 ====
 ====
 
 
+or you can use the issuer and have `NimbusJwtDecoder` look up the `jwkSetUri` when `build()` is invoked, like the following:
+
+====
+.Java
+[source,java,role="primary"]
+----
+@Bean
+public JwtDecoder jwtDecoder() {
+    return NimbusJwtDecoder.withIssuerLocation(issuer).build();
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@Bean
+fun jwtDecoder(): JwtDecoder {
+    return NimbusJwtDecoder.withIssuerLocation(issuer).build()
+}
+----
+====
+
+Or, if the defaults work for you, you can also use `JwtDecoders`, which does the above in addition to configuring the decoder's validator:
+
+====
+.Java
+[source,java,role="primary"]
+----
+@Bean
+public JwtDecoders jwtDecoder() {
+    return JwtDecoders.fromIssuerLocation(issuer);
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@Bean
+fun jwtDecoder(): JwtDecoders {
+    return JwtDecoders.fromIssuerLocation(issuer)
+}
+----
+====
+
 [[oauth2resourceserver-jwt-decoder-algorithm]]
 [[oauth2resourceserver-jwt-decoder-algorithm]]
 == Configuring Trusted Algorithms
 == Configuring Trusted Algorithms
 
 
@@ -486,7 +531,7 @@ For greater power, though, we can use a builder that ships with `NimbusJwtDecode
 ----
 ----
 @Bean
 @Bean
 JwtDecoder jwtDecoder() {
 JwtDecoder jwtDecoder() {
-    return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri)
+    return NimbusJwtDecoder.withIssuerLocation(this.issuer)
             .jwsAlgorithm(RS512).build();
             .jwsAlgorithm(RS512).build();
 }
 }
 ----
 ----
@@ -496,7 +541,7 @@ JwtDecoder jwtDecoder() {
 ----
 ----
 @Bean
 @Bean
 fun jwtDecoder(): JwtDecoder {
 fun jwtDecoder(): JwtDecoder {
-    return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri)
+    return NimbusJwtDecoder.withIssuerLocation(this.issuer)
             .jwsAlgorithm(RS512).build()
             .jwsAlgorithm(RS512).build()
 }
 }
 ----
 ----
@@ -510,7 +555,7 @@ Calling `jwsAlgorithm` more than once will configure `NimbusJwtDecoder` to trust
 ----
 ----
 @Bean
 @Bean
 JwtDecoder jwtDecoder() {
 JwtDecoder jwtDecoder() {
-    return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri)
+    return NimbusJwtDecoder.withIssuerLocation(this.issuer)
             .jwsAlgorithm(RS512).jwsAlgorithm(ES512).build();
             .jwsAlgorithm(RS512).jwsAlgorithm(ES512).build();
 }
 }
 ----
 ----
@@ -520,7 +565,7 @@ JwtDecoder jwtDecoder() {
 ----
 ----
 @Bean
 @Bean
 fun jwtDecoder(): JwtDecoder {
 fun jwtDecoder(): JwtDecoder {
-    return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri)
+    return NimbusJwtDecoder.withIssuerLocation(this.issuer)
             .jwsAlgorithm(RS512).jwsAlgorithm(ES512).build()
             .jwsAlgorithm(RS512).jwsAlgorithm(ES512).build()
 }
 }
 ----
 ----
@@ -534,7 +579,7 @@ Or, you can call `jwsAlgorithms`:
 ----
 ----
 @Bean
 @Bean
 JwtDecoder jwtDecoder() {
 JwtDecoder jwtDecoder() {
-    return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri)
+    return NimbusJwtDecoder.withIssuerLocation(this.issuer)
             .jwsAlgorithms(algorithms -> {
             .jwsAlgorithms(algorithms -> {
                     algorithms.add(RS512);
                     algorithms.add(RS512);
                     algorithms.add(ES512);
                     algorithms.add(ES512);
@@ -547,7 +592,7 @@ JwtDecoder jwtDecoder() {
 ----
 ----
 @Bean
 @Bean
 fun jwtDecoder(): JwtDecoder {
 fun jwtDecoder(): JwtDecoder {
-    return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri)
+    return NimbusJwtDecoder.withIssuerLocation(this.issuer)
             .jwsAlgorithms {
             .jwsAlgorithms {
                 it.add(RS512)
                 it.add(RS512)
                 it.add(ES512)
                 it.add(ES512)
@@ -1207,7 +1252,7 @@ An individual claim's conversion strategy can be configured using `MappedJwtClai
 ----
 ----
 @Bean
 @Bean
 JwtDecoder jwtDecoder() {
 JwtDecoder jwtDecoder() {
-    NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
+    NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build();
 
 
     MappedJwtClaimSetConverter converter = MappedJwtClaimSetConverter
     MappedJwtClaimSetConverter converter = MappedJwtClaimSetConverter
             .withDefaults(Collections.singletonMap("sub", this::lookupUserIdBySub));
             .withDefaults(Collections.singletonMap("sub", this::lookupUserIdBySub));
@@ -1222,7 +1267,7 @@ JwtDecoder jwtDecoder() {
 ----
 ----
 @Bean
 @Bean
 fun jwtDecoder(): JwtDecoder {
 fun jwtDecoder(): JwtDecoder {
-    val jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build()
+    val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build()
 
 
     val converter = MappedJwtClaimSetConverter
     val converter = MappedJwtClaimSetConverter
             .withDefaults(mapOf("sub" to this::lookupUserIdBySub))
             .withDefaults(mapOf("sub" to this::lookupUserIdBySub))
@@ -1319,7 +1364,7 @@ And then, the instance can be supplied like normal:
 ----
 ----
 @Bean
 @Bean
 JwtDecoder jwtDecoder() {
 JwtDecoder jwtDecoder() {
-    NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
+    NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build();
     jwtDecoder.setClaimSetConverter(new UsernameSubClaimAdapter());
     jwtDecoder.setClaimSetConverter(new UsernameSubClaimAdapter());
     return jwtDecoder;
     return jwtDecoder;
 }
 }
@@ -1330,7 +1375,7 @@ JwtDecoder jwtDecoder() {
 ----
 ----
 @Bean
 @Bean
 fun jwtDecoder(): JwtDecoder {
 fun jwtDecoder(): JwtDecoder {
-    val jwtDecoder: NimbusJwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build()
+    val jwtDecoder: NimbusJwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build()
     jwtDecoder.setClaimSetConverter(UsernameSubClaimAdapter())
     jwtDecoder.setClaimSetConverter(UsernameSubClaimAdapter())
     return jwtDecoder
     return jwtDecoder
 }
 }
@@ -1358,7 +1403,7 @@ public JwtDecoder jwtDecoder(RestTemplateBuilder builder) {
             .setReadTimeout(Duration.ofSeconds(60))
             .setReadTimeout(Duration.ofSeconds(60))
             .build();
             .build();
 
 
-    NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).restOperations(rest).build();
+    NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).restOperations(rest).build();
     return jwtDecoder;
     return jwtDecoder;
 }
 }
 ----
 ----
@@ -1372,7 +1417,7 @@ fun jwtDecoder(builder: RestTemplateBuilder): JwtDecoder {
             .setConnectTimeout(Duration.ofSeconds(60))
             .setConnectTimeout(Duration.ofSeconds(60))
             .setReadTimeout(Duration.ofSeconds(60))
             .setReadTimeout(Duration.ofSeconds(60))
             .build()
             .build()
-    return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).restOperations(rest).build()
+    return NimbusJwtDecoder.withIssuerLocation(issuer).restOperations(rest).build()
 }
 }
 ----
 ----
 ====
 ====
@@ -1388,7 +1433,7 @@ To adjust the way in which Resource Server caches the JWK set, `NimbusJwtDecoder
 ----
 ----
 @Bean
 @Bean
 public JwtDecoder jwtDecoder(CacheManager cacheManager) {
 public JwtDecoder jwtDecoder(CacheManager cacheManager) {
-    return NimbusJwtDecoder.withJwkSetUri(jwkSetUri)
+    return NimbusJwtDecoder.withIssuerLocation(issuer)
             .cache(cacheManager.getCache("jwks"))
             .cache(cacheManager.getCache("jwks"))
             .build();
             .build();
 }
 }
@@ -1399,7 +1444,7 @@ public JwtDecoder jwtDecoder(CacheManager cacheManager) {
 ----
 ----
 @Bean
 @Bean
 fun jwtDecoder(cacheManager: CacheManager): JwtDecoder {
 fun jwtDecoder(cacheManager: CacheManager): JwtDecoder {
-    return NimbusJwtDecoder.withJwkSetUri(jwkSetUri)
+    return NimbusJwtDecoder.withIssuerLocation(issuer)
             .cache(cacheManager.getCache("jwks"))
             .cache(cacheManager.getCache("jwks"))
             .build()
             .build()
 }
 }

+ 10 - 5
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoderProviderConfigurationUtils.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2002-2021 the original author or authors.
+ * Copyright 2002-2023 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -42,6 +42,7 @@ import org.springframework.http.ResponseEntity;
 import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
 import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
 import org.springframework.util.Assert;
 import org.springframework.util.Assert;
 import org.springframework.web.client.HttpClientErrorException;
 import org.springframework.web.client.HttpClientErrorException;
+import org.springframework.web.client.RestOperations;
 import org.springframework.web.client.RestTemplate;
 import org.springframework.web.client.RestTemplate;
 import org.springframework.web.util.UriComponentsBuilder;
 import org.springframework.web.util.UriComponentsBuilder;
 
 
@@ -71,12 +72,16 @@ final class JwtDecoderProviderConfigurationUtils {
 	}
 	}
 
 
 	static Map<String, Object> getConfigurationForOidcIssuerLocation(String oidcIssuerLocation) {
 	static Map<String, Object> getConfigurationForOidcIssuerLocation(String oidcIssuerLocation) {
-		return getConfiguration(oidcIssuerLocation, oidc(URI.create(oidcIssuerLocation)));
+		return getConfiguration(oidcIssuerLocation, rest, oidc(URI.create(oidcIssuerLocation)));
 	}
 	}
 
 
-	static Map<String, Object> getConfigurationForIssuerLocation(String issuer) {
+	static Map<String, Object> getConfigurationForIssuerLocation(String issuer, RestOperations rest) {
 		URI uri = URI.create(issuer);
 		URI uri = URI.create(issuer);
-		return getConfiguration(issuer, oidc(uri), oidcRfc8414(uri), oauth(uri));
+		return getConfiguration(issuer, rest, oidc(uri), oidcRfc8414(uri), oauth(uri));
+	}
+
+	static Map<String, Object> getConfigurationForIssuerLocation(String issuer) {
+		return getConfigurationForIssuerLocation(issuer, rest);
 	}
 	}
 
 
 	static void validateIssuer(Map<String, Object> configuration, String issuer) {
 	static void validateIssuer(Map<String, Object> configuration, String issuer) {
@@ -142,7 +147,7 @@ final class JwtDecoderProviderConfigurationUtils {
 		return "(unavailable)";
 		return "(unavailable)";
 	}
 	}
 
 
-	private static Map<String, Object> getConfiguration(String issuer, URI... uris) {
+	private static Map<String, Object> getConfiguration(String issuer, RestOperations rest, URI... uris) {
 		String errorMessage = "Unable to resolve the Configuration with the provided Issuer of " + "\"" + issuer + "\"";
 		String errorMessage = "Unable to resolve the Configuration with the provided Issuer of " + "\"" + issuer + "\"";
 		for (URI uri : uris) {
 		for (URI uri : uris) {
 			try {
 			try {

+ 5 - 4
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoders.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2023 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -89,9 +89,10 @@ public final class JwtDecoders {
 	@SuppressWarnings("unchecked")
 	@SuppressWarnings("unchecked")
 	public static <T extends JwtDecoder> T fromIssuerLocation(String issuer) {
 	public static <T extends JwtDecoder> T fromIssuerLocation(String issuer) {
 		Assert.hasText(issuer, "issuer cannot be empty");
 		Assert.hasText(issuer, "issuer cannot be empty");
-		Map<String, Object> configuration = JwtDecoderProviderConfigurationUtils
-				.getConfigurationForIssuerLocation(issuer);
-		return (T) withProviderConfiguration(configuration, issuer);
+		NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build();
+		OAuth2TokenValidator<Jwt> jwtValidator = JwtValidators.createDefaultWithIssuer(issuer);
+		jwtDecoder.setJwtValidator(jwtValidator);
+		return (T) jwtDecoder;
 	}
 	}
 
 
 	/**
 	/**

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

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2002-2022 the original author or authors.
+ * Copyright 2002-2023 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -29,6 +29,7 @@ import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Map;
 import java.util.Set;
 import java.util.Set;
 import java.util.function.Consumer;
 import java.util.function.Consumer;
+import java.util.function.Function;
 
 
 import javax.crypto.SecretKey;
 import javax.crypto.SecretKey;
 
 
@@ -200,6 +201,31 @@ public final class NimbusJwtDecoder implements JwtDecoder {
 		return "Unable to validate Jwt";
 		return "Unable to validate Jwt";
 	}
 	}
 
 
+	/**
+	 * Use the given <a href=
+	 * "https://openid.net/specs/openid-connect-core-1_0.html#IssuerIdentifier">Issuer</a>
+	 * by making an <a href=
+	 * "https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest">OpenID
+	 * Provider Configuration Request</a> and using the values in the <a href=
+	 * "https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse">OpenID
+	 * Provider Configuration Response</a> to derive the needed
+	 * <a href="https://tools.ietf.org/html/rfc7517#section-5">JWK Set</a> uri.
+	 * @param issuer the <a href=
+	 * "https://openid.net/specs/openid-connect-core-1_0.html#IssuerIdentifier">Issuer</a>
+	 * @return a {@link JwkSetUriJwtDecoderBuilder} that will derive the JWK Set uri when
+	 * {@link JwkSetUriJwtDecoderBuilder#build} is called
+	 * @since 6.1
+	 * @see JwtDecoders
+	 */
+	public static JwkSetUriJwtDecoderBuilder withIssuerLocation(String issuer) {
+		return new JwkSetUriJwtDecoderBuilder((rest) -> {
+			Map<String, Object> configuration = JwtDecoderProviderConfigurationUtils
+					.getConfigurationForIssuerLocation(issuer, rest);
+			JwtDecoderProviderConfigurationUtils.validateIssuer(configuration, issuer);
+			return configuration.get("jwks_uri").toString();
+		}, JwtDecoderProviderConfigurationUtils::getJWSAlgorithms);
+	}
+
 	/**
 	/**
 	 * Use the given <a href="https://tools.ietf.org/html/rfc7517#section-5">JWK Set</a>
 	 * Use the given <a href="https://tools.ietf.org/html/rfc7517#section-5">JWK Set</a>
 	 * uri.
 	 * uri.
@@ -235,7 +261,10 @@ public final class NimbusJwtDecoder implements JwtDecoder {
 	 */
 	 */
 	public static final class JwkSetUriJwtDecoderBuilder {
 	public static final class JwkSetUriJwtDecoderBuilder {
 
 
-		private String jwkSetUri;
+		private Function<RestOperations, String> jwkSetUri;
+
+		private Function<JWKSource<SecurityContext>, Set<JWSAlgorithm>> defaultAlgorithms = (source) -> Set
+				.of(JWSAlgorithm.RS256);
 
 
 		private Set<SignatureAlgorithm> signatureAlgorithms = new HashSet<>();
 		private Set<SignatureAlgorithm> signatureAlgorithms = new HashSet<>();
 
 
@@ -247,7 +276,17 @@ public final class NimbusJwtDecoder implements JwtDecoder {
 
 
 		private JwkSetUriJwtDecoderBuilder(String jwkSetUri) {
 		private JwkSetUriJwtDecoderBuilder(String jwkSetUri) {
 			Assert.hasText(jwkSetUri, "jwkSetUri cannot be empty");
 			Assert.hasText(jwkSetUri, "jwkSetUri cannot be empty");
+			this.jwkSetUri = (rest) -> jwkSetUri;
+			this.jwtProcessorCustomizer = (processor) -> {
+			};
+		}
+
+		private JwkSetUriJwtDecoderBuilder(Function<RestOperations, String> jwkSetUri,
+				Function<JWKSource<SecurityContext>, Set<JWSAlgorithm>> defaultAlgorithms) {
+			Assert.notNull(jwkSetUri, "jwkSetUri function cannot be null");
+			Assert.notNull(defaultAlgorithms, "defaultAlgorithms function cannot be null");
 			this.jwkSetUri = jwkSetUri;
 			this.jwkSetUri = jwkSetUri;
+			this.defaultAlgorithms = defaultAlgorithms;
 			this.jwtProcessorCustomizer = (processor) -> {
 			this.jwtProcessorCustomizer = (processor) -> {
 			};
 			};
 		}
 		}
@@ -324,7 +363,7 @@ public final class NimbusJwtDecoder implements JwtDecoder {
 
 
 		JWSKeySelector<SecurityContext> jwsKeySelector(JWKSource<SecurityContext> jwkSource) {
 		JWSKeySelector<SecurityContext> jwsKeySelector(JWKSource<SecurityContext> jwkSource) {
 			if (this.signatureAlgorithms.isEmpty()) {
 			if (this.signatureAlgorithms.isEmpty()) {
-				return new JWSVerificationKeySelector<>(JWSAlgorithm.RS256, jwkSource);
+				return new JWSVerificationKeySelector<>(this.defaultAlgorithms.apply(jwkSource), jwkSource);
 			}
 			}
 			Set<JWSAlgorithm> jwsAlgorithms = new HashSet<>();
 			Set<JWSAlgorithm> jwsAlgorithms = new HashSet<>();
 			for (SignatureAlgorithm signatureAlgorithm : this.signatureAlgorithms) {
 			for (SignatureAlgorithm signatureAlgorithm : this.signatureAlgorithms) {
@@ -334,17 +373,18 @@ public final class NimbusJwtDecoder implements JwtDecoder {
 			return new JWSVerificationKeySelector<>(jwsAlgorithms, jwkSource);
 			return new JWSVerificationKeySelector<>(jwsAlgorithms, jwkSource);
 		}
 		}
 
 
-		JWKSource<SecurityContext> jwkSource(ResourceRetriever jwkSetRetriever) {
+		JWKSource<SecurityContext> jwkSource(ResourceRetriever jwkSetRetriever, String jwkSetUri) {
 			if (this.cache == null) {
 			if (this.cache == null) {
-				return new RemoteJWKSet<>(toURL(this.jwkSetUri), jwkSetRetriever);
+				return new RemoteJWKSet<>(toURL(jwkSetUri), jwkSetRetriever);
 			}
 			}
-			JWKSetCache jwkSetCache = new SpringJWKSetCache(this.jwkSetUri, this.cache);
-			return new RemoteJWKSet<>(toURL(this.jwkSetUri), jwkSetRetriever, jwkSetCache);
+			JWKSetCache jwkSetCache = new SpringJWKSetCache(jwkSetUri, this.cache);
+			return new RemoteJWKSet<>(toURL(jwkSetUri), jwkSetRetriever, jwkSetCache);
 		}
 		}
 
 
 		JWTProcessor<SecurityContext> processor() {
 		JWTProcessor<SecurityContext> processor() {
 			ResourceRetriever jwkSetRetriever = new RestOperationsResourceRetriever(this.restOperations);
 			ResourceRetriever jwkSetRetriever = new RestOperationsResourceRetriever(this.restOperations);
-			JWKSource<SecurityContext> jwkSource = jwkSource(jwkSetRetriever);
+			String jwkSetUri = this.jwkSetUri.apply(this.restOperations);
+			JWKSource<SecurityContext> jwkSource = jwkSource(jwkSetRetriever, jwkSetUri);
 			ConfigurableJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
 			ConfigurableJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
 			jwtProcessor.setJWSKeySelector(jwsKeySelector(jwkSource));
 			jwtProcessor.setJWSKeySelector(jwsKeySelector(jwkSource));
 			// Spring Security validates the claim set independent from Nimbus
 			// Spring Security validates the claim set independent from Nimbus

+ 55 - 12
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java

@@ -38,7 +38,6 @@ import com.nimbusds.jose.jwk.JWK;
 import com.nimbusds.jose.jwk.JWKMatcher;
 import com.nimbusds.jose.jwk.JWKMatcher;
 import com.nimbusds.jose.jwk.JWKSelector;
 import com.nimbusds.jose.jwk.JWKSelector;
 import com.nimbusds.jose.jwk.source.JWKSecurityContextJWKSet;
 import com.nimbusds.jose.jwk.source.JWKSecurityContextJWKSet;
-import com.nimbusds.jose.jwk.source.JWKSource;
 import com.nimbusds.jose.proc.BadJOSEException;
 import com.nimbusds.jose.proc.BadJOSEException;
 import com.nimbusds.jose.proc.JWKSecurityContext;
 import com.nimbusds.jose.proc.JWKSecurityContext;
 import com.nimbusds.jose.proc.JWSKeySelector;
 import com.nimbusds.jose.proc.JWSKeySelector;
@@ -211,6 +210,36 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
 		return "Unable to validate Jwt";
 		return "Unable to validate Jwt";
 	}
 	}
 
 
+	/**
+	 * Use the given <a href=
+	 * "https://openid.net/specs/openid-connect-core-1_0.html#IssuerIdentifier">Issuer</a>
+	 * by making an <a href=
+	 * "https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest">OpenID
+	 * Provider Configuration Request</a> and using the values in the <a href=
+	 * "https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse">OpenID
+	 * Provider Configuration Response</a> to derive the needed
+	 * <a href="https://tools.ietf.org/html/rfc7517#section-5">JWK Set</a> uri.
+	 * @param issuer the <a href=
+	 * "https://openid.net/specs/openid-connect-core-1_0.html#IssuerIdentifier">Issuer</a>
+	 * @return a {@link NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder} that will derive the
+	 * JWK Set uri when {@link NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder#build} is
+	 * called
+	 * @since 6.1
+	 * @see JwtDecoders
+	 */
+	public static JwkSetUriReactiveJwtDecoderBuilder withIssuerLocation(String issuer) {
+		return new JwkSetUriReactiveJwtDecoderBuilder((web) -> ReactiveJwtDecoderProviderConfigurationUtils
+				.getConfigurationForIssuerLocation(issuer, web).flatMap((configuration) -> {
+					try {
+						JwtDecoderProviderConfigurationUtils.validateIssuer(configuration, issuer);
+					}
+					catch (IllegalStateException ex) {
+						return Mono.error(ex);
+					}
+					return Mono.just(configuration.get("jwks_uri").toString());
+				}), ReactiveJwtDecoderProviderConfigurationUtils::getJWSAlgorithms);
+	}
+
 	/**
 	/**
 	 * Use the given <a href="https://tools.ietf.org/html/rfc7517#section-5">JWK Set</a>
 	 * Use the given <a href="https://tools.ietf.org/html/rfc7517#section-5">JWK Set</a>
 	 * uri to validate JWTs.
 	 * uri to validate JWTs.
@@ -280,7 +309,10 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
 
 
 		private static final Duration FOREVER = Duration.ofMillis(Long.MAX_VALUE);
 		private static final Duration FOREVER = Duration.ofMillis(Long.MAX_VALUE);
 
 
-		private final String jwkSetUri;
+		private Function<WebClient, Mono<String>> jwkSetUri;
+
+		private Function<ReactiveRemoteJWKSource, Mono<Set<JWSAlgorithm>>> defaultAlgorithms = (source) -> Mono
+				.just(Set.of(JWSAlgorithm.RS256));
 
 
 		private Set<SignatureAlgorithm> signatureAlgorithms = new HashSet<>();
 		private Set<SignatureAlgorithm> signatureAlgorithms = new HashSet<>();
 
 
@@ -290,7 +322,16 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
 
 
 		private JwkSetUriReactiveJwtDecoderBuilder(String jwkSetUri) {
 		private JwkSetUriReactiveJwtDecoderBuilder(String jwkSetUri) {
 			Assert.hasText(jwkSetUri, "jwkSetUri cannot be empty");
 			Assert.hasText(jwkSetUri, "jwkSetUri cannot be empty");
+			this.jwkSetUri = (web) -> Mono.just(jwkSetUri);
+			this.jwtProcessorCustomizer = (source, processor) -> Mono.just(processor);
+		}
+
+		private JwkSetUriReactiveJwtDecoderBuilder(Function<WebClient, Mono<String>> jwkSetUri,
+				Function<ReactiveRemoteJWKSource, Mono<Set<JWSAlgorithm>>> defaultAlgorithms) {
+			Assert.notNull(jwkSetUri, "jwkSetUri cannot be null");
+			Assert.notNull(defaultAlgorithms, "defaultAlgorithms cannot be null");
 			this.jwkSetUri = jwkSetUri;
 			this.jwkSetUri = jwkSetUri;
+			this.defaultAlgorithms = defaultAlgorithms;
 			this.jwtProcessorCustomizer = (source, processor) -> Mono.just(processor);
 			this.jwtProcessorCustomizer = (source, processor) -> Mono.just(processor);
 		}
 		}
 
 
@@ -369,30 +410,32 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
 			return new NimbusReactiveJwtDecoder(processor());
 			return new NimbusReactiveJwtDecoder(processor());
 		}
 		}
 
 
-		JWSKeySelector<JWKSecurityContext> jwsKeySelector(JWKSource<JWKSecurityContext> jwkSource) {
+		Mono<JWSKeySelector<JWKSecurityContext>> jwsKeySelector(ReactiveRemoteJWKSource source) {
+			JWKSecurityContextJWKSet jwkSource = new JWKSecurityContextJWKSet();
 			if (this.signatureAlgorithms.isEmpty()) {
 			if (this.signatureAlgorithms.isEmpty()) {
-				return new JWSVerificationKeySelector<>(JWSAlgorithm.RS256, jwkSource);
+				return this.defaultAlgorithms.apply(source)
+						.map((algorithms) -> new JWSVerificationKeySelector<>(algorithms, jwkSource));
 			}
 			}
 			Set<JWSAlgorithm> jwsAlgorithms = new HashSet<>();
 			Set<JWSAlgorithm> jwsAlgorithms = new HashSet<>();
 			for (SignatureAlgorithm signatureAlgorithm : this.signatureAlgorithms) {
 			for (SignatureAlgorithm signatureAlgorithm : this.signatureAlgorithms) {
 				JWSAlgorithm jwsAlgorithm = JWSAlgorithm.parse(signatureAlgorithm.getName());
 				JWSAlgorithm jwsAlgorithm = JWSAlgorithm.parse(signatureAlgorithm.getName());
 				jwsAlgorithms.add(jwsAlgorithm);
 				jwsAlgorithms.add(jwsAlgorithm);
 			}
 			}
-			return new JWSVerificationKeySelector<>(jwsAlgorithms, jwkSource);
+			return Mono.just(new JWSVerificationKeySelector<>(jwsAlgorithms, jwkSource));
 		}
 		}
 
 
 		Converter<JWT, Mono<JWTClaimsSet>> processor() {
 		Converter<JWT, Mono<JWTClaimsSet>> processor() {
-			JWKSecurityContextJWKSet jwkSource = new JWKSecurityContextJWKSet();
 			DefaultJWTProcessor<JWKSecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
 			DefaultJWTProcessor<JWKSecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
-			JWSKeySelector<JWKSecurityContext> jwsKeySelector = jwsKeySelector(jwkSource);
-			jwtProcessor.setJWSKeySelector(jwsKeySelector);
 			jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> {
 			jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> {
 			});
 			});
-			ReactiveRemoteJWKSource source = new ReactiveRemoteJWKSource(this.jwkSetUri);
+			ReactiveRemoteJWKSource source = new ReactiveRemoteJWKSource(this.jwkSetUri.apply(this.webClient));
 			source.setWebClient(this.webClient);
 			source.setWebClient(this.webClient);
-			Mono<Tuple2<ConfigurableJWTProcessor<JWKSecurityContext>, Function<JWSAlgorithm, Boolean>>> jwtProcessorMono = this.jwtProcessorCustomizer
-					.apply(source, jwtProcessor)
-					.map((processor) -> Tuples.of(processor, getExpectedJwsAlgorithms(processor.getJWSKeySelector())))
+			Mono<JWSKeySelector<JWKSecurityContext>> jwsKeySelector = jwsKeySelector(source);
+			Mono<Tuple2<ConfigurableJWTProcessor<JWKSecurityContext>, Function<JWSAlgorithm, Boolean>>> jwtProcessorMono = jwsKeySelector
+					.flatMap((selector) -> {
+						jwtProcessor.setJWSKeySelector(selector);
+						return this.jwtProcessorCustomizer.apply(source, jwtProcessor);
+					}).map((processor) -> Tuples.of(processor, getExpectedJwsAlgorithms(processor.getJWSKeySelector())))
 					.cache((processor) -> FOREVER, (ex) -> Duration.ZERO, () -> Duration.ZERO);
 					.cache((processor) -> FOREVER, (ex) -> Duration.ZERO, () -> Duration.ZERO);
 			return (jwt) -> {
 			return (jwt) -> {
 				return jwtProcessorMono.flatMap((tuple) -> {
 				return jwtProcessorMono.flatMap((tuple) -> {

+ 66 - 1
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoderProviderConfigurationUtils.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2002-2021 the original author or authors.
+ * Copyright 2002-2023 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -16,7 +16,10 @@
 
 
 package org.springframework.security.oauth2.jwt;
 package org.springframework.security.oauth2.jwt;
 
 
+import java.net.URI;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.HashSet;
+import java.util.Map;
 import java.util.Set;
 import java.util.Set;
 
 
 import com.nimbusds.jose.JWSAlgorithm;
 import com.nimbusds.jose.JWSAlgorithm;
@@ -31,12 +34,24 @@ import com.nimbusds.jose.proc.JWSKeySelector;
 import com.nimbusds.jose.proc.JWSVerificationKeySelector;
 import com.nimbusds.jose.proc.JWSVerificationKeySelector;
 import com.nimbusds.jose.proc.SecurityContext;
 import com.nimbusds.jose.proc.SecurityContext;
 import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
 import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
+import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 import reactor.core.publisher.Mono;
 
 
+import org.springframework.core.ParameterizedTypeReference;
 import org.springframework.util.Assert;
 import org.springframework.util.Assert;
+import org.springframework.web.reactive.function.client.WebClient;
+import org.springframework.web.reactive.function.client.WebClientResponseException;
+import org.springframework.web.util.UriComponentsBuilder;
 
 
 final class ReactiveJwtDecoderProviderConfigurationUtils {
 final class ReactiveJwtDecoderProviderConfigurationUtils {
 
 
+	private static final String OIDC_METADATA_PATH = "/.well-known/openid-configuration";
+
+	private static final String OAUTH_METADATA_PATH = "/.well-known/oauth-authorization-server";
+
+	private static final ParameterizedTypeReference<Map<String, Object>> STRING_OBJECT_MAP = new ParameterizedTypeReference<Map<String, Object>>() {
+	};
+
 	static <C extends SecurityContext> Mono<ConfigurableJWTProcessor<C>> addJWSAlgorithms(
 	static <C extends SecurityContext> Mono<ConfigurableJWTProcessor<C>> addJWSAlgorithms(
 			ReactiveRemoteJWKSource jwkSource, ConfigurableJWTProcessor<C> jwtProcessor) {
 			ReactiveRemoteJWKSource jwkSource, ConfigurableJWTProcessor<C> jwtProcessor) {
 		JWSKeySelector<C> selector = jwtProcessor.getJWSKeySelector();
 		JWSKeySelector<C> selector = jwtProcessor.getJWSKeySelector();
@@ -75,6 +90,56 @@ final class ReactiveJwtDecoderProviderConfigurationUtils {
 		}).onErrorMap(KeySourceException.class, (ex) -> new IllegalStateException(ex));
 		}).onErrorMap(KeySourceException.class, (ex) -> new IllegalStateException(ex));
 	}
 	}
 
 
+	static Mono<Map<String, Object>> getConfigurationForIssuerLocation(String issuer, WebClient web) {
+		URI uri = URI.create(issuer);
+		return getConfiguration(issuer, web, oidc(uri), oidcRfc8414(uri), oauth(uri));
+	}
+
+	private static URI oidc(URI issuer) {
+		// @formatter:off
+		return UriComponentsBuilder.fromUri(issuer)
+				.replacePath(issuer.getPath() + OIDC_METADATA_PATH)
+				.build(Collections.emptyMap());
+		// @formatter:on
+	}
+
+	private static URI oidcRfc8414(URI issuer) {
+		// @formatter:off
+		return UriComponentsBuilder.fromUri(issuer)
+				.replacePath(OIDC_METADATA_PATH + issuer.getPath())
+				.build(Collections.emptyMap());
+		// @formatter:on
+	}
+
+	private static URI oauth(URI issuer) {
+		// @formatter:off
+		return UriComponentsBuilder.fromUri(issuer)
+				.replacePath(OAUTH_METADATA_PATH + issuer.getPath())
+				.build(Collections.emptyMap());
+		// @formatter:on
+	}
+
+	private static Mono<Map<String, Object>> getConfiguration(String issuer, WebClient web, URI... uris) {
+		String errorMessage = "Unable to resolve the Configuration with the provided Issuer of " + "\"" + issuer + "\"";
+		return Flux.just(uris).concatMap((uri) -> web.get().uri(uri).retrieve().bodyToMono(STRING_OBJECT_MAP))
+				.flatMap((configuration) -> {
+					if (configuration.get("jwks_uri") == null) {
+						return Mono
+								.error(() -> new IllegalArgumentException("The public JWK set URI must not be null"));
+					}
+					return Mono.just(configuration);
+				})
+				.onErrorContinue(
+						(ex) -> ex instanceof WebClientResponseException
+								&& ((WebClientResponseException) ex).getStatusCode().is4xxClientError(),
+						(ex, object) -> {
+						})
+				.onErrorMap(RuntimeException.class,
+						(ex) -> (ex instanceof IllegalArgumentException) ? ex
+								: new IllegalArgumentException(errorMessage, ex))
+				.next().switchIfEmpty(Mono.error(() -> new IllegalArgumentException(errorMessage)));
+	}
+
 	private ReactiveJwtDecoderProviderConfigurationUtils() {
 	private ReactiveJwtDecoderProviderConfigurationUtils() {
 	}
 	}
 
 

+ 11 - 6
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveRemoteJWKSource.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2023 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -45,11 +45,16 @@ class ReactiveRemoteJWKSource implements ReactiveJWKSource {
 
 
 	private WebClient webClient = WebClient.create();
 	private WebClient webClient = WebClient.create();
 
 
-	private final String jwkSetURL;
+	private final Mono<String> jwkSetURL;
 
 
 	ReactiveRemoteJWKSource(String jwkSetURL) {
 	ReactiveRemoteJWKSource(String jwkSetURL) {
 		Assert.hasText(jwkSetURL, "jwkSetURL cannot be empty");
 		Assert.hasText(jwkSetURL, "jwkSetURL cannot be empty");
-		this.jwkSetURL = jwkSetURL;
+		this.jwkSetURL = Mono.just(jwkSetURL);
+	}
+
+	ReactiveRemoteJWKSource(Mono<String> jwkSetURL) {
+		Assert.notNull(jwkSetURL, "jwkSetURL cannot be null");
+		this.jwkSetURL = jwkSetURL.cache();
 	}
 	}
 
 
 	@Override
 	@Override
@@ -95,10 +100,10 @@ class ReactiveRemoteJWKSource implements ReactiveJWKSource {
 	 */
 	 */
 	private Mono<JWKSet> getJWKSet() {
 	private Mono<JWKSet> getJWKSet() {
 		// @formatter:off
 		// @formatter:off
-		return this.webClient.get()
-				.uri(this.jwkSetURL)
+		return this.jwkSetURL.flatMap((jwkSetURL) -> this.webClient.get()
+				.uri(jwkSetURL)
 				.retrieve()
 				.retrieve()
-				.bodyToMono(String.class)
+				.bodyToMono(String.class))
 				.map(this::parse)
 				.map(this::parse)
 				.doOnNext((jwkSet) -> this.cachedJWKSet
 				.doOnNext((jwkSet) -> this.cachedJWKSet
 						.set(Mono.just(jwkSet))
 						.set(Mono.just(jwkSet))

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

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2002-2022 the original author or authors.
+ * Copyright 2002-2023 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -61,6 +61,7 @@ import org.mockito.ArgumentCaptor;
 import org.springframework.cache.Cache;
 import org.springframework.cache.Cache;
 import org.springframework.cache.concurrent.ConcurrentMapCache;
 import org.springframework.cache.concurrent.ConcurrentMapCache;
 import org.springframework.cache.support.SimpleValueWrapper;
 import org.springframework.cache.support.SimpleValueWrapper;
+import org.springframework.core.ParameterizedTypeReference;
 import org.springframework.core.convert.converter.Converter;
 import org.springframework.core.convert.converter.Converter;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.MediaType;
 import org.springframework.http.MediaType;
@@ -97,7 +98,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
  */
  */
 public class NimbusJwtDecoderTests {
 public class NimbusJwtDecoderTests {
 
 
-	private static final String JWK_SET = "{\"keys\":[{\"p\":\"49neceJFs8R6n7WamRGy45F5Tv0YM-R2ODK3eSBUSLOSH2tAqjEVKOkLE5fiNA3ygqq15NcKRadB2pTVf-Yb5ZIBuKzko8bzYIkIqYhSh_FAdEEr0vHF5fq_yWSvc6swsOJGqvBEtuqtJY027u-G2gAQasCQdhyejer68zsTn8M\",\"kty\":\"RSA\",\"q\":\"tWR-ysspjZ73B6p2vVRVyHwP3KQWL5KEQcdgcmMOE_P_cPs98vZJfLhxobXVmvzuEWBpRSiqiuyKlQnpstKt94Cy77iO8m8ISfF3C9VyLWXi9HUGAJb99irWABFl3sNDff5K2ODQ8CmuXLYM25OwN3ikbrhEJozlXg_NJFSGD4E\",\"d\":\"FkZHYZlw5KSoqQ1i2RA2kCUygSUOf1OqMt3uomtXuUmqKBm_bY7PCOhmwbvbn4xZYEeHuTR8Xix-0KpHe3NKyWrtRjkq1T_un49_1LLVUhJ0dL-9_x0xRquVjhl_XrsRXaGMEHs8G9pLTvXQ1uST585gxIfmCe0sxPZLvwoic-bXf64UZ9BGRV3lFexWJQqCZp2S21HfoU7wiz6kfLRNi-K4xiVNB1gswm_8o5lRuY7zB9bRARQ3TS2G4eW7p5sxT3CgsGiQD3_wPugU8iDplqAjgJ5ofNJXZezoj0t6JMB_qOpbrmAM1EnomIPebSLW7Ky9SugEd6KMdL5lW6AuAQ\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"one\",\"qi\":\"wdkFu_tV2V1l_PWUUimG516Zvhqk2SWDw1F7uNDD-Lvrv_WNRIJVzuffZ8WYiPy8VvYQPJUrT2EXL8P0ocqwlaSTuXctrORcbjwgxDQDLsiZE0C23HYzgi0cofbScsJdhcBg7d07LAf7cdJWG0YVl1FkMCsxUlZ2wTwHfKWf-v4\",\"dp\":\"uwnPxqC-IxG4r33-SIT02kZC1IqC4aY7PWq0nePiDEQMQWpjjNH50rlq9EyLzbtdRdIouo-jyQXB01K15-XXJJ60dwrGLYNVqfsTd0eGqD1scYJGHUWG9IDgCsxyEnuG3s0AwbW2UolWVSsU2xMZGb9PurIUZECeD1XDZwMp2s0\",\"dq\":\"hra786AunB8TF35h8PpROzPoE9VJJMuLrc6Esm8eZXMwopf0yhxfN2FEAvUoTpLJu93-UH6DKenCgi16gnQ0_zt1qNNIVoRfg4rw_rjmsxCYHTVL3-RDeC8X_7TsEySxW0EgFTHh-nr6I6CQrAJjPM88T35KHtdFATZ7BCBB8AE\",\"n\":\"oXJ8OyOv_eRnce4akdanR4KYRfnC2zLV4uYNQpcFn6oHL0dj7D6kxQmsXoYgJV8ZVDn71KGmuLvolxsDncc2UrhyMBY6DVQVgMSVYaPCTgW76iYEKGgzTEw5IBRQL9w3SRJWd3VJTZZQjkXef48Ocz06PGF3lhbz4t5UEZtdF4rIe7u-977QwHuh7yRPBQ3sII-cVoOUMgaXB9SHcGF2iZCtPzL_IffDUcfhLQteGebhW8A6eUHgpD5A1PQ-JCw_G7UOzZAjjDjtNM2eqm8j-Ms_gqnm4MiCZ4E-9pDN77CAAPVN7kuX6ejs9KBXpk01z48i9fORYk9u7rAkh1HuQw\"}]}";
+	private static final String JWK_SET = "{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"one\",\"n\":\"oXJ8OyOv_eRnce4akdanR4KYRfnC2zLV4uYNQpcFn6oHL0dj7D6kxQmsXoYgJV8ZVDn71KGmuLvolxsDncc2UrhyMBY6DVQVgMSVYaPCTgW76iYEKGgzTEw5IBRQL9w3SRJWd3VJTZZQjkXef48Ocz06PGF3lhbz4t5UEZtdF4rIe7u-977QwHuh7yRPBQ3sII-cVoOUMgaXB9SHcGF2iZCtPzL_IffDUcfhLQteGebhW8A6eUHgpD5A1PQ-JCw_G7UOzZAjjDjtNM2eqm8j-Ms_gqnm4MiCZ4E-9pDN77CAAPVN7kuX6ejs9KBXpk01z48i9fORYk9u7rAkh1HuQw\"}]}";
 
 
 	private static final String NEW_KID_JWK_SET = "{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\",\"kid\":\"two\",\"n\":\"ra9UJw4I0fCHuOqr1xWJsh-qcVeZWtKEU3uoqq1sAg5fG67dujNCm_Q16yuO0ZdDiU0vlJkbc_MXFAvm4ZxdJ_qR7PAneV-BOGNtLpSaiPclscCy3m7zjRWkaqwt9ZZEsdK5UqXyPlBpcYhNKsmnQGjnX4sYb7d8b2jSCM_qto48-6451rbyEhXXywtFy_JqtTpbsw_IIdQHMr1O-MdSjsQxX9kkvZwPU8LsC-CcqlcsZ7mnpOhmIXaf4tbRwAaluXwYft0yykFsp8e5C4t9mMs9Vu8AB5gT8o-D_ovXd2qh4k3ejzVpYLtzD4nbfvPJA_TXmjhn-9GOPAqkzfON2Q\"}]}";
 	private static final String NEW_KID_JWK_SET = "{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\",\"kid\":\"two\",\"n\":\"ra9UJw4I0fCHuOqr1xWJsh-qcVeZWtKEU3uoqq1sAg5fG67dujNCm_Q16yuO0ZdDiU0vlJkbc_MXFAvm4ZxdJ_qR7PAneV-BOGNtLpSaiPclscCy3m7zjRWkaqwt9ZZEsdK5UqXyPlBpcYhNKsmnQGjnX4sYb7d8b2jSCM_qto48-6451rbyEhXXywtFy_JqtTpbsw_IIdQHMr1O-MdSjsQxX9kkvZwPU8LsC-CcqlcsZ7mnpOhmIXaf4tbRwAaluXwYft0yykFsp8e5C4t9mMs9Vu8AB5gT8o-D_ovXd2qh4k3ejzVpYLtzD4nbfvPJA_TXmjhn-9GOPAqkzfON2Q\"}]}";
 
 
@@ -312,6 +313,19 @@ public class NimbusJwtDecoderTests {
 		}
 		}
 	}
 	}
 
 
+	@Test
+	public void decodeWhenIssuerLocationThenOk() {
+		String issuer = "https://example.org/issuer";
+		RestOperations restOperations = mock(RestOperations.class);
+		given(restOperations.exchange(any(RequestEntity.class), any(ParameterizedTypeReference.class))).willReturn(
+				new ResponseEntity<>(Map.of("issuer", issuer, "jwks_uri", issuer + "/jwks"), HttpStatus.OK));
+		given(restOperations.exchange(any(RequestEntity.class), eq(String.class)))
+				.willReturn(new ResponseEntity<>(JWK_SET, HttpStatus.OK));
+		JwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).restOperations(restOperations).build();
+		Jwt jwt = jwtDecoder.decode(SIGNED_JWT);
+		assertThat(jwt.hasClaim(JwtClaimNames.EXP)).isNotNull();
+	}
+
 	@Test
 	@Test
 	public void withJwkSetUriWhenNullOrEmptyThenThrowsException() {
 	public void withJwkSetUriWhenNullOrEmptyThenThrowsException() {
 		// @formatter:off
 		// @formatter:off

+ 25 - 7
oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoderTests.java

@@ -41,7 +41,6 @@ import com.nimbusds.jose.crypto.MACSigner;
 import com.nimbusds.jose.jwk.JWKSet;
 import com.nimbusds.jose.jwk.JWKSet;
 import com.nimbusds.jose.jwk.RSAKey;
 import com.nimbusds.jose.jwk.RSAKey;
 import com.nimbusds.jose.jwk.source.JWKSecurityContextJWKSet;
 import com.nimbusds.jose.jwk.source.JWKSecurityContextJWKSet;
-import com.nimbusds.jose.jwk.source.JWKSource;
 import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier;
 import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier;
 import com.nimbusds.jose.proc.JWKSecurityContext;
 import com.nimbusds.jose.proc.JWKSecurityContext;
 import com.nimbusds.jose.proc.JWSKeySelector;
 import com.nimbusds.jose.proc.JWSKeySelector;
@@ -58,6 +57,7 @@ import org.junit.jupiter.api.Test;
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 import reactor.core.publisher.Mono;
 
 
+import org.springframework.core.ParameterizedTypeReference;
 import org.springframework.core.convert.converter.Converter;
 import org.springframework.core.convert.converter.Converter;
 import org.springframework.security.oauth2.core.OAuth2Error;
 import org.springframework.security.oauth2.core.OAuth2Error;
 import org.springframework.security.oauth2.core.OAuth2TokenValidator;
 import org.springframework.security.oauth2.core.OAuth2TokenValidator;
@@ -597,11 +597,29 @@ public class NimbusReactiveJwtDecoderTests {
 		// @formatter:on
 		// @formatter:on
 	}
 	}
 
 
+	@Test
+	public void decodeWhenIssuerLocationThenOk() {
+		String issuer = "https://example.org/issuer";
+		WebClient real = WebClient.builder().build();
+		WebClient.RequestHeadersUriSpec spec = spy(real.get());
+		WebClient webClient = spy(WebClient.class);
+		given(webClient.get()).willReturn(spec);
+		WebClient.ResponseSpec responseSpec = mock(WebClient.ResponseSpec.class);
+		given(responseSpec.bodyToMono(String.class)).willReturn(Mono.just(this.jwkSet));
+		given(responseSpec.bodyToMono(any(ParameterizedTypeReference.class)))
+				.willReturn(Mono.just(Map.of("issuer", issuer, "jwks_uri", issuer + "/jwks")));
+		given(spec.retrieve()).willReturn(responseSpec);
+		ReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuer).webClient(webClient)
+				.build();
+		Jwt jwt = jwtDecoder.decode(this.messageReadToken).block();
+		assertThat(jwt.hasClaim(JwtClaimNames.EXP)).isNotNull();
+	}
+
 	@Test
 	@Test
 	public void jwsKeySelectorWhenNoAlgorithmThenReturnsRS256Selector() {
 	public void jwsKeySelectorWhenNoAlgorithmThenReturnsRS256Selector() {
-		JWKSource<JWKSecurityContext> jwkSource = mock(JWKSource.class);
+		ReactiveRemoteJWKSource jwkSource = mock(ReactiveRemoteJWKSource.class);
 		JWSKeySelector<JWKSecurityContext> jwsKeySelector = NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
 		JWSKeySelector<JWKSecurityContext> jwsKeySelector = NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
-				.jwsKeySelector(jwkSource);
+				.jwsKeySelector(jwkSource).block();
 		assertThat(jwsKeySelector instanceof JWSVerificationKeySelector);
 		assertThat(jwsKeySelector instanceof JWSVerificationKeySelector);
 		JWSVerificationKeySelector<JWKSecurityContext> jwsVerificationKeySelector = (JWSVerificationKeySelector<JWKSecurityContext>) jwsKeySelector;
 		JWSVerificationKeySelector<JWKSecurityContext> jwsVerificationKeySelector = (JWSVerificationKeySelector<JWKSecurityContext>) jwsKeySelector;
 		assertThat(jwsVerificationKeySelector.isAllowed(JWSAlgorithm.RS256)).isTrue();
 		assertThat(jwsVerificationKeySelector.isAllowed(JWSAlgorithm.RS256)).isTrue();
@@ -609,9 +627,9 @@ public class NimbusReactiveJwtDecoderTests {
 
 
 	@Test
 	@Test
 	public void jwsKeySelectorWhenOneAlgorithmThenReturnsSingleSelector() {
 	public void jwsKeySelectorWhenOneAlgorithmThenReturnsSingleSelector() {
-		JWKSource<JWKSecurityContext> jwkSource = mock(JWKSource.class);
+		ReactiveRemoteJWKSource jwkSource = mock(ReactiveRemoteJWKSource.class);
 		JWSKeySelector<JWKSecurityContext> jwsKeySelector = NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
 		JWSKeySelector<JWKSecurityContext> jwsKeySelector = NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
-				.jwsAlgorithm(SignatureAlgorithm.RS512).jwsKeySelector(jwkSource);
+				.jwsAlgorithm(SignatureAlgorithm.RS512).jwsKeySelector(jwkSource).block();
 		assertThat(jwsKeySelector instanceof JWSVerificationKeySelector);
 		assertThat(jwsKeySelector instanceof JWSVerificationKeySelector);
 		JWSVerificationKeySelector<JWKSecurityContext> jwsVerificationKeySelector = (JWSVerificationKeySelector<JWKSecurityContext>) jwsKeySelector;
 		JWSVerificationKeySelector<JWKSecurityContext> jwsVerificationKeySelector = (JWSVerificationKeySelector<JWKSecurityContext>) jwsKeySelector;
 		assertThat(jwsVerificationKeySelector.isAllowed(JWSAlgorithm.RS512)).isTrue();
 		assertThat(jwsVerificationKeySelector.isAllowed(JWSAlgorithm.RS512)).isTrue();
@@ -619,12 +637,12 @@ public class NimbusReactiveJwtDecoderTests {
 
 
 	@Test
 	@Test
 	public void jwsKeySelectorWhenMultipleAlgorithmThenReturnsCompositeSelector() {
 	public void jwsKeySelectorWhenMultipleAlgorithmThenReturnsCompositeSelector() {
-		JWKSource<JWKSecurityContext> jwkSource = mock(JWKSource.class);
+		ReactiveRemoteJWKSource jwkSource = mock(ReactiveRemoteJWKSource.class);
 		// @formatter:off
 		// @formatter:off
 		JWSKeySelector<JWKSecurityContext> jwsKeySelector = NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
 		JWSKeySelector<JWKSecurityContext> jwsKeySelector = NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
 				.jwsAlgorithm(SignatureAlgorithm.RS256)
 				.jwsAlgorithm(SignatureAlgorithm.RS256)
 				.jwsAlgorithm(SignatureAlgorithm.RS512)
 				.jwsAlgorithm(SignatureAlgorithm.RS512)
-				.jwsKeySelector(jwkSource);
+				.jwsKeySelector(jwkSource).block();
 		// @formatter:on
 		// @formatter:on
 		assertThat(jwsKeySelector instanceof JWSVerificationKeySelector);
 		assertThat(jwsKeySelector instanceof JWSVerificationKeySelector);
 		JWSVerificationKeySelector<?> jwsAlgorithmMapKeySelector = (JWSVerificationKeySelector<?>) jwsKeySelector;
 		JWSVerificationKeySelector<?> jwsAlgorithmMapKeySelector = (JWSVerificationKeySelector<?>) jwsKeySelector;

+ 319 - 0
oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoderProviderConfigurationUtilsTests.java

@@ -0,0 +1,319 @@
+/*
+ * Copyright 2002-2023 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.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import okhttp3.HttpUrl;
+import okhttp3.mockwebserver.Dispatcher;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.web.reactive.function.client.WebClient;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
+
+/**
+ * Tests for {@link ReactiveJwtDecoderProviderConfigurationUtils}
+ *
+ * @author Josh Cummings
+ */
+public class ReactiveJwtDecoderProviderConfigurationUtilsTests {
+
+	/**
+	 * Contains those parameters required to construct a ReactiveJwtDecoder as well as any
+	 * required parameters
+	 */
+	// @formatter:off
+	private static final String DEFAULT_RESPONSE_TEMPLATE = "{\n"
+			+ "    \"authorization_endpoint\": \"https://example.com/o/oauth2/v2/auth\", \n"
+			+ "    \"id_token_signing_alg_values_supported\": [\n"
+			+ "        \"RS256\"\n"
+			+ "    ], \n"
+			+ "    \"issuer\": \"%s\", \n"
+			+ "    \"jwks_uri\": \"%s/.well-known/jwks.json\", \n"
+			+ "    \"response_types_supported\": [\n"
+			+ "        \"code\", \n"
+			+ "        \"token\", \n"
+			+ "        \"id_token\", \n"
+			+ "        \"code token\", \n"
+			+ "        \"code id_token\", \n"
+			+ "        \"token id_token\", \n"
+			+ "        \"code token id_token\", \n"
+			+ "        \"none\"\n"
+			+ "    ], \n"
+			+ "    \"subject_types_supported\": [\n"
+			+ "        \"public\"\n"
+			+ "    ], \n"
+			+ "    \"token_endpoint\": \"https://example.com/oauth2/v4/token\"\n"
+			+ "}";
+	// @formatter:on
+
+	private static final String JWK_SET = "{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"one\",\"n\":\"oXJ8OyOv_eRnce4akdanR4KYRfnC2zLV4uYNQpcFn6oHL0dj7D6kxQmsXoYgJV8ZVDn71KGmuLvolxsDncc2UrhyMBY6DVQVgMSVYaPCTgW76iYEKGgzTEw5IBRQL9w3SRJWd3VJTZZQjkXef48Ocz06PGF3lhbz4t5UEZtdF4rIe7u-977QwHuh7yRPBQ3sII-cVoOUMgaXB9SHcGF2iZCtPzL_IffDUcfhLQteGebhW8A6eUHgpD5A1PQ-JCw_G7UOzZAjjDjtNM2eqm8j-Ms_gqnm4MiCZ4E-9pDN77CAAPVN7kuX6ejs9KBXpk01z48i9fORYk9u7rAkh1HuQw\"}]}";
+
+	private static final String OIDC_METADATA_PATH = "/.well-known/openid-configuration";
+
+	private static final String OAUTH_METADATA_PATH = "/.well-known/oauth-authorization-server";
+
+	private final WebClient web = WebClient.builder().build();
+
+	private MockWebServer server;
+
+	private String issuer;
+
+	@BeforeEach
+	public void setup() throws Exception {
+		this.server = new MockWebServer();
+		this.server.start();
+		this.issuer = createIssuerFromServer();
+		this.issuer += "path";
+	}
+
+	@AfterEach
+	public void cleanup() throws Exception {
+		this.server.shutdown();
+	}
+
+	@Test
+	public void issuerWhenResponseIsTypicalThenReturnedConfigurationContainsJwksUri() {
+		prepareConfigurationResponse();
+		Map<String, Object> configuration = ReactiveJwtDecoderProviderConfigurationUtils
+				.getConfigurationForIssuerLocation(this.issuer, this.web).block();
+		assertThat(configuration).containsKey("jwks_uri");
+	}
+
+	@Test
+	public void issuerWhenOidcFallbackResponseIsTypicalThenReturnedConfigurationContainsJwksUri() {
+		prepareConfigurationResponseOidc();
+		Map<String, Object> configuration = ReactiveJwtDecoderProviderConfigurationUtils
+				.getConfigurationForIssuerLocation(this.issuer, this.web).block();
+		assertThat(configuration).containsKey("jwks_uri");
+	}
+
+	@Test
+	public void issuerWhenOAuth2ResponseIsTypicalThenReturnedConfigurationContainsJwksUri() {
+		prepareConfigurationResponseOAuth2();
+		Map<String, Object> configuration = ReactiveJwtDecoderProviderConfigurationUtils
+				.getConfigurationForIssuerLocation(this.issuer, this.web).block();
+		assertThat(configuration).containsKey("jwks_uri");
+	}
+
+	@Test
+	public void issuerWhenOidcFallbackResponseIsNonCompliantThenThrowsRuntimeException() {
+		prepareConfigurationResponseOidc("{ \"missing_required_keys\" : \"and_values\" }");
+		// @formatter:off
+		assertThatExceptionOfType(RuntimeException.class)
+				.isThrownBy(() -> ReactiveJwtDecoderProviderConfigurationUtils.getConfigurationForIssuerLocation(this.issuer, this.web).block());
+		// @formatter:on
+	}
+
+	@Test
+	public void issuerWhenOAuth2ResponseIsNonCompliantThenThrowsRuntimeException() {
+		prepareConfigurationResponseOAuth2("{ \"missing_required_keys\" : \"and_values\" }");
+		// @formatter:off
+		assertThatExceptionOfType(RuntimeException.class)
+				.isThrownBy(() -> ReactiveJwtDecoderProviderConfigurationUtils.getConfigurationForIssuerLocation(this.issuer, this.web).block());
+		// @formatter:on
+	}
+
+	// gh-7512
+	@Test
+	public void issuerWhenOidcFallbackResponseDoesNotContainJwksUriThenThrowsIllegalArgumentException()
+			throws JsonMappingException, JsonProcessingException {
+		prepareConfigurationResponseOidc(this.buildResponseWithMissingJwksUri());
+		// @formatter:off
+		assertThatIllegalArgumentException()
+				.isThrownBy(() -> ReactiveJwtDecoderProviderConfigurationUtils.getConfigurationForIssuerLocation(this.issuer, this.web).block())
+				.withMessage("The public JWK set URI must not be null");
+		// @formatter:on
+	}
+
+	// gh-7512
+	@Test
+	public void issuerWhenOAuth2ResponseDoesNotContainJwksUriThenThrowsIllegalArgumentException()
+			throws JsonMappingException, JsonProcessingException {
+		prepareConfigurationResponseOAuth2(this.buildResponseWithMissingJwksUri());
+		// @formatter:off
+		assertThatIllegalArgumentException()
+				.isThrownBy(() -> ReactiveJwtDecoderProviderConfigurationUtils.getConfigurationForIssuerLocation(this.issuer, this.web).block())
+				.withMessage("The public JWK set URI must not be null");
+		// @formatter:on
+	}
+
+	@Test
+	public void issuerWhenOidcFallbackResponseIsMalformedThenThrowsRuntimeException() {
+		prepareConfigurationResponseOidc("malformed");
+		// @formatter:off
+		assertThatExceptionOfType(RuntimeException.class)
+				.isThrownBy(() -> ReactiveJwtDecoderProviderConfigurationUtils.getConfigurationForIssuerLocation(this.issuer, this.web).block());
+		// @formatter:on
+	}
+
+	@Test
+	public void issuerWhenOAuth2ResponseIsMalformedThenThrowsRuntimeException() {
+		prepareConfigurationResponseOAuth2("malformed");
+		// @formatter:off
+		assertThatExceptionOfType(RuntimeException.class)
+				.isThrownBy(() -> ReactiveJwtDecoderProviderConfigurationUtils.getConfigurationForIssuerLocation(this.issuer, this.web).block());
+		// @formatter:on
+	}
+
+	@Test
+	public void issuerWhenOidcFallbackRespondingIssuerMismatchesRequestedIssuerThenThrowsIllegalStateException() {
+		prepareConfigurationResponseOidc(String.format(DEFAULT_RESPONSE_TEMPLATE, this.issuer + "/wrong", this.issuer));
+		// @formatter:off
+		assertThatIllegalStateException()
+				.isThrownBy(() -> {
+					Map<String, Object> configuration = ReactiveJwtDecoderProviderConfigurationUtils.getConfigurationForIssuerLocation(this.issuer, this.web).block();
+					JwtDecoderProviderConfigurationUtils.validateIssuer(configuration, this.issuer);
+				});
+		// @formatter:on
+	}
+
+	@Test
+	public void issuerWhenOAuth2RespondingIssuerMismatchesRequestedIssuerThenThrowsIllegalStateException() {
+		prepareConfigurationResponseOAuth2(
+				String.format(DEFAULT_RESPONSE_TEMPLATE, this.issuer + "/wrong", this.issuer));
+		// @formatter:off
+		assertThatIllegalStateException()
+				.isThrownBy(() -> {
+					Map<String, Object> configuration = ReactiveJwtDecoderProviderConfigurationUtils.getConfigurationForIssuerLocation(this.issuer, this.web).block();
+					JwtDecoderProviderConfigurationUtils.validateIssuer(configuration, this.issuer);
+				});
+		// @formatter:on
+	}
+
+	@Test
+	public void issuerWhenOidcFallbackRequestedIssuerIsUnresponsiveThenThrowsIllegalArgumentException()
+			throws Exception {
+		this.server.shutdown();
+		// @formatter:off
+		assertThatIllegalArgumentException()
+				.isThrownBy(() -> ReactiveJwtDecoderProviderConfigurationUtils.getConfigurationForIssuerLocation("https://issuer", this.web).block());
+		// @formatter:on
+	}
+
+	private void prepareConfigurationResponse() {
+		String body = String.format(DEFAULT_RESPONSE_TEMPLATE, this.issuer, this.issuer);
+		prepareConfigurationResponse(body);
+	}
+
+	private void prepareConfigurationResponse(String body) {
+		this.server.enqueue(response(body));
+		this.server.enqueue(response(JWK_SET));
+	}
+
+	private void prepareConfigurationResponseOidc() {
+		String body = String.format(DEFAULT_RESPONSE_TEMPLATE, this.issuer, this.issuer);
+		prepareConfigurationResponseOidc(body);
+	}
+
+	private void prepareConfigurationResponseOidc(String body) {
+		Map<String, MockResponse> responses = new HashMap<>();
+		responses.put(oidc(), response(body));
+		responses.put(jwks(), response(JWK_SET));
+		prepareConfigurationResponses(responses);
+	}
+
+	private void prepareConfigurationResponseOAuth2() {
+		String body = String.format(DEFAULT_RESPONSE_TEMPLATE, this.issuer, this.issuer);
+		prepareConfigurationResponseOAuth2(body);
+	}
+
+	private void prepareConfigurationResponseOAuth2(String body) {
+		Map<String, MockResponse> responses = new HashMap<>();
+		responses.put(oauth(), response(body));
+		responses.put(jwks(), response(JWK_SET));
+		prepareConfigurationResponses(responses);
+	}
+
+	private void prepareConfigurationResponses(Map<String, MockResponse> responses) {
+		Dispatcher dispatcher = new Dispatcher() {
+			@Override
+			public MockResponse dispatch(RecordedRequest request) {
+				// @formatter:off
+				return Optional.of(request)
+						.map(RecordedRequest::getRequestUrl)
+						.map(HttpUrl::toString)
+						.map(responses::get)
+						.orElse(new MockResponse().setResponseCode(404));
+				// @formatter:on
+			}
+		};
+		this.server.setDispatcher(dispatcher);
+	}
+
+	private String createIssuerFromServer() {
+		return this.server.url("").toString();
+	}
+
+	private String oidc() {
+		URI uri = URI.create(this.issuer);
+		// @formatter:off
+		return UriComponentsBuilder.fromUri(uri)
+				.replacePath(uri.getPath() + OIDC_METADATA_PATH)
+				.toUriString();
+		// @formatter:on
+	}
+
+	private String oauth() {
+		URI uri = URI.create(this.issuer);
+		// @formatter:off
+		return UriComponentsBuilder.fromUri(uri)
+				.replacePath(OAUTH_METADATA_PATH + uri.getPath())
+				.toUriString();
+		// @formatter:on
+	}
+
+	private String jwks() {
+		return this.issuer + "/.well-known/jwks.json";
+	}
+
+	private MockResponse response(String body) {
+		// @formatter:off
+		return new MockResponse().setBody(body)
+				.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
+		// @formatter:on
+	}
+
+	public String buildResponseWithMissingJwksUri() throws JsonMappingException, JsonProcessingException {
+		ObjectMapper mapper = new ObjectMapper();
+		Map<String, Object> response = mapper.readValue(DEFAULT_RESPONSE_TEMPLATE,
+				new TypeReference<Map<String, Object>>() {
+				});
+		response.remove("jwks_uri");
+		return mapper.writeValueAsString(response);
+	}
+
+}