|
@@ -16,16 +16,39 @@
|
|
|
|
|
|
package org.springframework.security.config.annotation.web.reactive;
|
|
|
|
|
|
+import java.net.URI;
|
|
|
+
|
|
|
import org.junit.jupiter.api.Test;
|
|
|
import org.junit.jupiter.api.extension.ExtendWith;
|
|
|
+import reactor.core.publisher.Mono;
|
|
|
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.context.ApplicationContext;
|
|
|
+import org.springframework.context.annotation.Bean;
|
|
|
import org.springframework.context.annotation.Configuration;
|
|
|
+import org.springframework.security.config.Customizer;
|
|
|
import org.springframework.security.config.test.SpringTestContext;
|
|
|
import org.springframework.security.config.test.SpringTestContextExtension;
|
|
|
import org.springframework.security.config.users.ReactiveAuthenticationTestConfiguration;
|
|
|
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
|
|
+import org.springframework.security.core.password.CompromisedPasswordCheckResult;
|
|
|
+import org.springframework.security.core.password.CompromisedPasswordException;
|
|
|
+import org.springframework.security.core.password.ReactiveCompromisedPasswordChecker;
|
|
|
+import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
|
|
|
+import org.springframework.security.core.userdetails.PasswordEncodedUser;
|
|
|
+import org.springframework.security.core.userdetails.User;
|
|
|
+import org.springframework.security.core.userdetails.UserDetails;
|
|
|
+import org.springframework.security.web.server.DefaultServerRedirectStrategy;
|
|
|
+import org.springframework.security.web.server.SecurityWebFilterChain;
|
|
|
+import org.springframework.test.web.reactive.server.WebTestClient;
|
|
|
+import org.springframework.util.LinkedMultiValueMap;
|
|
|
+import org.springframework.util.MultiValueMap;
|
|
|
+import org.springframework.web.reactive.config.EnableWebFlux;
|
|
|
+import org.springframework.web.reactive.function.BodyInserters;
|
|
|
+import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
|
|
|
|
|
|
import static org.assertj.core.api.Assertions.assertThat;
|
|
|
+import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf;
|
|
|
|
|
|
/**
|
|
|
* Tests for {@link ServerHttpSecurityConfiguration}.
|
|
@@ -37,6 +60,16 @@ public class ServerHttpSecurityConfigurationTests {
|
|
|
|
|
|
public final SpringTestContext spring = new SpringTestContext(this);
|
|
|
|
|
|
+ WebTestClient webClient;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ void setup(ApplicationContext context) {
|
|
|
+ if (!context.containsBean(WebHttpHandlerBuilder.WEB_HANDLER_BEAN_NAME)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.webClient = WebTestClient.bindToApplicationContext(context).configureClient().build();
|
|
|
+ }
|
|
|
+
|
|
|
@Test
|
|
|
public void loadConfigWhenReactiveUserDetailsServiceConfiguredThenServerHttpSecurityExists() {
|
|
|
this.spring
|
|
@@ -57,9 +90,151 @@ public class ServerHttpSecurityConfigurationTests {
|
|
|
assertThat(serverHttpSecurity).isNotNull();
|
|
|
}
|
|
|
|
|
|
+ @Test
|
|
|
+ void loginWhenCompromisePasswordCheckerConfiguredAndPasswordCompromisedThenUnauthorized() {
|
|
|
+ this.spring.register(FormLoginConfig.class, UserDetailsConfig.class, CompromisedPasswordCheckerConfig.class)
|
|
|
+ .autowire();
|
|
|
+ MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
|
|
|
+ data.add("username", "user");
|
|
|
+ data.add("password", "password");
|
|
|
+ // @formatter:off
|
|
|
+ this.webClient.mutateWith(csrf())
|
|
|
+ .post()
|
|
|
+ .uri("/login")
|
|
|
+ .body(BodyInserters.fromFormData(data))
|
|
|
+ .exchange()
|
|
|
+ .expectStatus().is3xxRedirection()
|
|
|
+ .expectHeader().location("/login?error");
|
|
|
+ // @formatter:on
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void loginWhenCompromisePasswordCheckerConfiguredAndPasswordNotCompromisedThenUnauthorized() {
|
|
|
+ this.spring.register(FormLoginConfig.class, UserDetailsConfig.class, CompromisedPasswordCheckerConfig.class)
|
|
|
+ .autowire();
|
|
|
+ MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
|
|
|
+ data.add("username", "admin");
|
|
|
+ data.add("password", "password2");
|
|
|
+ // @formatter:off
|
|
|
+ this.webClient.mutateWith(csrf())
|
|
|
+ .post()
|
|
|
+ .uri("/login")
|
|
|
+ .body(BodyInserters.fromFormData(data))
|
|
|
+ .exchange()
|
|
|
+ .expectStatus().is3xxRedirection()
|
|
|
+ .expectHeader().location("/");
|
|
|
+ // @formatter:on
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void loginWhenCompromisedPasswordAndRedirectIfPasswordExceptionThenRedirectedToResetPassword() {
|
|
|
+ this.spring
|
|
|
+ .register(FormLoginRedirectToResetPasswordConfig.class, UserDetailsConfig.class,
|
|
|
+ CompromisedPasswordCheckerConfig.class)
|
|
|
+ .autowire();
|
|
|
+ MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
|
|
|
+ data.add("username", "user");
|
|
|
+ data.add("password", "password");
|
|
|
+ // @formatter:off
|
|
|
+ this.webClient.mutateWith(csrf())
|
|
|
+ .post()
|
|
|
+ .uri("/login")
|
|
|
+ .body(BodyInserters.fromFormData(data))
|
|
|
+ .exchange()
|
|
|
+ .expectStatus().is3xxRedirection()
|
|
|
+ .expectHeader().location("/reset-password");
|
|
|
+ // @formatter:on
|
|
|
+ }
|
|
|
+
|
|
|
@Configuration
|
|
|
static class SubclassConfig extends ServerHttpSecurityConfiguration {
|
|
|
|
|
|
}
|
|
|
|
|
|
+ @Configuration(proxyBeanMethods = false)
|
|
|
+ @EnableWebFlux
|
|
|
+ @EnableWebFluxSecurity
|
|
|
+ static class FormLoginConfig {
|
|
|
+
|
|
|
+ @Bean
|
|
|
+ SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
|
|
|
+ // @formatter:off
|
|
|
+ http
|
|
|
+ .authorizeExchange((exchange) -> exchange
|
|
|
+ .anyExchange().authenticated()
|
|
|
+ )
|
|
|
+ .formLogin(Customizer.withDefaults());
|
|
|
+ // @formatter:on
|
|
|
+ return http.build();
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ @Configuration(proxyBeanMethods = false)
|
|
|
+ @EnableWebFlux
|
|
|
+ @EnableWebFluxSecurity
|
|
|
+ static class FormLoginRedirectToResetPasswordConfig {
|
|
|
+
|
|
|
+ @Bean
|
|
|
+ SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
|
|
|
+ // @formatter:off
|
|
|
+ http
|
|
|
+ .authorizeExchange((exchange) -> exchange
|
|
|
+ .anyExchange().authenticated()
|
|
|
+ )
|
|
|
+ .formLogin((form) -> form
|
|
|
+ .authenticationFailureHandler((webFilterExchange, exception) -> {
|
|
|
+ String redirectUrl = "/login?error";
|
|
|
+ if (exception instanceof CompromisedPasswordException) {
|
|
|
+ redirectUrl = "/reset-password";
|
|
|
+ }
|
|
|
+ return new DefaultServerRedirectStrategy().sendRedirect(webFilterExchange.getExchange(), URI.create(redirectUrl));
|
|
|
+ })
|
|
|
+ );
|
|
|
+ // @formatter:on
|
|
|
+ return http.build();
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ @Configuration(proxyBeanMethods = false)
|
|
|
+ static class UserDetailsConfig {
|
|
|
+
|
|
|
+ @Bean
|
|
|
+ MapReactiveUserDetailsService userDetailsService() {
|
|
|
+ // @formatter:off
|
|
|
+ UserDetails user = PasswordEncodedUser.user();
|
|
|
+ UserDetails admin = User.withDefaultPasswordEncoder()
|
|
|
+ .username("admin")
|
|
|
+ .password("password2")
|
|
|
+ .roles("USER", "ADMIN")
|
|
|
+ .build();
|
|
|
+ // @formatter:on
|
|
|
+ return new MapReactiveUserDetailsService(user, admin);
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ @Configuration(proxyBeanMethods = false)
|
|
|
+ static class CompromisedPasswordCheckerConfig {
|
|
|
+
|
|
|
+ @Bean
|
|
|
+ TestReactivePasswordChecker compromisedPasswordChecker() {
|
|
|
+ return new TestReactivePasswordChecker();
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ static class TestReactivePasswordChecker implements ReactiveCompromisedPasswordChecker {
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Mono<CompromisedPasswordCheckResult> check(String password) {
|
|
|
+ if ("password".equals(password)) {
|
|
|
+ return Mono.just(new CompromisedPasswordCheckResult(true));
|
|
|
+ }
|
|
|
+ return Mono.just(new CompromisedPasswordCheckResult(false));
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
}
|