diff --git a/samples/boot/oauth2resourceserver-multitenancy/README.adoc b/samples/boot/oauth2resourceserver-multitenancy/README.adoc index 85065e31b8..97674479bd 100644 --- a/samples/boot/oauth2resourceserver-multitenancy/README.adoc +++ b/samples/boot/oauth2resourceserver-multitenancy/README.adoc @@ -27,14 +27,14 @@ The Resource Server subsequently verifies with the Authorization Server and auth phrase ```bash -Hello, subject for tenantOne! +Hello, subject for tenant one! ``` where "subject" is the value of the `sub` field in the JWT sent in the `Authorization` header, or the phrase ```bash -Hello, subject for tenantTwo! +Hello, subject for tenant two! ``` where "subject" is the value of the `sub` field in the Introspection response from the Authorization Server. @@ -60,13 +60,13 @@ export TOKEN=eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzdWJqZWN0IiwiZXhwIjo0NjgzODA1MTI4fQ And then make this request: ```bash -curl -H "Authorization: Bearer $TOKEN" localhost:8080/tenantOne +curl -H "tenant: one" -H "Authorization: Bearer $TOKEN" localhost:8080 ``` Which will respond with the phrase: ```bash -Hello, subject for tenantOne! +Hello, subject for tenant one! ``` where `subject` is the value of the `sub` field in the JWT sent in the `Authorization` header. @@ -76,13 +76,13 @@ Or this: ```bash export TOKEN=eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzdWJqZWN0Iiwic2NvcGUiOiJtZXNzYWdlOnJlYWQiLCJleHAiOjQ2ODM4MDUxNDF9.h-j6FKRFdnTdmAueTZCdep45e6DPwqM68ZQ8doIJ1exi9YxAlbWzOwId6Bd0L5YmCmp63gGQgsBUBLzwnZQ8kLUgUOBEC3UzSWGRqMskCY9_k9pX0iomX6IfF3N0PaYs0WPC4hO1s8wfZQ-6hKQ4KigFi13G9LMLdH58PRMK0pKEvs3gCbHJuEPw-K5ORlpdnleUTQIwINafU57cmK3KocTeknPAM_L716sCuSYGvDl6xUTXO7oPdrXhS_EhxLP6KxrpI1uD4Ea_5OWTh7S0Wx5LLDfU6wBG1DowN20d374zepOIEkR-Jnmr_QlR44vmRqS5ncrF-1R0EGcPX49U6A -curl -H "Authorization: Bearer $TOKEN" localhost:8080/tenantOne/message +curl -H "tenant: one" -H "Authorization: Bearer $TOKEN" localhost:8080/message ``` Will respond with: ```bash -secret message for tenantOne +secret message for tenant one ``` === Authorizing with tenantTwo (Opaque token) @@ -96,13 +96,13 @@ export TOKEN=00ed5855-1869-47a0-b0c9-0f3ce520aee7 And then make this request: ```bash -curl -H "Authorization: Bearer $TOKEN" localhost:8080/tenantTwo +curl -H "tenant: two" -H "Authorization: Bearer $TOKEN" localhost:8080 ``` Which will respond with the phrase: ```bash -Hello, subject for tenantTwo! +Hello, subject for tenant two! ``` where `subject` is the value of the `sub` field in the Introspection response from the Authorization Server. @@ -112,13 +112,13 @@ Or this: ```bash export TOKEN=b43d1500-c405-4dc9-b9c9-6cfd966c34c9 -curl -H "Authorization: Bearer $TOKEN" localhost:8080/tenantTwo/message +curl -H "tenant: two" -H "Authorization: Bearer $TOKEN" localhost:8080/message ``` Will respond with: ```bash -secret message for tenantTwo +secret message for tenant two ``` == 2. Testing against other Authorization Servers diff --git a/samples/boot/oauth2resourceserver-multitenancy/src/integration-test/java/sample/OAuth2ResourceServerApplicationITests.java b/samples/boot/oauth2resourceserver-multitenancy/src/integration-test/java/sample/OAuth2ResourceServerApplicationITests.java index 2bca10f1ea..c50157dd92 100644 --- a/samples/boot/oauth2resourceserver-multitenancy/src/integration-test/java/sample/OAuth2ResourceServerApplicationITests.java +++ b/samples/boot/oauth2resourceserver-multitenancy/src/integration-test/java/sample/OAuth2ResourceServerApplicationITests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -22,11 +22,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.HttpHeaders; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.request.RequestPostProcessor; import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -42,7 +39,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc -@ActiveProfiles("test") public class OAuth2ResourceServerApplicationITests { String tenantOneNoScopesToken = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzdWJqZWN0IiwiZXhwIjo0NjgzODA1MTI4fQ.ULEPdHG-MK5GlrTQMhgqcyug2brTIZaJIrahUeq9zaiwUSdW83fJ7W1IDd2Z3n4a25JY2uhEcoV95lMfccHR6y_2DLrNvfta22SumY9PEDF2pido54LXG6edIGgarnUbJdR4rpRe_5oRGVa8gDx8FnuZsNv6StSZHAzw5OsuevSTJ1UbJm4UfX3wiahFOQ2OI6G-r5TB2rQNdiPHuNyzG5yznUqRIZ7-GCoMqHMaC-1epKxiX8gYXRROuUYTtcMNa86wh7OVDmvwVmFioRcR58UWBRoO1XQexTtOQq_t8KYsrPZhb9gkyW8x2bAQF-d0J0EJY8JslaH6n4RBaZISww"; @@ -57,18 +53,11 @@ public class OAuth2ResourceServerApplicationITests { public void tenantOnePerformWhenValidBearerTokenThenAllows() throws Exception { - this.mvc.perform(get("/tenantOne").with(bearerToken(this.tenantOneNoScopesToken))) + this.mvc.perform(get("/") + .header("tenant", "one") + .header("Authorization", "Bearer " + this.tenantOneNoScopesToken)) .andExpect(status().isOk()) - .andExpect(content().string(containsString("Hello, subject for tenantOne!"))); - } - - @Test - public void tenantOnePerformWhenValidBearerTokenWithServletPathThenAllows() - throws Exception { - - this.mvc.perform(get("/tenantOne").servletPath("/tenantOne").with(bearerToken(this.tenantOneNoScopesToken))) - .andExpect(status().isOk()) - .andExpect(content().string(containsString("Hello, subject for tenantOne!"))); + .andExpect(content().string(containsString("Hello, subject for tenant one!"))); } // -- tests with scopes @@ -77,16 +66,20 @@ public class OAuth2ResourceServerApplicationITests { public void tenantOnePerformWhenValidBearerTokenThenScopedRequestsAlsoWork() throws Exception { - this.mvc.perform(get("/tenantOne/message").with(bearerToken(this.tenantOneMessageReadToken))) + this.mvc.perform(get("/message") + .header("tenant", "one") + .header("Authorization", "Bearer " + this.tenantOneMessageReadToken)) .andExpect(status().isOk()) - .andExpect(content().string(containsString("secret message for tenantOne"))); + .andExpect(content().string(containsString("secret message for tenant one"))); } @Test public void tenantOnePerformWhenInsufficientlyScopedBearerTokenThenDeniesScopedMethodAccess() throws Exception { - this.mvc.perform(get("/tenantOne/message").with(bearerToken(this.tenantOneNoScopesToken))) + this.mvc.perform(get("/message") + .header("tenant", "one") + .header("Authorization", "Bearer " + this.tenantOneNoScopesToken)) .andExpect(status().isForbidden()) .andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, containsString("Bearer error=\"insufficient_scope\""))); @@ -96,9 +89,11 @@ public class OAuth2ResourceServerApplicationITests { public void tenantTwoPerformWhenValidBearerTokenThenAllows() throws Exception { - this.mvc.perform(get("/tenantTwo").with(bearerToken(this.tenantTwoNoScopesToken))) + this.mvc.perform(get("/") + .header("tenant", "two") + .header("Authorization", "Bearer " + this.tenantTwoNoScopesToken)) .andExpect(status().isOk()) - .andExpect(content().string(containsString("Hello, subject for tenantTwo!"))); + .andExpect(content().string(containsString("Hello, subject for tenant two!"))); } // -- tests with scopes @@ -107,16 +102,20 @@ public class OAuth2ResourceServerApplicationITests { public void tenantTwoPerformWhenValidBearerTokenThenScopedRequestsAlsoWork() throws Exception { - this.mvc.perform(get("/tenantTwo/message").with(bearerToken(this.tenantTwoMessageReadToken))) + this.mvc.perform(get("/message") + .header("tenant", "two") + .header("Authorization", "Bearer " + this.tenantTwoMessageReadToken)) .andExpect(status().isOk()) - .andExpect(content().string(containsString("secret message for tenantTwo"))); + .andExpect(content().string(containsString("secret message for tenant two"))); } @Test public void tenantTwoPerformWhenInsufficientlyScopedBearerTokenThenDeniesScopedMethodAccess() throws Exception { - this.mvc.perform(get("/tenantTwo/message").with(bearerToken(this.tenantTwoNoScopesToken))) + this.mvc.perform(get("/message") + .header("tenant", "two") + .header("Authorization", "Bearer " + this.tenantTwoNoScopesToken)) .andExpect(status().isForbidden()) .andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, containsString("Bearer error=\"insufficient_scope\""))); @@ -126,24 +125,8 @@ public class OAuth2ResourceServerApplicationITests { public void invalidTenantPerformWhenValidBearerTokenThenThrowsException() throws Exception { - this.mvc.perform(get("/tenantThree").with(bearerToken(this.tenantOneNoScopesToken))); - } - - private static class BearerTokenRequestPostProcessor implements RequestPostProcessor { - private String token; - - BearerTokenRequestPostProcessor(String token) { - this.token = token; - } - - @Override - public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { - request.addHeader("Authorization", "Bearer " + this.token); - return request; - } - } - - private static BearerTokenRequestPostProcessor bearerToken(String token) { - return new BearerTokenRequestPostProcessor(token); + this.mvc.perform(get("/") + .header("tenant", "three") + .header("Authorization", "Bearer " + this.tenantOneNoScopesToken)); } } diff --git a/samples/boot/oauth2resourceserver-multitenancy/src/main/java/sample/OAuth2ResourceServerController.java b/samples/boot/oauth2resourceserver-multitenancy/src/main/java/sample/OAuth2ResourceServerController.java index e14a29b032..18165789a7 100644 --- a/samples/boot/oauth2resourceserver-multitenancy/src/main/java/sample/OAuth2ResourceServerController.java +++ b/samples/boot/oauth2resourceserver-multitenancy/src/main/java/sample/OAuth2ResourceServerController.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -18,7 +18,7 @@ package sample; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; /** @@ -27,14 +27,14 @@ import org.springframework.web.bind.annotation.RestController; @RestController public class OAuth2ResourceServerController { - @GetMapping("/{tenantId}") - public String index(@AuthenticationPrincipal OAuth2AuthenticatedPrincipal token, @PathVariable("tenantId") String tenantId) { + @GetMapping("/") + public String index(@AuthenticationPrincipal OAuth2AuthenticatedPrincipal token, @RequestHeader("tenant") String tenant) { String subject = token.getAttribute("sub"); - return String.format("Hello, %s for %s!", subject, tenantId); + return String.format("Hello, %s for tenant %s!", subject, tenant); } - @GetMapping("/{tenantId}/message") - public String message(@PathVariable("tenantId") String tenantId) { - return String.format("secret message for %s", tenantId); + @GetMapping("/message") + public String message(@RequestHeader("tenant") String tenant) { + return String.format("secret message for tenant %s", tenant); } } diff --git a/samples/boot/oauth2resourceserver-multitenancy/src/main/java/sample/OAuth2ResourceServerSecurityConfiguration.java b/samples/boot/oauth2resourceserver-multitenancy/src/main/java/sample/OAuth2ResourceServerSecurityConfiguration.java index ff44461855..a52933b648 100644 --- a/samples/boot/oauth2resourceserver-multitenancy/src/main/java/sample/OAuth2ResourceServerSecurityConfiguration.java +++ b/samples/boot/oauth2resourceserver-multitenancy/src/main/java/sample/OAuth2ResourceServerSecurityConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -15,25 +15,13 @@ */ 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; -import org.springframework.context.annotation.Bean; -import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.beans.factory.annotation.Autowired; 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.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.JwtBearerTokenAuthenticationConverter; -import org.springframework.security.oauth2.server.resource.authentication.OpaqueTokenAuthenticationProvider; -import org.springframework.security.oauth2.server.resource.introspection.NimbusOpaqueTokenIntrospector; -import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; /** * @author Josh Cummings @@ -41,59 +29,20 @@ import org.springframework.security.oauth2.server.resource.introspection.OpaqueT @EnableWebSecurity public class OAuth2ResourceServerSecurityConfiguration extends WebSecurityConfigurerAdapter { - @Value("${tenantOne.jwk-set-uri}") - String jwkSetUri; - - @Value("${tenantTwo.introspection-uri}") - String introspectionUri; - - @Value("${tenantTwo.introspection-client-id}") - String introspectionClientId; - - @Value("${tenantTwo.introspection-client-secret}") - String introspectionClientSecret; + @Autowired + AuthenticationManagerResolver authenticationManagerResolver; @Override protected void configure(HttpSecurity http) throws Exception { // @formatter:off http - .authorizeRequests(authorizeRequests -> - authorizeRequests - .antMatchers("/**/message/**").hasAuthority("SCOPE_message:read") - .anyRequest().authenticated() + .authorizeRequests(authz -> authz + .antMatchers("/message/**").hasAuthority("SCOPE_message:read") + .anyRequest().authenticated() ) - .oauth2ResourceServer(oauth2ResourceServer -> - oauth2ResourceServer - .authenticationManagerResolver(multitenantAuthenticationManager()) + .oauth2ResourceServer(oauth2 -> oauth2 + .authenticationManagerResolver(this.authenticationManagerResolver) ); // @formatter:on } - - @Bean - AuthenticationManagerResolver multitenantAuthenticationManager() { - Map 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")); - }; - } - - AuthenticationManager jwt() { - JwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri).build(); - JwtAuthenticationProvider authenticationProvider = new JwtAuthenticationProvider(jwtDecoder); - authenticationProvider.setJwtAuthenticationConverter(new JwtBearerTokenAuthenticationConverter()); - return authenticationProvider::authenticate; - } - - AuthenticationManager opaque() { - OpaqueTokenIntrospector introspectionClient = - new NimbusOpaqueTokenIntrospector(this.introspectionUri, - this.introspectionClientId, this.introspectionClientSecret); - return new OpaqueTokenAuthenticationProvider(introspectionClient)::authenticate; - } } diff --git a/samples/boot/oauth2resourceserver-multitenancy/src/main/java/sample/TenantAuthenticationManagerResolver.java b/samples/boot/oauth2resourceserver-multitenancy/src/main/java/sample/TenantAuthenticationManagerResolver.java new file mode 100644 index 0000000000..939bfc0b8b --- /dev/null +++ b/samples/boot/oauth2resourceserver-multitenancy/src/main/java/sample/TenantAuthenticationManagerResolver.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2020 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 sample; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationManagerResolver; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider; +import org.springframework.security.oauth2.server.resource.authentication.JwtBearerTokenAuthenticationConverter; +import org.springframework.security.oauth2.server.resource.authentication.OpaqueTokenAuthenticationProvider; +import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; +import org.springframework.stereotype.Component; + +@Component +public class TenantAuthenticationManagerResolver + implements AuthenticationManagerResolver { + + private AuthenticationManager jwt; + private AuthenticationManager opaqueToken; + + public TenantAuthenticationManagerResolver( + JwtDecoder jwtDecoder, OpaqueTokenIntrospector opaqueTokenIntrospector) { + + JwtAuthenticationProvider jwtAuthenticationProvider = new JwtAuthenticationProvider(jwtDecoder); + jwtAuthenticationProvider.setJwtAuthenticationConverter(new JwtBearerTokenAuthenticationConverter()); + this.jwt = new ProviderManager(jwtAuthenticationProvider); + this.opaqueToken = new ProviderManager(new OpaqueTokenAuthenticationProvider(opaqueTokenIntrospector)); + } + + @Override + public AuthenticationManager resolve(HttpServletRequest request) { + String tenant = request.getHeader("tenant"); + if ("one".equals(tenant)) { + return this.jwt; + } + if ("two".equals(tenant)) { + return this.opaqueToken; + } + throw new IllegalArgumentException("unknown tenant"); + } +} diff --git a/samples/boot/oauth2resourceserver-multitenancy/src/main/resources/application.yml b/samples/boot/oauth2resourceserver-multitenancy/src/main/resources/application.yml index de938e33b4..6447ad0d94 100644 --- a/samples/boot/oauth2resourceserver-multitenancy/src/main/resources/application.yml +++ b/samples/boot/oauth2resourceserver-multitenancy/src/main/resources/application.yml @@ -1,4 +1,10 @@ -tenantOne.jwk-set-uri: ${mockwebserver.url}/.well-known/jwks.json -tenantTwo.introspection-uri: ${mockwebserver.url}/introspect -tenantTwo.introspection-client-id: client -tenantTwo.introspection-client-secret: secret +spring: + security: + oauth2: + resourceserver: + jwt: + jwk-set-uri: ${mockwebserver.url}/.well-known/jwks.json + opaquetoken: + introspection-uri: ${mockwebserver.url}/introspect + client-id: client + client-secret: secret