diff --git a/samples/boot/oauth2login/src/integration-test/java/sample/OAuth2LoginApplicationTests.java b/samples/boot/oauth2login/src/integration-test/java/sample/OAuth2LoginApplicationTests.java index d9640ff6c1..42984ee7da 100644 --- a/samples/boot/oauth2login/src/integration-test/java/sample/OAuth2LoginApplicationTests.java +++ b/samples/boot/oauth2login/src/integration-test/java/sample/OAuth2LoginApplicationTests.java @@ -73,8 +73,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import static org.springframework.security.oauth2.core.oidc.IdTokenClaimNames.SUB; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.oidcLogin; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.oauth2Login; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; @@ -261,12 +260,12 @@ public class OAuth2LoginApplicationTests { } @Test - public void requestWhenMockOidcLoginThenIndex() throws Exception { + public void requestWhenMockOAuth2LoginThenIndex() throws Exception { ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("github"); - this.mvc.perform(get("/").with(oidcLogin().clientRegistration(clientRegistration))) + this.mvc.perform(get("/").with(oauth2Login().clientRegistration(clientRegistration))) .andExpect(model().attribute("userName", "test-subject")) .andExpect(model().attribute("clientName", "GitHub")) - .andExpect(model().attribute("userAttributes", Collections.singletonMap(SUB, "test-subject"))); + .andExpect(model().attribute("userAttributes", Collections.singletonMap("sub", "test-subject"))); } private void assertLoginPage(HtmlPage page) { diff --git a/samples/boot/oauth2login/src/test/java/sample/web/OAuth2LoginControllerTests.java b/samples/boot/oauth2login/src/test/java/sample/web/OAuth2LoginControllerTests.java index d70caa48d4..cd664893ac 100644 --- a/samples/boot/oauth2login/src/test/java/sample/web/OAuth2LoginControllerTests.java +++ b/samples/boot/oauth2login/src/test/java/sample/web/OAuth2LoginControllerTests.java @@ -34,8 +34,7 @@ import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; -import static org.springframework.security.oauth2.core.oidc.IdTokenClaimNames.SUB; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.oidcLogin; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.oauth2Login; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; @@ -64,10 +63,10 @@ public class OAuth2LoginControllerTests { @Test public void rootWhenAuthenticatedReturnsUserAndClient() throws Exception { - this.mvc.perform(get("/").with(oidcLogin())) + this.mvc.perform(get("/").with(oauth2Login())) .andExpect(model().attribute("userName", "test-subject")) .andExpect(model().attribute("clientName", "test")) - .andExpect(model().attribute("userAttributes", Collections.singletonMap(SUB, "test-subject"))); + .andExpect(model().attribute("userAttributes", Collections.singletonMap("sub", "test-subject"))); } @Test @@ -79,11 +78,11 @@ public class OAuth2LoginControllerTests { .tokenUri("https://token-uri.example.org") .build(); - this.mvc.perform(get("/").with(oidcLogin() + this.mvc.perform(get("/").with(oauth2Login() .clientRegistration(clientRegistration) - .idToken(i -> i.subject("spring-security")))) + .attributes(a -> a.put("sub", "spring-security")))) .andExpect(model().attribute("userName", "spring-security")) .andExpect(model().attribute("clientName", "my-client-name")) - .andExpect(model().attribute("userAttributes", Collections.singletonMap(SUB, "spring-security"))); + .andExpect(model().attribute("userAttributes", Collections.singletonMap("sub", "spring-security"))); } } diff --git a/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessors.java b/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessors.java index 4e7f0a2a64..854cf09fc9 100644 --- a/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessors.java +++ b/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessors.java @@ -33,6 +33,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Supplier; import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -70,6 +71,9 @@ import org.springframework.security.oauth2.core.oidc.OidcUserInfo; import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2UserAuthority; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; @@ -371,8 +375,38 @@ public final class SecurityMockMvcRequestPostProcessors { /** * Establish a {@link SecurityContext} that has a * {@link OAuth2AuthenticationToken} for the - * {@link Authentication} and a {@link OAuth2AuthorizedClient} in - * the session. All details are + * {@link Authentication}, a {@link OAuth2User} as the principal, + * and a {@link OAuth2AuthorizedClient} in the session. All details are + * declarative and do not require associated tokens to be valid. + * + *

+ * The support works by associating the authentication to the HttpServletRequest. To associate + * the request to the SecurityContextHolder you need to ensure that the + * SecurityContextPersistenceFilter is associated with the MockMvc instance. A few + * ways to do this are: + *

+ * + * + * + * @return the {@link OidcLoginRequestPostProcessor} for additional customization + * @since 5.3 + */ + public static OAuth2LoginRequestPostProcessor oauth2Login() { + OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "access-token", + null, null, Collections.singleton("user")); + return new OAuth2LoginRequestPostProcessor(accessToken); + } + + /** + * Establish a {@link SecurityContext} that has a + * {@link OAuth2AuthenticationToken} for the + * {@link Authentication}, a {@link OidcUser} as the principal, + * and a {@link OAuth2AuthorizedClient} in the session. All details are * declarative and do not require associated tokens to be valid. * *

@@ -1248,6 +1282,147 @@ public final class SecurityMockMvcRequestPostProcessors { } } + /** + * @author Josh Cummings + * @since 5.3 + */ + public final static class OAuth2LoginRequestPostProcessor implements RequestPostProcessor { + private ClientRegistration clientRegistration; + private OAuth2AccessToken accessToken; + + private Supplier> authorities = this::defaultAuthorities; + private Supplier> attributes = this::defaultAttributes; + private String nameAttributeKey = "sub"; + private Supplier oauth2User = this::defaultPrincipal; + + private OAuth2LoginRequestPostProcessor(OAuth2AccessToken accessToken) { + this.accessToken = accessToken; + this.clientRegistration = clientRegistrationBuilder().build(); + } + + /** + * Use the provided authorities in the {@link Authentication} + * + * @param authorities the authorities to use + * @return the {@link OAuth2LoginRequestPostProcessor} for further configuration + */ + public OAuth2LoginRequestPostProcessor authorities(Collection authorities) { + Assert.notNull(authorities, "authorities cannot be null"); + this.authorities = () -> authorities; + this.oauth2User = this::defaultPrincipal; + return this; + } + + /** + * Use the provided authorities in the {@link Authentication} + * + * @param authorities the authorities to use + * @return the {@link OAuth2LoginRequestPostProcessor} for further configuration + */ + public OAuth2LoginRequestPostProcessor authorities(GrantedAuthority... authorities) { + Assert.notNull(authorities, "authorities cannot be null"); + this.authorities = () -> Arrays.asList(authorities); + this.oauth2User = this::defaultPrincipal; + return this; + } + + /** + * Mutate the attributes using the given {@link Consumer} + * + * @param attributesConsumer The {@link Consumer} for mutating the {@Map} of attributes + * @return the {@link OAuth2LoginRequestPostProcessor} for further configuration + */ + public OAuth2LoginRequestPostProcessor attributes(Consumer> attributesConsumer) { + Assert.notNull(attributesConsumer, "attributesConsumer cannot be null"); + this.attributes = () -> { + Map attrs = new HashMap<>(); + attrs.put(this.nameAttributeKey, "test-subject"); + attributesConsumer.accept(attrs); + return attrs; + }; + this.oauth2User = this::defaultPrincipal; + return this; + } + + /** + * Use the provided key for the attribute containing the principal's name + * + * @param nameAttributeKey The attribute key to use + * @return the {@link OAuth2LoginRequestPostProcessor} for further configuration + */ + public OAuth2LoginRequestPostProcessor nameAttributeKey(String nameAttributeKey) { + Assert.notNull(nameAttributeKey, "nameAttributeKey cannot be null"); + this.nameAttributeKey = nameAttributeKey; + this.oauth2User = this::defaultPrincipal; + return this; + } + + /** + * Use the provided {@link OAuth2User} as the authenticated user. + * + * @param oauth2User the {@link OAuth2User} to use + * @return the {@link OAuth2LoginRequestPostProcessor} for further configuration + */ + public OAuth2LoginRequestPostProcessor oauth2User(OAuth2User oauth2User) { + this.oauth2User = () -> oauth2User; + return this; + } + + /** + * Use the provided {@link ClientRegistration} as the client to authorize. + * + * The supplied {@link ClientRegistration} will be registered into an + * {@link HttpSessionOAuth2AuthorizedClientRepository}. Tests relying on + * {@link org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient} + * annotations should register an {@link HttpSessionOAuth2AuthorizedClientRepository} bean + * to the application context. + * + * @param clientRegistration the {@link ClientRegistration} to use + * @return the {@link OAuth2LoginRequestPostProcessor} for further configuration + */ + public OAuth2LoginRequestPostProcessor clientRegistration(ClientRegistration clientRegistration) { + this.clientRegistration = clientRegistration; + return this; + } + + @Override + public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { + OAuth2User oauth2User = this.oauth2User.get(); + OAuth2AuthenticationToken token = new OAuth2AuthenticationToken + (oauth2User, oauth2User.getAuthorities(), this.clientRegistration.getRegistrationId()); + OAuth2AuthorizedClient client = new OAuth2AuthorizedClient + (this.clientRegistration, token.getName(), this.accessToken); + OAuth2AuthorizedClientRepository authorizedClientRepository = new HttpSessionOAuth2AuthorizedClientRepository(); + authorizedClientRepository.saveAuthorizedClient(client, token, request, new MockHttpServletResponse()); + + return new AuthenticationRequestPostProcessor(token).postProcessRequest(request); + } + + private ClientRegistration.Builder clientRegistrationBuilder() { + return ClientRegistration.withRegistrationId("test") + .authorizationGrantType(AuthorizationGrantType.PASSWORD) + .clientId("test-client") + .tokenUri("https://token-uri.example.org"); + } + + private Collection defaultAuthorities() { + Set authorities = new LinkedHashSet<>(); + authorities.add(new OAuth2UserAuthority(this.attributes.get())); + for (String authority : this.accessToken.getScopes()) { + authorities.add(new SimpleGrantedAuthority("SCOPE_" + authority)); + } + return authorities; + } + + private Map defaultAttributes() { + return Collections.singletonMap(this.nameAttributeKey, "test-subject"); + } + + private OAuth2User defaultPrincipal() { + return new DefaultOAuth2User(this.authorities.get(), this.attributes.get(), this.nameAttributeKey); + } + } + /** * @author Josh Cummings * @since 5.3 @@ -1350,12 +1525,10 @@ public final class SecurityMockMvcRequestPostProcessors { @Override public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { OidcUser oidcUser = getOidcUser(); - OAuth2AuthenticationToken token = new OAuth2AuthenticationToken(oidcUser, oidcUser.getAuthorities(), this.clientRegistration.getRegistrationId()); - OAuth2AuthorizedClient client = new OAuth2AuthorizedClient(this.clientRegistration, token.getName(), this.accessToken); - OAuth2AuthorizedClientRepository authorizedClientRepository = new HttpSessionOAuth2AuthorizedClientRepository(); - authorizedClientRepository.saveAuthorizedClient(client, token, request, new MockHttpServletResponse()); - - return new AuthenticationRequestPostProcessor(token).postProcessRequest(request); + return new OAuth2LoginRequestPostProcessor(this.accessToken) + .oauth2User(oidcUser) + .clientRegistration(this.clientRegistration) + .postProcessRequest(request); } private ClientRegistration.Builder clientRegistrationBuilder() { diff --git a/test/src/test/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessorsOAuth2LoginTests.java b/test/src/test/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessorsOAuth2LoginTests.java new file mode 100644 index 0000000000..2ba4cb600e --- /dev/null +++ b/test/src/test/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessorsOAuth2LoginTests.java @@ -0,0 +1,208 @@ +/* + * 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.test.web.servlet.request; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +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.core.GrantedAuthority; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import static org.mockito.Mockito.mock; +import static org.springframework.security.oauth2.client.registration.TestClientRegistrations.clientRegistration; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.oauth2Login; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Tests for {@link SecurityMockMvcRequestPostProcessors#oauth2Login()} + * + * @author Josh Cummings + * @since 5.3 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +@WebAppConfiguration +public class SecurityMockMvcRequestPostProcessorsOAuth2LoginTests { + @Autowired + WebApplicationContext context; + + MockMvc mvc; + + @Before + public void setup() { + // @formatter:off + this.mvc = MockMvcBuilders + .webAppContextSetup(this.context) + .apply(springSecurity()) + .build(); + // @formatter:on + } + + @Test + public void oauth2LoginWhenUsingDefaultsThenProducesDefaultAuthentication() + throws Exception { + + this.mvc.perform(get("/name").with(oauth2Login())) + .andExpect(content().string("test-subject")); + this.mvc.perform(get("/admin/id-token/name").with(oauth2Login())) + .andExpect(status().isForbidden()); + } + + @Test + public void oauth2LoginWhenUsingDefaultsThenProducesDefaultAuthorizedClient() + throws Exception { + + this.mvc.perform(get("/client-id").with(oauth2Login())) + .andExpect(content().string("test-client")); + } + + @Test + public void oauth2LoginWhenAuthoritiesSpecifiedThenGrantsAccess() throws Exception { + this.mvc.perform(get("/admin/scopes") + .with(oauth2Login().authorities(new SimpleGrantedAuthority("SCOPE_admin")))) + .andExpect(content().string("[\"SCOPE_admin\"]")); + } + + @Test + public void oauth2LoginWhenAttributeSpecifiedThenUserHasAttribute() throws Exception { + this.mvc.perform(get("/attributes/iss") + .with(oauth2Login().attributes(a -> a.put("iss", "https://idp.example.org")))) + .andExpect(content().string("https://idp.example.org")); + } + + @Test + public void oauth2LoginWhenNameSpecifiedThenUserHasName() throws Exception { + this.mvc.perform(get("/attributes/custom-attribute") + .with(oauth2Login().nameAttributeKey("custom-attribute"))) + .andExpect(content().string("test-subject")); + + this.mvc.perform(get("/name") + .with(oauth2Login().nameAttributeKey("custom-attribute"))) + .andExpect(content().string("test-subject")); + } + + @Test + public void oauth2LoginWhenClientRegistrationSpecifiedThenUses() throws Exception { + this.mvc.perform(get("/client-id") + .with(oauth2Login().clientRegistration(clientRegistration().build()))) + .andExpect(content().string("client-id")); + } + + @Test + public void oauth2LoginWhenOAuth2UserSpecifiedThenLastCalledTakesPrecedence() throws Exception { + OAuth2User oauth2User = new DefaultOAuth2User( + AuthorityUtils.createAuthorityList("SCOPE_user"), + Collections.singletonMap("username", "user"), + "username"); + + this.mvc.perform(get("/attributes/sub") + .with(oauth2Login() + .attributes(a -> a.put("sub", "bar")) + .oauth2User(oauth2User))) + .andExpect(status().isOk()) + .andExpect(content().string("no-attribute")); + this.mvc.perform(get("/attributes/sub") + .with(oauth2Login() + .oauth2User(oauth2User) + .attributes(a -> a.put("sub", "bar")))) + .andExpect(content().string("bar")); + } + + @EnableWebSecurity + @EnableWebMvc + static class OAuth2LoginConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeRequests(authorize -> authorize + .mvcMatchers("/admin/**").hasAuthority("SCOPE_admin") + .anyRequest().hasAuthority("SCOPE_user") + ).oauth2Login(); + } + + @Bean + ClientRegistrationRepository clientRegistrationRepository() { + return mock(ClientRegistrationRepository.class); + } + + @Bean + OAuth2AuthorizedClientRepository oAuth2AuthorizedClientRepository() { + return new HttpSessionOAuth2AuthorizedClientRepository(); + } + + @RestController + static class PrincipalController { + @GetMapping("/name") + String name(@AuthenticationPrincipal OAuth2User oauth2User) { + return oauth2User.getName(); + } + + @GetMapping("/client-id") + String authorizedClient(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient) { + return authorizedClient.getClientRegistration().getClientId(); + } + + @GetMapping("/attributes/{attribute}") + String attributes( + @AuthenticationPrincipal OAuth2User oauth2User, @PathVariable("attribute") String attribute) { + + return Optional.ofNullable((String) oauth2User.getAttribute(attribute)).orElse("no-attribute"); + } + + @GetMapping("/admin/scopes") + List scopes( + @AuthenticationPrincipal(expression = "authorities") Collection authorities) { + + return authorities.stream().map(GrantedAuthority::getAuthority) + .collect(Collectors.toList()); + } + } + } +}