Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 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 final Converter<HttpServletRequest, String> issuerConverter;

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

/**
* Construct a {@link JwtIssuerAuthenticationManagerResolver} with a custom
* {@link JwtAuthenticationConverter} using the provided parameters
*
* A custom {@link JwtAuthenticationConverter} allows to use a custom
* {@link Converter} (much like {@link JwtGrantedAuthoritiesConverter}) to handle an
* untypical JWT token
* @param trustedIssuers a list of trusted issuers
* @param jwtAuthenticationConverter a custom {@link JwtAuthenticationConverter}
* @since 5.4
*/
public JwtIssuerAuthenticationManagerResolver(Collection<String> trustedIssuers,
JwtAuthenticationConverter jwtAuthenticationConverter) {
Assert.notEmpty(trustedIssuers, "trustedIssuers cannot be empty");
Assert.notNull(jwtAuthenticationConverter, "jwtAuthenticationConverter cannot be null");
this.issuerAuthenticationManagerResolver = new TrustedIssuerJwtAuthenticationManagerResolver(
Collections.unmodifiableCollection(trustedIssuers)::contains, jwtAuthenticationConverter);
this.issuerConverter = new JwtClaimIssuerConverter();
}

/**
Expand All @@ -110,8 +131,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 Down Expand Up @@ -160,8 +214,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 +233,17 @@ public AuthenticationManager resolve(String issuer) {
(k) -> {
this.logger.debug("Constructing AuthenticationManager");
JwtDecoder jwtDecoder = JwtDecoders.fromIssuerLocation(issuer);
return new JwtAuthenticationProvider(jwtDecoder)::authenticate;
if (jwtAuthenticationConverter != null) {
this.logger.debug(("Using custom JwtAuthenticationConverter"));
final JwtAuthenticationProvider jwtAuthenticationProvider = new JwtAuthenticationProvider(
jwtDecoder);
jwtAuthenticationProvider.setJwtAuthenticationConverter(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 final Converter<ServerWebExchange, Mono<String>> issuerConverter;

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

/**
* Construct a {@link JwtIssuerReactiveAuthenticationManagerResolver} using the
* provided parameters
*
* A custom {@link ReactiveJwtAuthenticationConverterAdapter} allows to use a custom
* {@link Converter} (much like {@link JwtGrantedAuthoritiesConverter}) to handle an
* untypical JWT token
* @param trustedIssuers a collection of trusted issuers
* @param reactiveJwtAuthenticationConverterAdapter a custom
* {@link ReactiveJwtAuthenticationConverterAdapter}
*/
public JwtIssuerReactiveAuthenticationManagerResolver(Collection<String> trustedIssuers,
ReactiveJwtAuthenticationConverterAdapter reactiveJwtAuthenticationConverterAdapter) {
Assert.notEmpty(trustedIssuers, "trustedIssuers cannot be empty");
this.issuerAuthenticationManagerResolver = new TrustedIssuerJwtAuthenticationManagerResolver(
new ArrayList<>(trustedIssuers)::contains, reactiveJwtAuthenticationConverterAdapter);
this.issuerConverter = new JwtClaimIssuerConverter();
}

/**
Expand All @@ -110,8 +130,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 Down Expand Up @@ -161,8 +212,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 +231,18 @@ 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(jwtAuthenticationConverterAdapter != null) {
final JwtReactiveAuthenticationManager authenticationManager =
new JwtReactiveAuthenticationManager(ReactiveJwtDecoders.fromIssuerLocation(k));
authenticationManager.setJwtAuthenticationConverter(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 @@ -84,6 +88,32 @@ public void resolveWhenUsingTrustedIssuerThenReturnsAuthenticationManager() thro
}
}

@Test
public void resolveWhenUsingTrustedIssuerAndCustomJwtAuthConverterThenReturnsAuthenticationManager()
throws Exception {
try (MockWebServer server = new MockWebServer()) {
server.start();
String issuer = server.url("").toString();
// @formatter:off
server.enqueue(new MockResponse().setResponseCode(200)
.setHeader("Content-Type", "application/json")
.setBody(String.format(DEFAULT_RESPONSE_TEMPLATE, issuer, issuer)
));
// @formatter:on
JWSObject jws = new JWSObject(new JWSHeader(JWSAlgorithm.RS256),
new Payload(new JSONObject(Collections.singletonMap(JwtClaimNames.ISS, issuer))));
jws.sign(new RSASSASigner(TestKeys.DEFAULT_PRIVATE_KEY));
JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver(
Collections.singletonList(issuer), new JwtAuthenticationConverter());
MockHttpServletRequest request = new MockHttpServletRequest();
request.addHeader("Authorization", "Bearer " + jws.serialize());
AuthenticationManager authenticationManager = authenticationManagerResolver.resolve(request);
assertThat(authenticationManager).isNotNull();
AuthenticationManager cachedAuthenticationManager = authenticationManagerResolver.resolve(request);
assertThat(authenticationManager).isSameAs(cachedAuthenticationManager);
}
}

@Test
public void resolveWhenUsingUntrustedIssuerThenException() {
JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver(
Expand All @@ -107,6 +137,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 +227,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 @@ -32,6 +32,9 @@
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.Test;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoders;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
Expand Down Expand Up @@ -86,6 +89,29 @@ public void resolveWhenUsingTrustedIssuerThenReturnsAuthenticationManager() thro
}
}

@Test
public void resolveWhenUsingTrustedIssuerAndCustomJwtAuthConverterThenReturnsAuthenticationManager()
throws Exception {
try (MockWebServer server = new MockWebServer()) {
String issuer = server.url("").toString();
server.enqueue(new MockResponse().setResponseCode(200).setHeader("Content-Type", "application/json")
.setBody(String.format(DEFAULT_RESPONSE_TEMPLATE, issuer, issuer)));
JWSObject jws = new JWSObject(new JWSHeader(JWSAlgorithm.RS256),
new Payload(new JSONObject(Collections.singletonMap(JwtClaimNames.ISS, issuer))));
jws.sign(new RSASSASigner(TestKeys.DEFAULT_PRIVATE_KEY));
JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver(
Collections.singletonList(issuer),
new ReactiveJwtAuthenticationConverterAdapter(new JwtAuthenticationConverter()));
MockServerWebExchange exchange = withBearerToken(jws.serialize());
ReactiveAuthenticationManager authenticationManager = authenticationManagerResolver.resolve(exchange)
.block();
assertThat(authenticationManager).isNotNull();
ReactiveAuthenticationManager cachedAuthenticationManager = authenticationManagerResolver.resolve(exchange)
.block();
assertThat(authenticationManager).isSameAs(cachedAuthenticationManager);
}
}

@Test
public void resolveWhenUsingUntrustedIssuerThenException() {
JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver(
Expand All @@ -107,6 +133,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 +212,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