Add MultiTenantAuthenticationManagerResolver
A class with a number of handy request-based implementations of AuthenticationManagerResolver targeted at common multi-tenancy scenarios. Fixes: gh-6976
This commit is contained in:
parent
ecb13aa8cc
commit
f5da63118e
|
@ -17,7 +17,6 @@ package sample;
|
|||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
|
@ -27,12 +26,14 @@ import org.springframework.security.authentication.AuthenticationManagerResolver
|
|||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||
import org.springframework.security.oauth2.server.resource.introspection.NimbusOAuth2TokenIntrospectionClient;
|
||||
import org.springframework.security.oauth2.server.resource.introspection.OAuth2TokenIntrospectionClient;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.OAuth2IntrospectionAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.resource.introspection.NimbusOAuth2TokenIntrospectionClient;
|
||||
import org.springframework.security.oauth2.server.resource.introspection.OAuth2TokenIntrospectionClient;
|
||||
|
||||
import static org.springframework.security.web.authentication.MultiTenantAuthenticationManagerResolver.resolveFromPath;
|
||||
|
||||
/**
|
||||
* @author Josh Cummings
|
||||
|
@ -64,13 +65,7 @@ public class OAuth2ResourceServerSecurityConfiguration extends WebSecurityConfig
|
|||
Map<String, AuthenticationManager> authenticationManagers = new HashMap<>();
|
||||
authenticationManagers.put("tenantOne", jwt());
|
||||
authenticationManagers.put("tenantTwo", opaque());
|
||||
return request -> {
|
||||
String[] pathParts = request.getRequestURI().split("/");
|
||||
String tenantId = pathParts.length > 0 ? pathParts[1] : null;
|
||||
return Optional.ofNullable(tenantId)
|
||||
.map(authenticationManagers::get)
|
||||
.orElseThrow(() -> new IllegalArgumentException("unknown tenant"));
|
||||
};
|
||||
return resolveFromPath(authenticationManagers::get);
|
||||
}
|
||||
|
||||
AuthenticationManager jwt() {
|
||||
|
|
|
@ -0,0 +1,174 @@
|
|||
/*
|
||||
* 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.web.authentication;
|
||||
|
||||
import java.util.Optional;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationManagerResolver;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
* An implementation of {@link AuthenticationManagerResolver} that separates the tasks of
|
||||
* extracting the request's tenant identifier and looking up an {@link AuthenticationManager}
|
||||
* by that tenant identifier.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 5.2
|
||||
* @see AuthenticationManagerResolver
|
||||
*/
|
||||
public final class MultiTenantAuthenticationManagerResolver<T> implements AuthenticationManagerResolver<HttpServletRequest> {
|
||||
|
||||
private final Converter<HttpServletRequest, AuthenticationManager> authenticationManagerResolver;
|
||||
|
||||
/**
|
||||
* Constructs a {@link MultiTenantAuthenticationManagerResolver} with the provided parameters
|
||||
*
|
||||
* @param tenantResolver
|
||||
* @param authenticationManagerResolver
|
||||
*/
|
||||
public MultiTenantAuthenticationManagerResolver
|
||||
(Converter<HttpServletRequest, T> tenantResolver,
|
||||
Converter<T, AuthenticationManager> authenticationManagerResolver) {
|
||||
|
||||
Assert.notNull(tenantResolver, "tenantResolver cannot be null");
|
||||
Assert.notNull(authenticationManagerResolver, "authenticationManagerResolver cannot be null");
|
||||
|
||||
this.authenticationManagerResolver = request -> {
|
||||
Optional<T> context = Optional.ofNullable(tenantResolver.convert(request));
|
||||
return context.map(authenticationManagerResolver::convert)
|
||||
.orElseThrow(() -> new IllegalArgumentException
|
||||
("Could not resolve AuthenticationManager by reference " + context.orElse(null)));
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthenticationManager resolve(HttpServletRequest context) {
|
||||
return this.authenticationManagerResolver.convert(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an {@link AuthenticationManagerResolver} that will use a hostname's first label as
|
||||
* the resolution key for the underlying {@link AuthenticationManagerResolver}.
|
||||
*
|
||||
* For example, you might have a set of {@link AuthenticationManager}s defined like so:
|
||||
*
|
||||
* <pre>
|
||||
* Map<String, AuthenticationManager> authenticationManagers = new HashMap<>();
|
||||
* authenticationManagers.put("tenantOne", managerOne());
|
||||
* authenticationManagers.put("tenantTwo", managerTwo());
|
||||
* </pre>
|
||||
*
|
||||
* And that your system serves hostnames like <pre>https://tenantOne.example.org</pre>.
|
||||
*
|
||||
* Then, you could create an {@link AuthenticationManagerResolver} that uses the "tenantOne" value from
|
||||
* the hostname to resolve Tenant One's {@link AuthenticationManager} like so:
|
||||
*
|
||||
* <pre>
|
||||
* AuthenticationManagerResolver<HttpServletRequest> resolver =
|
||||
* resolveFromSubdomain(authenticationManagers::get);
|
||||
* </pre>
|
||||
*
|
||||
* {@link HttpServletRequest}
|
||||
* @param resolver A {@link String}-resolving {@link AuthenticationManagerResolver}
|
||||
* @return A hostname-resolving {@link AuthenticationManagerResolver}
|
||||
*/
|
||||
public static AuthenticationManagerResolver<HttpServletRequest>
|
||||
resolveFromSubdomain(Converter<String, AuthenticationManager> resolver) {
|
||||
|
||||
return new MultiTenantAuthenticationManagerResolver<>(request ->
|
||||
Optional.ofNullable(request.getServerName())
|
||||
.map(host -> host.split("\\."))
|
||||
.filter(segments -> segments.length > 0)
|
||||
.map(segments -> segments[0]).orElse(null), resolver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an {@link AuthenticationManagerResolver} that will use a request path's first segment as
|
||||
* the resolution key for the underlying {@link AuthenticationManagerResolver}.
|
||||
*
|
||||
* For example, you might have a set of {@link AuthenticationManager}s defined like so:
|
||||
*
|
||||
* <pre>
|
||||
* Map<String, AuthenticationManager> authenticationManagers = new HashMap<>();
|
||||
* authenticationManagers.put("tenantOne", managerOne());
|
||||
* authenticationManagers.put("tenantTwo", managerTwo());
|
||||
* </pre>
|
||||
*
|
||||
* And that your system serves requests like <pre>https://example.org/tenantOne</pre>.
|
||||
*
|
||||
* Then, you could create an {@link AuthenticationManagerResolver} that uses the "tenantOne" value from
|
||||
* the request to resolve Tenant One's {@link AuthenticationManager} like so:
|
||||
*
|
||||
* <pre>
|
||||
* AuthenticationManagerResolver<HttpServletRequest> resolver =
|
||||
* resolveFromPath(authenticationManagers::get);
|
||||
* </pre>
|
||||
*
|
||||
* {@link HttpServletRequest}
|
||||
* @param resolver A {@link String}-resolving {@link AuthenticationManagerResolver}
|
||||
* @return A path-resolving {@link AuthenticationManagerResolver}
|
||||
*/
|
||||
public static AuthenticationManagerResolver<HttpServletRequest>
|
||||
resolveFromPath(Converter<String, AuthenticationManager> resolver) {
|
||||
|
||||
return new MultiTenantAuthenticationManagerResolver<>(request ->
|
||||
Optional.ofNullable(request.getRequestURI())
|
||||
.map(UriComponentsBuilder::fromUriString)
|
||||
.map(UriComponentsBuilder::build)
|
||||
.map(UriComponents::getPathSegments)
|
||||
.filter(segments -> !segments.isEmpty())
|
||||
.map(segments -> segments.get(0)).orElse(null), resolver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an {@link AuthenticationManagerResolver} that will use a request headers's value as
|
||||
* the resolution key for the underlying {@link AuthenticationManagerResolver}.
|
||||
*
|
||||
* For example, you might have a set of {@link AuthenticationManager}s defined like so:
|
||||
*
|
||||
* <pre>
|
||||
* Map<String, AuthenticationManager> authenticationManagers = new HashMap<>();
|
||||
* authenticationManagers.put("tenantOne", managerOne());
|
||||
* authenticationManagers.put("tenantTwo", managerTwo());
|
||||
* </pre>
|
||||
*
|
||||
* And that your system serves requests with a header like <pre>X-Tenant-Id: tenantOne</pre>.
|
||||
*
|
||||
* Then, you could create an {@link AuthenticationManagerResolver} that uses the "tenantOne" value from
|
||||
* the request to resolve Tenant One's {@link AuthenticationManager} like so:
|
||||
*
|
||||
* <pre>
|
||||
* AuthenticationManagerResolver<HttpServletRequest> resolver =
|
||||
* resolveFromHeader("X-Tenant-Id", authenticationManagers::get);
|
||||
* </pre>
|
||||
*
|
||||
* {@link HttpServletRequest}
|
||||
* @param resolver A {@link String}-resolving {@link AuthenticationManagerResolver}
|
||||
* @return A header-resolving {@link AuthenticationManagerResolver}
|
||||
*/
|
||||
public static AuthenticationManagerResolver<HttpServletRequest>
|
||||
resolveFromHeader(String headerName, Converter<String, AuthenticationManager> resolver) {
|
||||
|
||||
return new MultiTenantAuthenticationManagerResolver<>
|
||||
(request -> request.getHeader(headerName), resolver);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
/*
|
||||
* 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.web.authentication;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationManagerResolver;
|
||||
|
||||
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.mockito.Mockito.when;
|
||||
import static org.springframework.security.web.authentication.MultiTenantAuthenticationManagerResolver.resolveFromSubdomain;
|
||||
import static org.springframework.security.web.authentication.MultiTenantAuthenticationManagerResolver.resolveFromPath;
|
||||
import static org.springframework.security.web.authentication.MultiTenantAuthenticationManagerResolver.resolveFromHeader;
|
||||
|
||||
/**
|
||||
* Tests for {@link MultiTenantAuthenticationManagerResolver}
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class MultiTenantAuthenticationManagerResolverTests {
|
||||
private static final String TENANT = "tenant";
|
||||
|
||||
@Mock
|
||||
AuthenticationManager authenticationManager;
|
||||
|
||||
@Mock
|
||||
HttpServletRequest request;
|
||||
|
||||
Map<String, AuthenticationManager> authenticationManagers;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
this.authenticationManagers = Collections.singletonMap(TENANT, this.authenticationManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveFromSubdomainWhenGivenResolverThenReturnsSubdomainParsingResolver() {
|
||||
AuthenticationManagerResolver<HttpServletRequest> fromSubdomain =
|
||||
resolveFromSubdomain(this.authenticationManagers::get);
|
||||
|
||||
when(this.request.getServerName()).thenReturn(TENANT + ".example.org");
|
||||
|
||||
AuthenticationManager authenticationManager = fromSubdomain.resolve(this.request);
|
||||
assertThat(authenticationManager).isEqualTo(this.authenticationManager);
|
||||
|
||||
when(this.request.getServerName()).thenReturn("wrong.example.org");
|
||||
|
||||
assertThatCode(() -> fromSubdomain.resolve(this.request))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
|
||||
when(this.request.getServerName()).thenReturn("example");
|
||||
|
||||
assertThatCode(() -> fromSubdomain.resolve(this.request))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveFromPathWhenGivenResolverThenReturnsPathParsingResolver() {
|
||||
AuthenticationManagerResolver<HttpServletRequest> fromPath =
|
||||
resolveFromPath(this.authenticationManagers::get);
|
||||
|
||||
when(this.request.getRequestURI()).thenReturn("/" + TENANT + "/otherthings");
|
||||
|
||||
AuthenticationManager authenticationManager = fromPath.resolve(this.request);
|
||||
assertThat(authenticationManager).isEqualTo(this.authenticationManager);
|
||||
|
||||
when(this.request.getRequestURI()).thenReturn("/otherthings");
|
||||
|
||||
assertThatCode(() -> fromPath.resolve(this.request))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
|
||||
when(this.request.getRequestURI()).thenReturn("/");
|
||||
|
||||
assertThatCode(() -> fromPath.resolve(this.request))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveFromHeaderWhenGivenResolverTheReturnsHeaderParsingResolver() {
|
||||
AuthenticationManagerResolver<HttpServletRequest> fromHeader =
|
||||
resolveFromHeader("X-Tenant-Id", this.authenticationManagers::get);
|
||||
|
||||
when(this.request.getHeader("X-Tenant-Id")).thenReturn(TENANT);
|
||||
|
||||
AuthenticationManager authenticationManager = fromHeader.resolve(this.request);
|
||||
assertThat(authenticationManager).isEqualTo(this.authenticationManager);
|
||||
|
||||
when(this.request.getHeader("X-Tenant-Id")).thenReturn("wrong");
|
||||
|
||||
assertThatCode(() -> fromHeader.resolve(this.request))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
|
||||
when(this.request.getHeader("X-Tenant-Id")).thenReturn(null);
|
||||
|
||||
assertThatCode(() -> fromHeader.resolve(this.request))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveWhenGivenTenantResolverThenResolves() {
|
||||
AuthenticationManagerResolver<HttpServletRequest> byRequestConverter =
|
||||
new MultiTenantAuthenticationManagerResolver<>(HttpServletRequest::getQueryString,
|
||||
this.authenticationManagers::get);
|
||||
|
||||
when(this.request.getQueryString()).thenReturn(TENANT);
|
||||
|
||||
AuthenticationManager authenticationManager = byRequestConverter.resolve(this.request);
|
||||
assertThat(authenticationManager).isEqualTo(this.authenticationManager);
|
||||
|
||||
when(this.request.getQueryString()).thenReturn("wrong");
|
||||
|
||||
assertThatCode(() -> byRequestConverter.resolve(this.request))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
|
||||
when(this.request.getQueryString()).thenReturn(null);
|
||||
|
||||
assertThatCode(() -> byRequestConverter.resolve(this.request))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenUsingNullTenantResolverThenException() {
|
||||
assertThatCode(() -> new MultiTenantAuthenticationManagerResolver
|
||||
(null, mock(Converter.class)))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenUsingNullAuthenticationManagerResolverThenException() {
|
||||
assertThatCode(() -> new MultiTenantAuthenticationManagerResolver
|
||||
(mock(Converter.class), null))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue