From 982f3f902cafac52ef4588bf0b67290a21aa9135 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Mon, 13 Jan 2020 17:49:52 -0700 Subject: [PATCH] Add oauth2Login Reactive Test Support Fixes gh-7828 --- .../sample/OAuth2LoginApplicationTests.java | 6 +- .../sample/OAuth2LoginControllerTests.java | 6 +- .../server/SecurityMockServerConfigurers.java | 191 +++++++++++++++++- ...MockServerConfigurersOAuth2LoginTests.java | 182 +++++++++++++++++ 4 files changed, 378 insertions(+), 7 deletions(-) create mode 100644 test/src/test/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurersOAuth2LoginTests.java diff --git a/samples/boot/oauth2login-webflux/src/integration-test/java/sample/OAuth2LoginApplicationTests.java b/samples/boot/oauth2login-webflux/src/integration-test/java/sample/OAuth2LoginApplicationTests.java index f694edb002..32bb75fad8 100644 --- a/samples/boot/oauth2login-webflux/src/integration-test/java/sample/OAuth2LoginApplicationTests.java +++ b/samples/boot/oauth2login-webflux/src/integration-test/java/sample/OAuth2LoginApplicationTests.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. @@ -30,7 +30,7 @@ import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.reactive.server.WebTestClient; import static org.hamcrest.core.StringContains.containsString; -import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockOidcLogin; +import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockOAuth2Login; /** * Tests for {@link ReactiveOAuth2LoginApplication} @@ -58,7 +58,7 @@ public class OAuth2LoginApplicationTests { public void requestWhenMockOidcLoginThenIndex() { this.clientRegistrationRepository.findByRegistrationId("github") .map(clientRegistration -> - this.test.mutateWith(mockOidcLogin().clientRegistration(clientRegistration)) + this.test.mutateWith(mockOAuth2Login().clientRegistration(clientRegistration)) .get().uri("/") .exchange() .expectBody(String.class).value(containsString("GitHub")) diff --git a/samples/boot/oauth2login-webflux/src/test/java/sample/OAuth2LoginControllerTests.java b/samples/boot/oauth2login-webflux/src/test/java/sample/OAuth2LoginControllerTests.java index 5c7d2ce1f8..7b3dd76c47 100644 --- a/samples/boot/oauth2login-webflux/src/test/java/sample/OAuth2LoginControllerTests.java +++ b/samples/boot/oauth2login-webflux/src/test/java/sample/OAuth2LoginControllerTests.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. @@ -36,7 +36,7 @@ import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.web.reactive.result.view.ViewResolver; import static org.hamcrest.Matchers.containsString; -import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockOidcLogin; +import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockOAuth2Login; import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity; /** @@ -77,7 +77,7 @@ public class OAuth2LoginControllerTests { @Test public void indexGreetsAuthenticatedUser() { - this.rest.mutateWith(mockOidcLogin()) + this.rest.mutateWith(mockOAuth2Login()) .get().uri("/").exchange() .expectBody(String.class).value(containsString("test-subject")); } diff --git a/test/src/main/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurers.java b/test/src/main/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurers.java index 975d6ea82c..fa49533a4e 100644 --- a/test/src/main/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurers.java +++ b/test/src/main/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurers.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. @@ -19,8 +19,10 @@ package org.springframework.security.test.web.reactive.server; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.function.Consumer; import java.util.function.Supplier; @@ -52,6 +54,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.JwtAuthenticationToken; import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; @@ -147,6 +152,21 @@ public class SecurityMockServerConfigurers { return new JwtMutator(); } + /** + * Updates the ServerWebExchange to establish a {@link SecurityContext} that has a + * {@link OAuth2AuthenticationToken} for the + * {@link Authentication}. All details are + * declarative and do not require the corresponding OAuth 2.0 tokens to be valid. + * + * @return the {@link OAuth2LoginMutator} to further configure or use + * @since 5.3 + */ + public static OAuth2LoginMutator mockOAuth2Login() { + OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "access-token", + null, null, Collections.singleton("user")); + return new OAuth2LoginMutator(accessToken); + } + /** * Updates the ServerWebExchange to establish a {@link SecurityContext} that has a * {@link OAuth2AuthenticationToken} for the @@ -462,6 +482,175 @@ public class SecurityMockServerConfigurers { } } + /** + * @author Josh Cummings + * @since 5.3 + */ + public final static class OAuth2LoginMutator implements WebTestClientConfigurer, MockServerConfigurer { + 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 final ServerOAuth2AuthorizedClientRepository authorizedClientRepository = + new WebSessionServerOAuth2AuthorizedClientRepository(); + + private OAuth2LoginMutator(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 OAuth2LoginMutator} for further configuration + */ + public OAuth2LoginMutator 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 OAuth2LoginMutator} for further configuration + */ + public OAuth2LoginMutator 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 OAuth2LoginMutator} for further configuration + */ + public OAuth2LoginMutator 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 OAuth2LoginMutator} for further configuration + */ + public OAuth2LoginMutator 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 OAuth2LoginMutator} for further configuration + */ + public OAuth2LoginMutator 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 WebSessionServerOAuth2AuthorizedClientRepository}. Tests relying on + * {@link org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient} + * annotations should register an {@link WebSessionServerOAuth2AuthorizedClientRepository} bean + * to the application context. + * + * @param clientRegistration the {@link ClientRegistration} to use + * @return the {@link OAuth2LoginMutator} for further configuration + */ + public OAuth2LoginMutator clientRegistration(ClientRegistration clientRegistration) { + this.clientRegistration = clientRegistration; + return this; + } + + @Override + public void beforeServerCreated(WebHttpHandlerBuilder builder) { + OAuth2AuthenticationToken token = getToken(); + builder.filters(addAuthorizedClientFilter(token)); + mockAuthentication(getToken()).beforeServerCreated(builder); + } + + @Override + public void afterConfigureAdded(WebTestClient.MockServerSpec serverSpec) { + mockAuthentication(getToken()).afterConfigureAdded(serverSpec); + } + + @Override + public void afterConfigurerAdded( + WebTestClient.Builder builder, + @Nullable WebHttpHandlerBuilder httpHandlerBuilder, + @Nullable ClientHttpConnector connector) { + OAuth2AuthenticationToken token = getToken(); + httpHandlerBuilder.filters(addAuthorizedClientFilter(token)); + mockAuthentication(token).afterConfigurerAdded(builder, httpHandlerBuilder, connector); + } + + private Consumer> addAuthorizedClientFilter(OAuth2AuthenticationToken token) { + OAuth2AuthorizedClient client = getClient(); + return filters -> filters.add(0, (exchange, chain) -> + this.authorizedClientRepository.saveAuthorizedClient(client, token, exchange) + .then(chain.filter(exchange))); + } + + private OAuth2AuthenticationToken getToken() { + OAuth2User oauth2User = this.oauth2User.get(); + return new OAuth2AuthenticationToken(oauth2User, oauth2User.getAuthorities(), this.clientRegistration.getRegistrationId()); + } + + private OAuth2AuthorizedClient getClient() { + return new OAuth2AuthorizedClient(this.clientRegistration, getToken().getName(), this.accessToken); + } + + 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 diff --git a/test/src/test/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurersOAuth2LoginTests.java b/test/src/test/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurersOAuth2LoginTests.java new file mode 100644 index 0000000000..10b03d173c --- /dev/null +++ b/test/src/test/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurersOAuth2LoginTests.java @@ -0,0 +1,182 @@ +/* + * 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 org.springframework.security.test.web.reactive.server; + +import java.util.Collection; +import java.util.Collections; + +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.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.security.core.GrantedAuthority; +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.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.reactive.result.method.annotation.OAuth2AuthorizedClientArgumentResolver; +import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.client.web.server.WebSessionServerOAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.web.server.context.SecurityContextServerWebExchangeWebFilter; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockOAuth2Login; +import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity; + +@RunWith(MockitoJUnitRunner.class) +public class SecurityMockServerConfigurersOAuth2LoginTests extends AbstractMockServerConfigurersTests { + private OAuth2LoginController controller = new OAuth2LoginController(); + + @Mock + private ReactiveClientRegistrationRepository clientRegistrationRepository; + + private WebTestClient client; + + @Before + public void setup() { + ServerOAuth2AuthorizedClientRepository authorizedClientRepository = + new WebSessionServerOAuth2AuthorizedClientRepository(); + + this.client = WebTestClient + .bindToController(this.controller) + .argumentResolvers(c -> c.addCustomResolver( + new OAuth2AuthorizedClientArgumentResolver + (this.clientRegistrationRepository, authorizedClientRepository))) + .webFilter(new SecurityContextServerWebExchangeWebFilter()) + .apply(springSecurity()) + .configureClient() + .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .build(); + } + + @Test + public void oauth2LoginWhenUsingDefaultsThenProducesDefaultAuthentication() { + this.client.mutateWith(mockOAuth2Login()) + .get().uri("/token") + .exchange() + .expectStatus().isOk(); + + OAuth2AuthenticationToken token = this.controller.token; + assertThat(token).isNotNull(); + assertThat(token.getAuthorizedClientRegistrationId()).isEqualTo("test"); + assertThat(token.getPrincipal()).isInstanceOf(OAuth2User.class); + assertThat(token.getPrincipal().getAttributes()) + .containsEntry("sub", "test-subject"); + assertThat((Collection) token.getPrincipal().getAuthorities()) + .contains(new SimpleGrantedAuthority("SCOPE_user")); + } + + @Test + public void oauth2LoginWhenUsingDefaultsThenProducesDefaultAuthorizedClient() { + this.client.mutateWith(mockOAuth2Login()) + .get().uri("/client") + .exchange() + .expectStatus().isOk(); + + OAuth2AuthorizedClient client = this.controller.authorizedClient; + assertThat(client).isNotNull(); + assertThat(client.getClientRegistration().getRegistrationId()).isEqualTo("test"); + assertThat(client.getAccessToken().getTokenValue()).isEqualTo("access-token"); + assertThat(client.getRefreshToken()).isNull(); + } + + @Test + public void oauth2LoginWhenAuthoritiesSpecifiedThenGrantsAccess() { + this.client.mutateWith(mockOAuth2Login() + .authorities(new SimpleGrantedAuthority("SCOPE_admin"))) + .get().uri("/token") + .exchange() + .expectStatus().isOk(); + + OAuth2AuthenticationToken token = this.controller.token; + assertThat((Collection) token.getPrincipal().getAuthorities()) + .contains(new SimpleGrantedAuthority("SCOPE_admin")); + } + + @Test + public void oauth2LoginWhenAttributeSpecifiedThenUserHasAttribute() { + this.client.mutateWith(mockOAuth2Login() + .attributes(a -> a.put("iss", "https://idp.example.org"))) + .get().uri("/token") + .exchange() + .expectStatus().isOk(); + + OAuth2AuthenticationToken token = this.controller.token; + assertThat(token.getPrincipal().getAttributes()) + .containsEntry("iss", "https://idp.example.org"); + } + + @Test + public void oauth2LoginWhenOAuth2UserSpecifiedThenLastCalledTakesPrecedence() throws Exception { + OAuth2User oauth2User = new DefaultOAuth2User( + AuthorityUtils.createAuthorityList("SCOPE_user"), + Collections.singletonMap("sub", "subject"), + "sub"); + + this.client.mutateWith(mockOAuth2Login() + .attributes(a -> a.put("subject", "foo")) + .oauth2User(oauth2User)) + .get().uri("/token") + .exchange() + .expectStatus().isOk(); + + OAuth2AuthenticationToken token = this.controller.token; + assertThat(token.getPrincipal().getAttributes()) + .containsEntry("sub", "subject"); + + this.client.mutateWith(mockOAuth2Login() + .oauth2User(oauth2User) + .attributes(a -> a.put("sub", "bar"))) + .get().uri("/token") + .exchange() + .expectStatus().isOk(); + + token = this.controller.token; + assertThat(token.getPrincipal().getAttributes()) + .containsEntry("sub", "bar"); + } + + @RestController + static class OAuth2LoginController { + volatile OAuth2AuthenticationToken token; + volatile OAuth2AuthorizedClient authorizedClient; + + @GetMapping("/token") + OAuth2AuthenticationToken token(OAuth2AuthenticationToken token) { + this.token = token; + return token; + } + + @GetMapping("/client") + String authorizedClient + (@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient) { + this.authorizedClient = authorizedClient; + return authorizedClient.getPrincipalName(); + } + } +}