|
16 | 16 |
|
17 | 17 | package org.springframework.security.config.annotation.web.configurers;
|
18 | 18 |
|
| 19 | +import java.time.Duration; |
| 20 | + |
19 | 21 | import org.junit.jupiter.api.Test;
|
20 | 22 | import org.junit.jupiter.api.extension.ExtendWith;
|
21 | 23 |
|
22 | 24 | import org.springframework.beans.factory.annotation.Autowired;
|
23 | 25 | import org.springframework.context.annotation.Bean;
|
24 | 26 | import org.springframework.context.annotation.Configuration;
|
25 | 27 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
| 28 | +import org.springframework.security.config.Customizer; |
26 | 29 | import org.springframework.security.config.ObjectPostProcessor;
|
27 | 30 | import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
|
28 | 31 | import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
|
34 | 37 | import org.springframework.security.core.context.SecurityContextChangedListener;
|
35 | 38 | import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
36 | 39 | import org.springframework.security.core.userdetails.PasswordEncodedUser;
|
| 40 | +import org.springframework.security.core.userdetails.UserDetails; |
37 | 41 | import org.springframework.security.core.userdetails.UserDetailsService;
|
| 42 | +import org.springframework.security.crypto.password.NoOpPasswordEncoder; |
| 43 | +import org.springframework.security.crypto.password.PasswordEncoder; |
38 | 44 | import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
39 | 45 | import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders;
|
| 46 | +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors; |
40 | 47 | import org.springframework.security.web.PortMapper;
|
41 | 48 | import org.springframework.security.web.PortResolver;
|
42 | 49 | import org.springframework.security.web.SecurityFilterChain;
|
43 | 50 | import org.springframework.security.web.access.ExceptionTranslationFilter;
|
44 | 51 | import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
45 | 52 | import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
|
46 | 53 | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
| 54 | +import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler; |
| 55 | +import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler; |
47 | 56 | import org.springframework.security.web.savedrequest.RequestCache;
|
48 | 57 | import org.springframework.test.web.servlet.MockMvc;
|
49 | 58 | import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
50 | 59 |
|
| 60 | +import static org.hamcrest.Matchers.containsString; |
51 | 61 | import static org.mockito.ArgumentMatchers.any;
|
52 | 62 | import static org.mockito.BDDMockito.given;
|
53 | 63 | import static org.mockito.Mockito.atLeastOnce;
|
|
61 | 71 | import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
|
62 | 72 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
63 | 73 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
| 74 | +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; |
64 | 75 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl;
|
65 | 76 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
|
66 | 77 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
@@ -386,6 +397,59 @@ public void configureWhenPortResolverBeanThenPortResolverUsed() throws Exception
|
386 | 397 | verify(this.spring.getContext().getBean(PortResolver.class)).getServerPort(any());
|
387 | 398 | }
|
388 | 399 |
|
| 400 | + @Test |
| 401 | + void requestWhenUnauthenticatedThenRequiresTwoSteps() throws Exception { |
| 402 | + this.spring.register(MfaDslConfig.class).autowire(); |
| 403 | + UserDetails user = PasswordEncodedUser.user(); |
| 404 | + this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.user(user))) |
| 405 | + .andExpect(status().is3xxRedirection()) |
| 406 | + .andExpect(redirectedUrl("http://localhost/login")); |
| 407 | + this.mockMvc |
| 408 | + .perform(post("/ott/generate").param("username", "user") |
| 409 | + .with(SecurityMockMvcRequestPostProcessors.user(user)) |
| 410 | + .with(SecurityMockMvcRequestPostProcessors.csrf())) |
| 411 | + .andExpect(status().is3xxRedirection()) |
| 412 | + .andExpect(redirectedUrl("/ott/sent")); |
| 413 | + this.mockMvc |
| 414 | + .perform(post("/login").param("username", user.getUsername()) |
| 415 | + .param("password", user.getPassword()) |
| 416 | + .with(SecurityMockMvcRequestPostProcessors.csrf())) |
| 417 | + .andExpect(status().is3xxRedirection()) |
| 418 | + .andExpect(redirectedUrl("/")); |
| 419 | + user = PasswordEncodedUser.withUserDetails(user).authorities("profile:read", "AUTHN_OTT").build(); |
| 420 | + this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.user(user))) |
| 421 | + .andExpect(status().is3xxRedirection()) |
| 422 | + .andExpect(redirectedUrl("http://localhost/login")); |
| 423 | + user = PasswordEncodedUser.withUserDetails(user).authorities("profile:read", "AUTHN_FORM").build(); |
| 424 | + this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.user(user))) |
| 425 | + .andExpect(status().isOk()) |
| 426 | + .andExpect(content().string(containsString("/ott/generate"))); |
| 427 | + user = PasswordEncodedUser.withUserDetails(user).authorities("profile:read", "AUTHN_FORM", "AUTHN_OTT").build(); |
| 428 | + this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.user(user))) |
| 429 | + .andExpect(status().isNotFound()); |
| 430 | + } |
| 431 | + |
| 432 | + @Test |
| 433 | + void requestWhenUnauthenticatedX509ThenRequiresTwoSteps() throws Exception { |
| 434 | + this.spring.register(MfaDslX509Config.class).autowire(); |
| 435 | + this.mockMvc.perform(get("/")).andExpect(status().isForbidden()); |
| 436 | + this.mockMvc.perform(get("/login")).andExpect(status().isOk()); |
| 437 | + this.mockMvc.perform(get("/").with(SecurityMockMvcRequestPostProcessors.x509("rod.cer"))) |
| 438 | + .andExpect(status().is3xxRedirection()) |
| 439 | + .andExpect(redirectedUrl("http://localhost/login")); |
| 440 | + UserDetails user = PasswordEncodedUser.withUsername("rod") |
| 441 | + .password("password") |
| 442 | + .authorities("AUTHN_FORM") |
| 443 | + .build(); |
| 444 | + this.mockMvc |
| 445 | + .perform(post("/login").param("username", user.getUsername()) |
| 446 | + .param("password", user.getPassword()) |
| 447 | + .with(SecurityMockMvcRequestPostProcessors.x509("rod.cer")) |
| 448 | + .with(SecurityMockMvcRequestPostProcessors.csrf())) |
| 449 | + .andExpect(status().is3xxRedirection()) |
| 450 | + .andExpect(redirectedUrl("/")); |
| 451 | + } |
| 452 | + |
389 | 453 | @Configuration
|
390 | 454 | @EnableWebSecurity
|
391 | 455 | static class RequestCacheConfig {
|
@@ -751,4 +815,64 @@ public <O> O postProcess(O object) {
|
751 | 815 |
|
752 | 816 | }
|
753 | 817 |
|
| 818 | + @Configuration |
| 819 | + @EnableWebSecurity |
| 820 | + static class MfaDslConfig { |
| 821 | + |
| 822 | + private static final Duration FIVE_MINUTES = Duration.ofMinutes(5); |
| 823 | + |
| 824 | + @Bean |
| 825 | + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { |
| 826 | + // @formatter:off |
| 827 | + http |
| 828 | + .formLogin((form) -> form.factor((f) -> f.grants(FIVE_MINUTES, "profile:read"))) |
| 829 | + .oneTimeTokenLogin((ott) -> ott.factor(Customizer.withDefaults())) |
| 830 | + .authorizeHttpRequests((authorize) -> authorize |
| 831 | + .requestMatchers("/profile").hasAuthority("profile:read") |
| 832 | + .anyRequest().authenticated() |
| 833 | + ); |
| 834 | + return http.build(); |
| 835 | + // @formatter:on |
| 836 | + } |
| 837 | + |
| 838 | + @Bean |
| 839 | + UserDetailsService users() { |
| 840 | + return new InMemoryUserDetailsManager(PasswordEncodedUser.user()); |
| 841 | + } |
| 842 | + |
| 843 | + @Bean |
| 844 | + PasswordEncoder encoder() { |
| 845 | + return NoOpPasswordEncoder.getInstance(); |
| 846 | + } |
| 847 | + |
| 848 | + @Bean |
| 849 | + OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler() { |
| 850 | + return new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent"); |
| 851 | + } |
| 852 | + |
| 853 | + } |
| 854 | + |
| 855 | + @Configuration |
| 856 | + @EnableWebSecurity |
| 857 | + static class MfaDslX509Config { |
| 858 | + |
| 859 | + @Bean |
| 860 | + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { |
| 861 | + // @formatter:off |
| 862 | + http |
| 863 | + .formLogin((form) -> form.factor(Customizer.withDefaults())) |
| 864 | + .x509((x509) -> x509.factor(Customizer.withDefaults())) |
| 865 | + .authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated()); |
| 866 | + return http.build(); |
| 867 | + // @formatter:on |
| 868 | + } |
| 869 | + |
| 870 | + @Bean |
| 871 | + UserDetailsService users() { |
| 872 | + return new InMemoryUserDetailsManager( |
| 873 | + PasswordEncodedUser.withUsername("rod").password("{noop}password").build()); |
| 874 | + } |
| 875 | + |
| 876 | + } |
| 877 | + |
754 | 878 | }
|
0 commit comments