diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolver.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolver.java index af1cddc205d..eb21703fc2d 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolver.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolver.java @@ -65,7 +65,7 @@ public final class JwtIssuerAuthenticationManagerResolver implements Authenticat private final AuthenticationManagerResolver issuerAuthenticationManagerResolver; - private final Converter issuerConverter = new JwtClaimIssuerConverter(); + private Converter issuerConverter; /** * Construct a {@link JwtIssuerAuthenticationManagerResolver} using the provided @@ -78,6 +78,7 @@ public JwtIssuerAuthenticationManagerResolver(String... trustedIssuers) { /** * Construct a {@link JwtIssuerAuthenticationManagerResolver} using the provided + * * parameters * @param trustedIssuers a list of trusted issuers */ @@ -85,6 +86,7 @@ public JwtIssuerAuthenticationManagerResolver(Collection trustedIssuers) Assert.notEmpty(trustedIssuers, "trustedIssuers cannot be empty"); this.issuerAuthenticationManagerResolver = new TrustedIssuerJwtAuthenticationManagerResolver( Collections.unmodifiableCollection(trustedIssuers)::contains); + this.issuerConverter = new JwtClaimIssuerConverter(); } /** @@ -110,8 +112,41 @@ public JwtIssuerAuthenticationManagerResolver(Collection trustedIssuers) */ public JwtIssuerAuthenticationManagerResolver( AuthenticationManagerResolver issuerAuthenticationManagerResolver) { + this(issuerAuthenticationManagerResolver, new JwtClaimIssuerConverter()); + } + + /** + * Construct a {@link JwtIssuerAuthenticationManagerResolver} using the provided + * parameters + * + * Note that the {@link AuthenticationManagerResolver} provided in this constructor + * will need to verify that the issuer is trusted. This should be done via an + * allowlist. + * + * One way to achieve this is with a {@link Map} where the keys are the known issuers: + *
+	 *     Map<String, AuthenticationManager> authenticationManagers = new HashMap<>();
+	 *     authenticationManagers.put("https://issuerOne.example.org", managerOne);
+	 *     authenticationManagers.put("https://issuerTwo.example.org", managerTwo);
+	 *     JwtAuthenticationManagerResolver resolver = new JwtAuthenticationManagerResolver
+	 *     	(authenticationManagers::get);
+	 * 
+ * + * The keys in the {@link Map} are the allowed issuers. + * @param issuerAuthenticationManagerResolver a strategy for resolving the + * {@link AuthenticationManager} by the issuer + * @param issuerConverter a custom converter to resolve the token A custom converter + * allows to use a custom {@link BearerTokenResolver} + * + * @since 5.4 + */ + public JwtIssuerAuthenticationManagerResolver( + AuthenticationManagerResolver issuerAuthenticationManagerResolver, + Converter issuerConverter) { Assert.notNull(issuerAuthenticationManagerResolver, "issuerAuthenticationManagerResolver cannot be null"); + Assert.notNull(issuerConverter, "issuerConverter cannot be null"); this.issuerAuthenticationManagerResolver = issuerAuthenticationManagerResolver; + this.issuerConverter = issuerConverter; } /** @@ -130,6 +165,10 @@ public AuthenticationManager resolve(HttpServletRequest request) { return authenticationManager; } + public void setIssuerConverter(Converter issuerConverter) { + this.issuerConverter = issuerConverter; + } + private static class JwtClaimIssuerConverter implements Converter { private final BearerTokenResolver resolver = new DefaultBearerTokenResolver(); @@ -160,8 +199,16 @@ private static class TrustedIssuerJwtAuthenticationManagerResolver private final Predicate trustedIssuer; + private final JwtAuthenticationConverter jwtAuthenticationConverter; + TrustedIssuerJwtAuthenticationManagerResolver(Predicate trustedIssuer) { + this(trustedIssuer, null); + } + + TrustedIssuerJwtAuthenticationManagerResolver(Predicate trustedIssuer, + JwtAuthenticationConverter jwtAuthenticationConverter) { this.trustedIssuer = trustedIssuer; + this.jwtAuthenticationConverter = jwtAuthenticationConverter; } @Override @@ -171,7 +218,18 @@ public AuthenticationManager resolve(String issuer) { (k) -> { this.logger.debug("Constructing AuthenticationManager"); JwtDecoder jwtDecoder = JwtDecoders.fromIssuerLocation(issuer); - return new JwtAuthenticationProvider(jwtDecoder)::authenticate; + if (this.jwtAuthenticationConverter != null) { + this.logger.debug(("Using custom JwtAuthenticationConverter")); + final JwtAuthenticationProvider jwtAuthenticationProvider = new JwtAuthenticationProvider( + jwtDecoder); + jwtAuthenticationProvider + .setJwtAuthenticationConverter(this.jwtAuthenticationConverter); + return jwtAuthenticationProvider::authenticate; + } + else { + this.logger.debug(("Using default JwtAuthenticationConverter")); + return new JwtAuthenticationProvider(jwtDecoder)::authenticate; + } }); this.logger.debug(LogMessage.format("Resolved AuthenticationManager for issuer '%s'", issuer)); return authenticationManager; diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolver.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolver.java index e73635e887d..239e21ff5a5 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolver.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolver.java @@ -65,7 +65,7 @@ public final class JwtIssuerReactiveAuthenticationManagerResolver private final ReactiveAuthenticationManagerResolver issuerAuthenticationManagerResolver; - private final Converter> issuerConverter = new JwtClaimIssuerConverter(); + private Converter> issuerConverter; /** * Construct a {@link JwtIssuerReactiveAuthenticationManagerResolver} using the @@ -85,6 +85,7 @@ public JwtIssuerReactiveAuthenticationManagerResolver(Collection trusted Assert.notEmpty(trustedIssuers, "trustedIssuers cannot be empty"); this.issuerAuthenticationManagerResolver = new TrustedIssuerJwtAuthenticationManagerResolver( new ArrayList<>(trustedIssuers)::contains); + this.issuerConverter = new JwtClaimIssuerConverter(); } /** @@ -110,8 +111,39 @@ public JwtIssuerReactiveAuthenticationManagerResolver(Collection trusted */ public JwtIssuerReactiveAuthenticationManagerResolver( ReactiveAuthenticationManagerResolver issuerAuthenticationManagerResolver) { + this(issuerAuthenticationManagerResolver, new JwtClaimIssuerConverter()); + } + + /** + * Construct a {@link JwtIssuerReactiveAuthenticationManagerResolver} using the + * provided parameters + * + * Note that the {@link ReactiveAuthenticationManagerResolver} provided in this + * constructor will need to verify that the issuer is trusted. This should be done via + * an allowed list of issuers. + * + * One way to achieve this is with a {@link Map} where the keys are the known issuers: + *
+	 *     Map<String, ReactiveAuthenticationManager> authenticationManagers = new HashMap<>();
+	 *     authenticationManagers.put("https://issuerOne.example.org", managerOne);
+	 *     authenticationManagers.put("https://issuerTwo.example.org", managerTwo);
+	 *     JwtIssuerReactiveAuthenticationManagerResolver resolver = new JwtIssuerReactiveAuthenticationManagerResolver
+	 *     	((issuer) -> Mono.justOrEmpty(authenticationManagers.get(issuer));
+	 * 
+ * + * The keys in the {@link Map} are the trusted issuers. + * @param issuerAuthenticationManagerResolver a strategy for resolving the + * {@link ReactiveAuthenticationManager} by the issuer + * @param issuerConverter a custom converter to resolve the token A custom converter + * allows to use a custom {@link ServerBearerTokenAuthenticationConverter} + */ + public JwtIssuerReactiveAuthenticationManagerResolver( + ReactiveAuthenticationManagerResolver issuerAuthenticationManagerResolver, + Converter> issuerConverter) { Assert.notNull(issuerAuthenticationManagerResolver, "issuerAuthenticationManagerResolver cannot be null"); + Assert.notNull(issuerConverter, "issuerConverter cannot be null"); this.issuerAuthenticationManagerResolver = issuerAuthenticationManagerResolver; + this.issuerConverter = new JwtClaimIssuerConverter(); } /** @@ -131,6 +163,10 @@ public Mono resolve(ServerWebExchange exchange) { // @formatter:on } + public void setIssuerConverter(Converter> issuerConverter) { + this.issuerConverter = issuerConverter; + } + private static class JwtClaimIssuerConverter implements Converter> { private final ServerBearerTokenAuthenticationConverter converter = new ServerBearerTokenAuthenticationConverter(); @@ -161,8 +197,16 @@ private static class TrustedIssuerJwtAuthenticationManagerResolver private final Predicate trustedIssuer; + private final ReactiveJwtAuthenticationConverterAdapter jwtAuthenticationConverterAdapter; + TrustedIssuerJwtAuthenticationManagerResolver(Predicate trustedIssuer) { + this(trustedIssuer, null); + } + + TrustedIssuerJwtAuthenticationManagerResolver(Predicate trustedIssuer, + ReactiveJwtAuthenticationConverterAdapter jwtAuthenticationConverterAdapter) { this.trustedIssuer = trustedIssuer; + this.jwtAuthenticationConverterAdapter = jwtAuthenticationConverterAdapter; } @Override @@ -172,9 +216,19 @@ public Mono resolve(String issuer) { } // @formatter:off return this.authenticationManagers.computeIfAbsent(issuer, - (k) -> Mono.fromCallable(() -> new JwtReactiveAuthenticationManager(ReactiveJwtDecoders.fromIssuerLocation(k))) - .subscribeOn(Schedulers.boundedElastic()) - .cache() + (k) -> Mono.fromCallable(() -> { + if (this.jwtAuthenticationConverterAdapter != null) { + final JwtReactiveAuthenticationManager authenticationManager = + new JwtReactiveAuthenticationManager(ReactiveJwtDecoders.fromIssuerLocation(k)); + authenticationManager.setJwtAuthenticationConverter(this.jwtAuthenticationConverterAdapter); + return authenticationManager; + } + else { + return new JwtReactiveAuthenticationManager(ReactiveJwtDecoders.fromIssuerLocation(k)); + } + }) + .subscribeOn(Schedulers.boundedElastic()) + .cache() ); // @formatter:on } diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolverTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolverTests.java index 354c5511058..73689179db3 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolverTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolverTests.java @@ -33,12 +33,16 @@ import okhttp3.mockwebserver.MockWebServer; import org.junit.Test; +import org.springframework.core.convert.converter.Converter; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManagerResolver; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.jose.TestKeys; import org.springframework.security.oauth2.jwt.JwtClaimNames; +import org.springframework.security.oauth2.jwt.JwtDecoders; + +import javax.servlet.http.HttpServletRequest; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -107,6 +111,18 @@ public void resolveWhenUsingCustomIssuerAuthenticationManagerResolverThenUses() assertThat(authenticationManagerResolver.resolve(request)).isSameAs(authenticationManager); } + @Test + public void resolveWhenUsingCustomIssuerAuthenticationManagerResolverAndCustomIssuerConverterThenUses() { + AuthenticationManager authenticationManager = mock(AuthenticationManager.class); + Converter jwtAuthConverter = (Converter) mock( + Converter.class); + JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver( + (issuer) -> authenticationManager, jwtAuthConverter); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", "Bearer " + this.jwt); + assertThat(authenticationManagerResolver.resolve(request)).isSameAs(authenticationManager); + } + @Test public void resolveWhenUsingExternalSourceThenRespondsToChanges() { MockHttpServletRequest request = new MockHttpServletRequest(); @@ -185,6 +201,13 @@ public void constructorWhenNullAuthenticationManagerResolverThenException() { .isThrownBy(() -> new JwtIssuerAuthenticationManagerResolver((AuthenticationManagerResolver) null)); } + @Test + public void constructWhenNullIssuerConverterThenException() { + assertThatIllegalArgumentException().isThrownBy(() -> new JwtIssuerAuthenticationManagerResolver( + context -> new JwtAuthenticationProvider(JwtDecoders.fromIssuerLocation("trusted"))::authenticate, + null)); + } + private String jwt(String claim, String value) { PlainJWT jwt = new PlainJWT(new JWTClaimsSet.Builder().claim(claim, value).build()); return jwt.serialize(); diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolverTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolverTests.java index a794aa1fdeb..c5e40fcbbeb 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolverTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolverTests.java @@ -42,6 +42,10 @@ import org.springframework.security.oauth2.jose.TestKeys; import org.springframework.security.oauth2.jwt.JwtClaimNames; +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.oauth2.jwt.ReactiveJwtDecoders; +import org.springframework.web.server.ServerWebExchange; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -107,6 +111,17 @@ public void resolveWhenUsingCustomIssuerAuthenticationManagerResolverThenUses() assertThat(authenticationManagerResolver.resolve(exchange).block()).isSameAs(authenticationManager); } + @Test + public void resolveWhenUsingCustomIssuerAuthenticationManagerResolverAndCustomIssuerConverterThenUses() { + ReactiveAuthenticationManager authenticationManager = mock(ReactiveAuthenticationManager.class); + Converter> jwtAuthConverter = (Converter>) mock( + Converter.class); + JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver( + (issuer) -> Mono.just(authenticationManager), jwtAuthConverter); + MockServerWebExchange exchange = withBearerToken(this.jwt); + assertThat(authenticationManagerResolver.resolve(exchange).block()).isSameAs(authenticationManager); + } + @Test public void resolveWhenUsingExternalSourceThenRespondsToChanges() { MockServerWebExchange exchange = withBearerToken(this.jwt); @@ -175,6 +190,14 @@ public void constructorWhenNullAuthenticationManagerResolverThenException() { () -> new JwtIssuerReactiveAuthenticationManagerResolver((ReactiveAuthenticationManagerResolver) null)); } + @Test + public void constructWhenNullIssuerConverterThenException() { + assertThatIllegalArgumentException().isThrownBy(() -> new JwtIssuerReactiveAuthenticationManagerResolver( + (context) -> Mono + .just(new JwtReactiveAuthenticationManager(ReactiveJwtDecoders.fromIssuerLocation("trusted"))), + null)); + } + private String jwt(String claim, String value) { PlainJWT jwt = new PlainJWT(new JWTClaimsSet.Builder().claim(claim, value).build()); return jwt.serialize();