Skip to content

Allow for customization of IssuerResolver #9005

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public final class JwtIssuerAuthenticationManagerResolver implements Authenticat

private final AuthenticationManagerResolver<String> issuerAuthenticationManagerResolver;

private final Converter<HttpServletRequest, String> issuerConverter = new JwtClaimIssuerConverter();
private Converter<HttpServletRequest, String> issuerConverter;

/**
* Construct a {@link JwtIssuerAuthenticationManagerResolver} using the provided
Expand All @@ -78,13 +78,15 @@ public JwtIssuerAuthenticationManagerResolver(String... trustedIssuers) {

/**
* Construct a {@link JwtIssuerAuthenticationManagerResolver} using the provided
*
* parameters
* @param trustedIssuers a list of trusted issuers
*/
public JwtIssuerAuthenticationManagerResolver(Collection<String> trustedIssuers) {
Assert.notEmpty(trustedIssuers, "trustedIssuers cannot be empty");
this.issuerAuthenticationManagerResolver = new TrustedIssuerJwtAuthenticationManagerResolver(
Collections.unmodifiableCollection(trustedIssuers)::contains);
this.issuerConverter = new JwtClaimIssuerConverter();
}

/**
Expand All @@ -110,8 +112,41 @@ public JwtIssuerAuthenticationManagerResolver(Collection<String> trustedIssuers)
*/
public JwtIssuerAuthenticationManagerResolver(
AuthenticationManagerResolver<String> 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:
* <pre>
* Map&lt;String, AuthenticationManager&gt; authenticationManagers = new HashMap&lt;&gt;();
* authenticationManagers.put("https://issuerOne.example.org", managerOne);
* authenticationManagers.put("https://issuerTwo.example.org", managerTwo);
* JwtAuthenticationManagerResolver resolver = new JwtAuthenticationManagerResolver
* (authenticationManagers::get);
* </pre>
*
* 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<String> issuerAuthenticationManagerResolver,
Converter<HttpServletRequest, String> issuerConverter) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we are using a setter for the issuer converter, this constructor is unnecessary.

Assert.notNull(issuerAuthenticationManagerResolver, "issuerAuthenticationManagerResolver cannot be null");
Assert.notNull(issuerConverter, "issuerConverter cannot be null");
this.issuerAuthenticationManagerResolver = issuerAuthenticationManagerResolver;
this.issuerConverter = issuerConverter;
}

/**
Expand All @@ -130,6 +165,10 @@ public AuthenticationManager resolve(HttpServletRequest request) {
return authenticationManager;
}

public void setIssuerConverter(Converter<HttpServletRequest, String> issuerConverter) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add JavaDoc, including a @since 5.5 so it's clear when the method became available.

this.issuerConverter = issuerConverter;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check for null here using Assert.notNull, as you did earlier in the constructor.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I believe it would be a bit more consistent with the rest of Spring Security to call this setIssuerResolver. It's certainly reminiscent of BearerTokenResolver in its role in the request.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, out of curiosity, do you have a use case where you need the more generic contract of Converter<HttpServletRequest, String>? If not, setBearerTokenResolver may be better since that's likely the most common use case.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jzheaux the problem is that you cannot set the BearerTokenResolver on the issuerConverter as it is of type Converter<HttpServletRequest,String>. So either you replace the entire thing or you would need to add a setter on that one as well and change the type of the issuerConverter attribute directly to JwtClaimIssuerConverter. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with you that adding a setter to JwtClaimIssuerConverter would be one way to do it, though it would remain private.

One nice thing about using BearerTokenResolver is that it simplifies the API and focuses on the most common use cases. It seems like a very uncommon use case to pull the issuer from somewhere else in the request other than the token itself.

}

private static class JwtClaimIssuerConverter implements Converter<HttpServletRequest, String> {

private final BearerTokenResolver resolver = new DefaultBearerTokenResolver();
Expand Down Expand Up @@ -160,8 +199,16 @@ private static class TrustedIssuerJwtAuthenticationManagerResolver

private final Predicate<String> trustedIssuer;

private final JwtAuthenticationConverter jwtAuthenticationConverter;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's leave these changes regarding JwtAuthenticationConverter for later.


TrustedIssuerJwtAuthenticationManagerResolver(Predicate<String> trustedIssuer) {
this(trustedIssuer, null);
}

TrustedIssuerJwtAuthenticationManagerResolver(Predicate<String> trustedIssuer,
JwtAuthenticationConverter jwtAuthenticationConverter) {
this.trustedIssuer = trustedIssuer;
this.jwtAuthenticationConverter = jwtAuthenticationConverter;
}

@Override
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public final class JwtIssuerReactiveAuthenticationManagerResolver

private final ReactiveAuthenticationManagerResolver<String> issuerAuthenticationManagerResolver;

private final Converter<ServerWebExchange, Mono<String>> issuerConverter = new JwtClaimIssuerConverter();
private Converter<ServerWebExchange, Mono<String>> issuerConverter;

/**
* Construct a {@link JwtIssuerReactiveAuthenticationManagerResolver} using the
Expand All @@ -85,6 +85,7 @@ public JwtIssuerReactiveAuthenticationManagerResolver(Collection<String> trusted
Assert.notEmpty(trustedIssuers, "trustedIssuers cannot be empty");
this.issuerAuthenticationManagerResolver = new TrustedIssuerJwtAuthenticationManagerResolver(
new ArrayList<>(trustedIssuers)::contains);
this.issuerConverter = new JwtClaimIssuerConverter();
}

/**
Expand All @@ -110,8 +111,39 @@ public JwtIssuerReactiveAuthenticationManagerResolver(Collection<String> trusted
*/
public JwtIssuerReactiveAuthenticationManagerResolver(
ReactiveAuthenticationManagerResolver<String> 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:
* <pre>
* Map&lt;String, ReactiveAuthenticationManager&gt; authenticationManagers = new HashMap&lt;&gt;();
* authenticationManagers.put("https://issuerOne.example.org", managerOne);
* authenticationManagers.put("https://issuerTwo.example.org", managerTwo);
* JwtIssuerReactiveAuthenticationManagerResolver resolver = new JwtIssuerReactiveAuthenticationManagerResolver
* ((issuer) -> Mono.justOrEmpty(authenticationManagers.get(issuer));
* </pre>
*
* 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<String> issuerAuthenticationManagerResolver,
Converter<ServerWebExchange, Mono<String>> issuerConverter) {
Assert.notNull(issuerAuthenticationManagerResolver, "issuerAuthenticationManagerResolver cannot be null");
Assert.notNull(issuerConverter, "issuerConverter cannot be null");
this.issuerAuthenticationManagerResolver = issuerAuthenticationManagerResolver;
this.issuerConverter = new JwtClaimIssuerConverter();
}

/**
Expand All @@ -131,6 +163,10 @@ public Mono<ReactiveAuthenticationManager> resolve(ServerWebExchange exchange) {
// @formatter:on
}

public void setIssuerConverter(Converter<ServerWebExchange, Mono<String>> issuerConverter) {
this.issuerConverter = issuerConverter;
}

private static class JwtClaimIssuerConverter implements Converter<ServerWebExchange, Mono<String>> {

private final ServerBearerTokenAuthenticationConverter converter = new ServerBearerTokenAuthenticationConverter();
Expand Down Expand Up @@ -161,8 +197,16 @@ private static class TrustedIssuerJwtAuthenticationManagerResolver

private final Predicate<String> trustedIssuer;

private final ReactiveJwtAuthenticationConverterAdapter jwtAuthenticationConverterAdapter;

TrustedIssuerJwtAuthenticationManagerResolver(Predicate<String> trustedIssuer) {
this(trustedIssuer, null);
}

TrustedIssuerJwtAuthenticationManagerResolver(Predicate<String> trustedIssuer,
ReactiveJwtAuthenticationConverterAdapter jwtAuthenticationConverterAdapter) {
this.trustedIssuer = trustedIssuer;
this.jwtAuthenticationConverterAdapter = jwtAuthenticationConverterAdapter;
}

@Override
Expand All @@ -172,9 +216,19 @@ public Mono<ReactiveAuthenticationManager> resolve(String issuer) {
}
// @formatter:off
return this.authenticationManagers.computeIfAbsent(issuer,
(k) -> Mono.<ReactiveAuthenticationManager>fromCallable(() -> new JwtReactiveAuthenticationManager(ReactiveJwtDecoders.fromIssuerLocation(k)))
.subscribeOn(Schedulers.boundedElastic())
.cache()
(k) -> Mono.<ReactiveAuthenticationManager>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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -107,6 +111,18 @@ public void resolveWhenUsingCustomIssuerAuthenticationManagerResolverThenUses()
assertThat(authenticationManagerResolver.resolve(request)).isSameAs(authenticationManager);
}

@Test
public void resolveWhenUsingCustomIssuerAuthenticationManagerResolverAndCustomIssuerConverterThenUses() {
AuthenticationManager authenticationManager = mock(AuthenticationManager.class);
Converter<HttpServletRequest, String> jwtAuthConverter = (Converter<HttpServletRequest, String>) 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();
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -107,6 +111,17 @@ public void resolveWhenUsingCustomIssuerAuthenticationManagerResolverThenUses()
assertThat(authenticationManagerResolver.resolve(exchange).block()).isSameAs(authenticationManager);
}

@Test
public void resolveWhenUsingCustomIssuerAuthenticationManagerResolverAndCustomIssuerConverterThenUses() {
ReactiveAuthenticationManager authenticationManager = mock(ReactiveAuthenticationManager.class);
Converter<ServerWebExchange, Mono<String>> jwtAuthConverter = (Converter<ServerWebExchange, Mono<String>>) 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);
Expand Down Expand Up @@ -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();
Expand Down