Add JwtIssuerAuthenticationManagerResolver

Fixes gh-7724
This commit is contained in:
Josh Cummings 2019-12-11 17:53:56 -07:00
parent 09810b8df9
commit de87675f6d
4 changed files with 508 additions and 104 deletions

View File

@ -28,10 +28,19 @@ import java.time.Instant;
import java.time.ZoneId;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import javax.annotation.PreDestroy;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSObject;
import com.nimbusds.jose.Payload;
import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import net.minidev.json.JSONObject;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.hamcrest.core.AllOf;
@ -82,14 +91,15 @@ import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrinci
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jose.TestKeys;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtException;
import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtIssuerAuthenticationManagerResolver;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.oauth2.server.resource.introspection.NimbusOpaqueTokenIntrospector;
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
@ -127,6 +137,8 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.security.config.Customizer.withDefaults;
import static org.springframework.security.oauth2.core.TestOAuth2AccessTokens.noScopes;
import static org.springframework.security.oauth2.jwt.JwtClaimNames.ISS;
import static org.springframework.security.oauth2.jwt.JwtClaimNames.SUB;
import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withJwkSetUri;
import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withPublicKey;
import static org.springframework.security.oauth2.jwt.TestJwts.jwt;
@ -149,7 +161,7 @@ import static org.springframework.web.bind.annotation.RequestMethod.POST;
public class OAuth2ResourceServerConfigurerTests {
private static final String JWT_TOKEN = "token";
private static final String JWT_SUBJECT = "mock-test-subject";
private static final Map<String, Object> JWT_CLAIMS = Collections.singletonMap(JwtClaimNames.SUB, JWT_SUBJECT);
private static final Map<String, Object> JWT_CLAIMS = Collections.singletonMap(SUB, JWT_SUBJECT);
private static final Jwt JWT = jwt().build();
private static final String JWK_SET_URI = "https://mock.org";
private static final JwtAuthenticationToken JWT_AUTHENTICATION_TOKEN =
@ -1332,6 +1344,50 @@ public class OAuth2ResourceServerConfigurerTests {
verify(http, never()).authenticationProvider(any(AuthenticationProvider.class));
}
// -- authentication manager resolver
@Test
public void getWhenMultipleIssuersThenUsesIssuerClaimToDifferentiate() throws Exception {
this.spring.register(WebServerConfig.class, MultipleIssuersConfig.class, BasicController.class).autowire();
MockWebServer server = this.spring.getContext().getBean(MockWebServer.class);
String metadata = "{\n"
+ " \"issuer\": \"%s\", \n"
+ " \"jwks_uri\": \"%s/.well-known/jwks.json\" \n"
+ "}";
String jwkSet = jwkSet();
String issuerOne = server.url("/issuerOne").toString();
String issuerTwo = server.url("/issuerTwo").toString();
String issuerThree = server.url("/issuerThree").toString();
String jwtOne = jwtFromIssuer(issuerOne);
String jwtTwo = jwtFromIssuer(issuerTwo);
String jwtThree = jwtFromIssuer(issuerThree);
mockWebServer(String.format(metadata, issuerOne, issuerOne));
mockWebServer(jwkSet);
this.mvc.perform(get("/authenticated")
.with(bearerToken(jwtOne)))
.andExpect(status().isOk())
.andExpect(content().string("test-subject"));
mockWebServer(String.format(metadata, issuerTwo, issuerTwo));
mockWebServer(jwkSet);
this.mvc.perform(get("/authenticated")
.with(bearerToken(jwtTwo)))
.andExpect(status().isOk())
.andExpect(content().string("test-subject"));
mockWebServer(String.format(metadata, issuerThree, issuerThree));
mockWebServer(jwkSet);
this.mvc.perform(get("/authenticated")
.with(bearerToken(jwtThree)))
.andExpect(status().isUnauthorized())
.andExpect(invalidTokenHeader("Invalid issuer"));
}
// -- Incorrect Configuration
@Test
@ -2070,6 +2126,26 @@ public class OAuth2ResourceServerConfigurerTests {
}
}
@EnableWebSecurity
static class MultipleIssuersConfig extends WebSecurityConfigurerAdapter {
@Autowired
MockWebServer web;
@Override
protected void configure(HttpSecurity http) throws Exception {
String issuerOne = this.web.url("/issuerOne").toString();
String issuerTwo = this.web.url("/issuerTwo").toString();
JwtIssuerAuthenticationManagerResolver authenticationManagerResolver =
new JwtIssuerAuthenticationManagerResolver(issuerOne, issuerTwo);
// @formatter:off
http
.oauth2ResourceServer()
.authenticationManagerResolver(authenticationManagerResolver);
// @formatter:on
}
}
@EnableWebSecurity
static class AuthenticationManagerResolverPlusOtherConfig extends WebSecurityConfigurerAdapter {
@Override
@ -2257,6 +2333,23 @@ public class OAuth2ResourceServerConfigurerTests {
", error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\"");
}
private String jwkSet() {
return new JWKSet(new RSAKey.Builder(TestKeys.DEFAULT_PUBLIC_KEY)
.keyID("1").build()).toString();
}
private String jwtFromIssuer(String issuer) throws Exception {
Map<String, Object> claims = new HashMap<>();
claims.put(ISS, issuer);
claims.put(SUB, "test-subject");
claims.put("scope", "message:read");
JWSObject jws = new JWSObject(
new JWSHeader.Builder(JWSAlgorithm.RS256).keyID("1").build(),
new Payload(new JSONObject(claims)));
jws.sign(new RSASSASigner(TestKeys.DEFAULT_PRIVATE_KEY));
return jws.serialize();
}
private void mockWebServer(String response) {
this.web.enqueue(new MockResponse()
.setResponseCode(200)

View File

@ -1243,109 +1243,15 @@ In each case, there are two things that need to be done and trade-offs associate
1. Resolve the tenant
2. Propagate the tenant
==== Resolving the Tenant By Request Material
Resolving the tenant by request material can be done my implementing an `AuthenticationManagerResolver`, which determines the `AuthenticationManager` at runtime, like so:
[source,java]
----
@Component
public class TenantAuthenticationManagerResolver
implements AuthenticationManagerResolver<HttpServletRequest> {
private final BearerTokenResolver resolver = new DefaultBearerTokenResolver();
private final TenantRepository tenants; <1>
private final Map<String, AuthenticationManager> authenticationManagers = new ConcurrentHashMap<>(); <2>
public TenantAuthenticationManagerResolver(TenantRepository tenants) {
this.tenants = tenants;
}
@Override
public AuthenticationManager resolve(HttpServletRequest request) {
return this.authenticationManagers.computeIfAbsent(toTenant(request), this::fromTenant);
}
private String toTenant(HttpServletRequest request) {
String[] pathParts = request.getRequestURI().split("/");
return pathParts.length > 0 ? pathParts[1] : null;
}
private AuthenticationManager fromTenant(String tenant) {
return Optional.ofNullable(this.tenants.get(tenant)) <3>
.map(JwtDecoders::fromIssuerLocation) <4>
.map(JwtAuthenticationProvider::new)
.orElseThrow(() -> new IllegalArgumentException("unknown tenant"))::authenticate;
}
}
----
<1> A hypothetical source for tenant information
<2> A cache for `AuthenticationManager`s, keyed by tenant identifier
<3> Looking up the tenant is more secure than simply computing the issuer location on the fly - the lookup acts as a tenant whitelist
<4> Create a `JwtDecoder` via the discovery endpoint - the lazy lookup here means that you don't need to configure all tenants at startup
And then specify this `AuthenticationManagerResolver` in the DSL:
[source,java]
----
http
.authorizeRequests(authorizeRequests ->
authorizeRequests
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2ResourceServer ->
oauth2ResourceServer
.authenticationManagerResolver(this.tenantAuthenticationManagerResolver)
);
----
==== Resolving the Tenant By Claim
Resolving the tenant by claim is similar to doing so by request material.
The only real difference is the `toTenant` method implementation:
One way to differentiate tenants is by the issuer claim. Since the issuer claim accompanies signed JWTs, this can be done with the `JwtIssuerAuthenticationManagerResolver`, like so:
[source,java]
----
@Component
public class TenantAuthenticationManagerResolver implements AuthenticationManagerResolver<HttpServletRequest> {
private final BearerTokenResolver resolver = new DefaultBearerTokenResolver();
private final TenantRepository tenants; <1>
JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver
("https://idp.example.org/issuerOne", "https://idp.example.org/issuerTwo");
private final Map<String, AuthenticationManager> authenticationManagers = new ConcurrentHashMap<>(); <2>
public TenantAuthenticationManagerResolver(TenantRepository tenants) {
this.tenants = tenants;
}
@Override
public AuthenticationManager resolve(HttpServletRequest request) {
return this.authenticationManagers.computeIfAbsent(toTenant(request), this::fromTenant); <3>
}
private String toTenant(HttpServletRequest request) {
try {
String token = this.resolver.resolve(request);
return (String) JWTParser.parse(token).getJWTClaimsSet().getIssuer();
} catch (Exception e) {
throw new IllegalArgumentException(e);
}
}
private AuthenticationManager fromTenant(String tenant) {
return Optional.ofNullable(this.tenants.get(tenant)) <3>
.map(JwtDecoders::fromIssuerLocation) <4>
.map(JwtAuthenticationProvider::new)
.orElseThrow(() -> new IllegalArgumentException("unknown tenant"))::authenticate;
}
}
----
<1> A hypothetical source for tenant information
<2> A cache for `AuthenticationManager`s, keyed by tenant identifier
<3> Looking up the tenant is more secure than simply computing the issuer location on the fly - the lookup acts as a tenant whitelist
<4> Create a `JwtDecoder` via the discovery endpoint - the lazy lookup here means that you don't need to configure all tenants at startup
[source,java]
----
http
.authorizeRequests(authorizeRequests ->
authorizeRequests
@ -1353,13 +1259,52 @@ http
)
.oauth2ResourceServer(oauth2ResourceServer ->
oauth2ResourceServer
.authenticationManagerResolver(this.tenantAuthenticationManagerResolver)
.authenticationManagerResolver(authenticationManagerResolver)
);
----
==== Parsing the Claim Only Once
This is nice because the issuer endpoints are loaded lazily.
In fact, the corresponding `JwtAuthenticationProvider` is instantiated only when the first request with the corresponding issuer is sent.
This allows for an application startup that is independent from those authorization servers being up and available.
You may have observed that this strategy, while simple, comes with the trade-off that the JWT is parsed once by the `AuthenticationManagerResolver` and then again by the `JwtDecoder`.
===== Dynamic Tenants
Of course, you may not want to restart the application each time a new tenant is added.
In this case, you can configure the `JwtIssuerAuthenticationManagerResolver` with a repository of `AuthenticationManager` instances, which you can edit at runtime, like so:
[source,java]
----
private void addManager(Map<String, AuthenticationManager> authenticationManagers, String issuer) {
JwtAuthenticationProvider authenticationProvider = new JwtAuthenticationProvider
(JwtDecoders.fromIssuerLocation(issuer));
authenticationManagers.put(issuer, authenticationProvider::authenticate);
}
// ...
JwtIssuerAuthenticationManagerResolver authenticationManagerResolver =
new JwtIssuerAuthenticationManagerResolver(authenticationManagers::get);
http
.authorizeRequests(authorizeRequests ->
authorizeRequests
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2ResourceServer ->
oauth2ResourceServer
.authenticationManagerResolver(authenticationManagerResolver)
);
----
In this case, you construct `JwtIssuerAuthenticationManagerResolver` with a strategy for obtaining the `AuthenticationManager` given the issuer.
This approach allows us to add and remove elements from the repository (shown as a `Map` in the snippet) at runtime.
NOTE: It would be unsafe to simply take any issuer and construct an `AuthenticationManager` from it.
The issuer should be one that the code can verify from a trusted source like a whitelist.
===== Parsing the Claim Only Once
You may have observed that this strategy, while simple, comes with the trade-off that the JWT is parsed once by the `AuthenticationManagerResolver` and then again by the `JwtDecoder` later on in the request.
This extra parsing can be alleviated by configuring the `JwtDecoder` directly with a `JWTClaimSetAwareJWSKeySelector` from Nimbus:
@ -1479,8 +1424,8 @@ JwtDecoder jwtDecoder(JWTProcessor jwtProcessor, OAuth2TokenValidator<Jwt> jwtVa
We've finished talking about resolving the tenant.
If you've chosen to resolve the tenant by request material, then you'll need to make sure you address your downstream resource servers in the same way.
For example, if you are resolving it by subdomain, you'll need to address the downstream resource server using the same subdomain.
If you've chosen to resolve the tenant by something other than a JWT claim, then you'll need to make sure you address your downstream resource servers in the same way.
For example, if you are resolving it by subdomain, you may need to address the downstream resource server using the same subdomain.
However, if you resolve it by a claim in the bearer token, read on to learn about <<oauth2resourceserver-bearertoken-resolver,Spring Security's support for bearer token propagation>>.

View File

@ -0,0 +1,180 @@
/*
* Copyright 2002-2019 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.server.resource.authentication;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
import javax.servlet.http.HttpServletRequest;
import com.nimbusds.jwt.JWTParser;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpStatus;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationManagerResolver;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtDecoders;
import org.springframework.security.oauth2.server.resource.BearerTokenError;
import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes;
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
import org.springframework.util.Assert;
/**
* An implementation of {@link AuthenticationManagerResolver} that resolves a JWT-based {@link AuthenticationManager}
* based on the <a href="https://openid.net/specs/openid-connect-core-1_0.html#IssuerIdentifier">Issuer</a> in a
* signed JWT (JWS).
*
* To use, this class must be able to determine whether or not the `iss` claim is trusted. Recall that
* anyone can stand up an authorization server and issue valid tokens to a resource server. The simplest way
* to achieve this is to supply a whitelist of trusted issuers in the constructor.
*
* This class derives the Issuer from the `iss` claim found in the {@link HttpServletRequest}'s
* <a href="https://tools.ietf.org/html/rfc6750#section-1.2" target="_blank">Bearer Token</a>.
*
* @author Josh Cummings
* @since 5.3
*/
public final class JwtIssuerAuthenticationManagerResolver implements AuthenticationManagerResolver<HttpServletRequest> {
private static final OAuth2Error DEFAULT_INVALID_TOKEN = invalidToken("Invalid token");
private final AuthenticationManagerResolver<String> issuerAuthenticationManagerResolver;
private final Converter<HttpServletRequest, String> issuerConverter = new JwtClaimIssuerConverter();
/**
* Construct a {@link JwtIssuerAuthenticationManagerResolver} using the provided parameters
*
* @param trustedIssuers a whitelist of trusted issuers
*/
public JwtIssuerAuthenticationManagerResolver(String... trustedIssuers) {
this(Arrays.asList(trustedIssuers));
}
/**
* Construct a {@link JwtIssuerAuthenticationManagerResolver} using the provided parameters
*
* @param trustedIssuers a whitelist of trusted issuers
*/
public JwtIssuerAuthenticationManagerResolver(Collection<String> trustedIssuers) {
Assert.notEmpty(trustedIssuers, "trustedIssuers cannot be empty");
this.issuerAuthenticationManagerResolver =
new TrustedIssuerJwtAuthenticationManagerResolver
(Collections.unmodifiableCollection(trustedIssuers)::contains);
}
/**
* 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 a whitelist.
*
* 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 whitelist.
*
* @param issuerAuthenticationManagerResolver a strategy for resolving the {@link AuthenticationManager} by the issuer
*/
public JwtIssuerAuthenticationManagerResolver(AuthenticationManagerResolver<String> issuerAuthenticationManagerResolver) {
Assert.notNull(issuerAuthenticationManagerResolver, "issuerAuthenticationManagerResolver cannot be null");
this.issuerAuthenticationManagerResolver = issuerAuthenticationManagerResolver;
}
/**
* Return an {@link AuthenticationManager} based off of the `iss` claim found in the request's bearer token
*
* @throws OAuth2AuthenticationException if the bearer token is malformed or an {@link AuthenticationManager}
* can't be derived from the issuer
*/
@Override
public AuthenticationManager resolve(HttpServletRequest request) {
String issuer = this.issuerConverter.convert(request);
AuthenticationManager authenticationManager = this.issuerAuthenticationManagerResolver.resolve(issuer);
if (authenticationManager == null) {
throw new OAuth2AuthenticationException(invalidToken("Invalid issuer " + issuer));
}
return authenticationManager;
}
private static class JwtClaimIssuerConverter
implements Converter<HttpServletRequest, String> {
private final BearerTokenResolver resolver = new DefaultBearerTokenResolver();
@Override
public String convert(@NonNull HttpServletRequest request) {
String token = this.resolver.resolve(request);
try {
String issuer = JWTParser.parse(token).getJWTClaimsSet().getIssuer();
if (issuer != null) {
return issuer;
}
} catch (Exception e) {
throw new OAuth2AuthenticationException(invalidToken(e.getMessage()));
}
throw new OAuth2AuthenticationException(invalidToken("Missing issuer"));
}
}
private static class TrustedIssuerJwtAuthenticationManagerResolver
implements AuthenticationManagerResolver<String> {
private final Map<String, AuthenticationManager> authenticationManagers = new ConcurrentHashMap<>();
private final Predicate<String> trustedIssuer;
TrustedIssuerJwtAuthenticationManagerResolver(Predicate<String> trustedIssuer) {
this.trustedIssuer = trustedIssuer;
}
@Override
public AuthenticationManager resolve(String issuer) {
if (this.trustedIssuer.test(issuer)) {
return this.authenticationManagers.computeIfAbsent(issuer, k -> {
JwtDecoder jwtDecoder = JwtDecoders.fromIssuerLocation(issuer);
return new JwtAuthenticationProvider(jwtDecoder)::authenticate;
});
}
return null;
}
}
private static OAuth2Error invalidToken(String message) {
try {
return new BearerTokenError(
BearerTokenErrorCodes.INVALID_TOKEN,
HttpStatus.UNAUTHORIZED,
message,
"https://tools.ietf.org/html/rfc6750#section-3.1");
} catch (IllegalArgumentException malformed) {
// some third-party library error messages are not suitable for RFC 6750's error message charset
return DEFAULT_INVALID_TOKEN;
}
}
}

View File

@ -0,0 +1,186 @@
/*
* Copyright 2002-2019 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.server.resource.authentication;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSObject;
import com.nimbusds.jose.Payload;
import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.PlainJWT;
import net.minidev.json.JSONObject;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.Test;
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 static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.mockito.Mockito.mock;
import static org.springframework.security.oauth2.jwt.JwtClaimNames.ISS;
/**
* Tests for {@link JwtIssuerAuthenticationManagerResolver}
*/
public class JwtIssuerAuthenticationManagerResolverTests {
private static final String DEFAULT_RESPONSE_TEMPLATE = "{\n"
+ " \"issuer\": \"%s\", \n"
+ " \"jwks_uri\": \"%s/.well-known/jwks.json\" \n"
+ "}";
private String jwt = jwt("iss", "trusted");
private String evil = jwt("iss", "\"");
private String noIssuer = jwt("sub", "sub");
@Test
public void resolveWhenUsingTrustedIssuerThenReturnsAuthenticationManager() throws Exception {
try (MockWebServer server = new MockWebServer()) {
server.start();
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(ISS, issuer))));
jws.sign(new RSASSASigner(TestKeys.DEFAULT_PRIVATE_KEY));
JwtIssuerAuthenticationManagerResolver authenticationManagerResolver =
new JwtIssuerAuthenticationManagerResolver(issuer);
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("other", "issuers");
MockHttpServletRequest request = new MockHttpServletRequest();
request.addHeader("Authorization", "Bearer " + this.jwt);
assertThatCode(() -> authenticationManagerResolver.resolve(request))
.isInstanceOf(OAuth2AuthenticationException.class)
.hasMessageContaining("Invalid issuer");
}
@Test
public void resolveWhenUsingCustomIssuerAuthenticationManagerResolverThenUses() {
AuthenticationManager authenticationManager = mock(AuthenticationManager.class);
JwtIssuerAuthenticationManagerResolver authenticationManagerResolver =
new JwtIssuerAuthenticationManagerResolver(issuer -> authenticationManager);
MockHttpServletRequest request = new MockHttpServletRequest();
request.addHeader("Authorization", "Bearer " + this.jwt);
assertThat(authenticationManagerResolver.resolve(request))
.isSameAs(authenticationManager);
}
@Test
public void resolveWhenUsingExternalSourceThenRespondsToChanges() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.addHeader("Authorization", "Bearer " + this.jwt);
Map<String, AuthenticationManager> authenticationManagers = new HashMap<>();
JwtIssuerAuthenticationManagerResolver authenticationManagerResolver =
new JwtIssuerAuthenticationManagerResolver(authenticationManagers::get);
assertThatCode(() -> authenticationManagerResolver.resolve(request))
.isInstanceOf(OAuth2AuthenticationException.class)
.hasMessageContaining("Invalid issuer");
AuthenticationManager authenticationManager = mock(AuthenticationManager.class);
authenticationManagers.put("trusted", authenticationManager);
assertThat(authenticationManagerResolver.resolve(request))
.isSameAs(authenticationManager);
authenticationManagers.clear();
assertThatCode(() -> authenticationManagerResolver.resolve(request))
.isInstanceOf(OAuth2AuthenticationException.class)
.hasMessageContaining("Invalid issuer");
}
@Test
public void resolveWhenBearerTokenMalformedThenException() {
JwtIssuerAuthenticationManagerResolver authenticationManagerResolver =
new JwtIssuerAuthenticationManagerResolver("trusted");
MockHttpServletRequest request = new MockHttpServletRequest();
request.addHeader("Authorization", "Bearer jwt");
assertThatCode(() -> authenticationManagerResolver.resolve(request))
.isInstanceOf(OAuth2AuthenticationException.class)
.hasMessageNotContaining("Invalid issuer");
}
@Test
public void resolveWhenBearerTokenNoIssuerThenException() {
JwtIssuerAuthenticationManagerResolver authenticationManagerResolver =
new JwtIssuerAuthenticationManagerResolver("trusted");
MockHttpServletRequest request = new MockHttpServletRequest();
request.addHeader("Authorization", "Bearer " + this.noIssuer);
assertThatCode(() -> authenticationManagerResolver.resolve(request))
.isInstanceOf(OAuth2AuthenticationException.class)
.hasMessageContaining("Missing issuer");
}
@Test
public void resolveWhenBearerTokenEvilThenGenericException() {
JwtIssuerAuthenticationManagerResolver authenticationManagerResolver =
new JwtIssuerAuthenticationManagerResolver("trusted");
MockHttpServletRequest request = new MockHttpServletRequest();
request.addHeader("Authorization", "Bearer " + this.evil);
assertThatCode(() -> authenticationManagerResolver.resolve(request))
.isInstanceOf(OAuth2AuthenticationException.class)
.hasMessage("Invalid token");
}
@Test
public void constructorWhenNullOrEmptyIssuersThenException() {
assertThatCode(() -> new JwtIssuerAuthenticationManagerResolver((Collection) null))
.isInstanceOf(IllegalArgumentException.class);
assertThatCode(() -> new JwtIssuerAuthenticationManagerResolver(Collections.emptyList()))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
public void constructorWhenNullAuthenticationManagerResolverThenException() {
assertThatCode(() -> new JwtIssuerAuthenticationManagerResolver((AuthenticationManagerResolver) null))
.isInstanceOf(IllegalArgumentException.class);
}
private String jwt(String claim, String value) {
PlainJWT jwt = new PlainJWT(new JWTClaimsSet.Builder().claim(claim, value).build());
return jwt.serialize();
}
}