|
21 | 21 | import java.util.List;
|
22 | 22 | import java.util.function.Supplier;
|
23 | 23 |
|
24 |
| -import org.jspecify.annotations.Nullable; |
25 | 24 | import org.junit.jupiter.api.Test;
|
26 | 25 | import org.junit.jupiter.api.extension.ExtendWith;
|
27 | 26 |
|
28 | 27 | import org.springframework.beans.factory.annotation.Autowired;
|
29 | 28 | import org.springframework.context.annotation.Bean;
|
30 | 29 | import org.springframework.context.annotation.Configuration;
|
| 30 | +import org.springframework.security.access.prepost.PreAuthorize; |
31 | 31 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
| 32 | +import org.springframework.security.authorization.AuthenticatedAuthorizationManager; |
32 | 33 | import org.springframework.security.authorization.AuthorityAuthorizationDecision;
|
| 34 | +import org.springframework.security.authorization.AuthorityAuthorizationManager; |
| 35 | +import org.springframework.security.authorization.AuthorizationDecision; |
33 | 36 | import org.springframework.security.authorization.AuthorizationManager;
|
| 37 | +import org.springframework.security.authorization.AuthorizationManagers; |
34 | 38 | import org.springframework.security.authorization.AuthorizationResult;
|
35 | 39 | import org.springframework.security.config.Customizer;
|
36 | 40 | import org.springframework.security.config.ObjectPostProcessor;
|
37 | 41 | import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
|
| 42 | +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; |
38 | 43 | import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
39 | 44 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
40 | 45 | import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
|
|
47 | 52 | import org.springframework.security.core.context.SecurityContextChangedListener;
|
48 | 53 | import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
49 | 54 | import org.springframework.security.core.userdetails.PasswordEncodedUser;
|
| 55 | +import org.springframework.security.core.userdetails.User; |
50 | 56 | import org.springframework.security.core.userdetails.UserDetails;
|
51 | 57 | import org.springframework.security.core.userdetails.UserDetailsService;
|
52 |
| -import org.springframework.security.crypto.password.NoOpPasswordEncoder; |
53 |
| -import org.springframework.security.crypto.password.PasswordEncoder; |
54 | 58 | import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
55 | 59 | import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders;
|
56 | 60 | import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors;
|
|
65 | 69 | import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler;
|
66 | 70 | import org.springframework.security.web.savedrequest.RequestCache;
|
67 | 71 | import org.springframework.test.web.servlet.MockMvc;
|
| 72 | +import org.springframework.web.bind.annotation.GetMapping; |
| 73 | +import org.springframework.web.bind.annotation.RestController; |
68 | 74 | import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
69 | 75 |
|
70 | 76 | import static org.hamcrest.Matchers.containsString;
|
|
78 | 84 | import static org.springframework.security.config.annotation.SecurityContextChangedListenerArgumentMatchers.setAuthentication;
|
79 | 85 | import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin;
|
80 | 86 | import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.logout;
|
| 87 | +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; |
81 | 88 | import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
|
82 | 89 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
83 | 90 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
@@ -409,57 +416,58 @@ public void configureWhenPortResolverBeanThenPortResolverUsed() throws Exception
|
409 | 416 |
|
410 | 417 | @Test
|
411 | 418 | void requestWhenUnauthenticatedThenRequiresTwoSteps() throws Exception {
|
412 |
| - this.spring.register(MfaDslConfig.class).autowire(); |
| 419 | + this.spring.register(MfaDslConfig.class, UserConfig.class).autowire(); |
413 | 420 | UserDetails user = PasswordEncodedUser.user();
|
414 |
| - this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.user(user))) |
| 421 | + this.mockMvc.perform(get("/profile").with(user(user))) |
415 | 422 | .andExpect(status().is3xxRedirection())
|
416 | 423 | .andExpect(redirectedUrl("http://localhost/login"));
|
417 | 424 | this.mockMvc
|
418 |
| - .perform(post("/ott/generate").param("username", "user") |
419 |
| - .with(SecurityMockMvcRequestPostProcessors.user(user)) |
| 425 | + .perform(post("/ott/generate").param("username", "rod") |
| 426 | + .with(user(user)) |
420 | 427 | .with(SecurityMockMvcRequestPostProcessors.csrf()))
|
421 | 428 | .andExpect(status().is3xxRedirection())
|
422 | 429 | .andExpect(redirectedUrl("/ott/sent"));
|
423 | 430 | this.mockMvc
|
424 |
| - .perform(post("/login").param("username", user.getUsername()) |
425 |
| - .param("password", user.getPassword()) |
| 431 | + .perform(post("/login").param("username", "rod") |
| 432 | + .param("password", "password") |
426 | 433 | .with(SecurityMockMvcRequestPostProcessors.csrf()))
|
427 | 434 | .andExpect(status().is3xxRedirection())
|
428 | 435 | .andExpect(redirectedUrl("/"));
|
429 | 436 | user = PasswordEncodedUser.withUserDetails(user).authorities("profile:read", "FACTOR_OTT").build();
|
430 |
| - this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.user(user))) |
| 437 | + this.mockMvc.perform(get("/profile").with(user(user))) |
431 | 438 | .andExpect(status().is3xxRedirection())
|
432 | 439 | .andExpect(redirectedUrl("http://localhost/login"));
|
433 | 440 | user = PasswordEncodedUser.withUserDetails(user).authorities("profile:read", "FACTOR_PASSWORD").build();
|
434 |
| - this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.user(user))) |
| 441 | + this.mockMvc.perform(get("/profile").with(user(user))) |
435 | 442 | .andExpect(status().isOk())
|
436 | 443 | .andExpect(content().string(containsString("/ott/generate")));
|
437 | 444 | user = PasswordEncodedUser.withUserDetails(user)
|
438 | 445 | .authorities("profile:read", "FACTOR_PASSWORD", "FACTOR_OTT")
|
439 | 446 | .build();
|
440 |
| - this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.user(user))) |
441 |
| - .andExpect(status().isNotFound()); |
| 447 | + this.mockMvc.perform(get("/profile").with(user(user))).andExpect(status().isNotFound()); |
442 | 448 | }
|
443 | 449 |
|
444 | 450 | @Test
|
445 | 451 | void requestWhenUnauthenticatedX509ThenRequiresTwoSteps() throws Exception {
|
446 |
| - this.spring.register(MfaDslX509Config.class).autowire(); |
447 |
| - this.mockMvc.perform(get("/")).andExpect(status().isForbidden()); |
| 452 | + this.spring.register(MfaDslX509Config.class, UserConfig.class, BasicController.class).autowire(); |
| 453 | + this.mockMvc.perform(get("/profile")).andExpect(status().isForbidden()); |
| 454 | + this.mockMvc.perform(get("/profile").with(user(User.withUsername("rod").authorities("profile:read").build()))) |
| 455 | + .andExpect(status().isForbidden()); |
448 | 456 | this.mockMvc.perform(get("/login")).andExpect(status().isOk());
|
449 |
| - this.mockMvc.perform(get("/").with(SecurityMockMvcRequestPostProcessors.x509("rod.cer"))) |
| 457 | + this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.x509("rod.cer"))) |
450 | 458 | .andExpect(status().is3xxRedirection())
|
451 | 459 | .andExpect(redirectedUrl("http://localhost/login"));
|
452 |
| - UserDetails user = PasswordEncodedUser.withUsername("rod") |
453 |
| - .password("password") |
454 |
| - .authorities("AUTHN_FORM") |
455 |
| - .build(); |
456 | 460 | this.mockMvc
|
457 |
| - .perform(post("/login").param("username", user.getUsername()) |
458 |
| - .param("password", user.getPassword()) |
| 461 | + .perform(post("/login").param("username", "rod") |
| 462 | + .param("password", "password") |
459 | 463 | .with(SecurityMockMvcRequestPostProcessors.x509("rod.cer"))
|
460 | 464 | .with(SecurityMockMvcRequestPostProcessors.csrf()))
|
461 | 465 | .andExpect(status().is3xxRedirection())
|
462 | 466 | .andExpect(redirectedUrl("/"));
|
| 467 | + UserDetails authorized = PasswordEncodedUser.withUsername("rod") |
| 468 | + .authorities("profile:read", "FACTOR_X509", "FACTOR_PASSWORD") |
| 469 | + .build(); |
| 470 | + this.mockMvc.perform(get("/profile").with(user(authorized))).andExpect(status().isOk()); |
463 | 471 | }
|
464 | 472 |
|
465 | 473 | @Configuration
|
@@ -832,75 +840,101 @@ public <O> O postProcess(O object) {
|
832 | 840 | static class MfaDslConfig {
|
833 | 841 |
|
834 | 842 | @Bean
|
835 |
| - SecurityFilterChain filterChain(HttpSecurity http) throws Exception { |
| 843 | + SecurityFilterChain filterChain(HttpSecurity http, AuthorizationManagerFactory authz) throws Exception { |
836 | 844 | // @formatter:off
|
837 | 845 | http
|
838 | 846 | .formLogin(Customizer.withDefaults())
|
839 | 847 | .oneTimeTokenLogin(Customizer.withDefaults())
|
840 | 848 | .authorizeHttpRequests((authorize) -> authorize
|
841 |
| - .requestMatchers("/profile").access( |
842 |
| - new HasAllAuthoritiesAuthorizationManager<>("profile:read", "FACTOR_PASSWORD", "FACTOR_OTT") |
843 |
| - ) |
844 |
| - .anyRequest().access(new HasAllAuthoritiesAuthorizationManager<>("FACTOR_PASSWORD", "FACTOR_OTT")) |
| 849 | + .requestMatchers("/profile").access(authz.hasAuthority("profile:read")) |
| 850 | + .anyRequest().access(authz.authenticated()) |
845 | 851 | );
|
846 | 852 | return http.build();
|
847 | 853 | // @formatter:on
|
848 | 854 | }
|
849 | 855 |
|
850 | 856 | @Bean
|
851 |
| - UserDetailsService users() { |
852 |
| - return new InMemoryUserDetailsManager(PasswordEncodedUser.user()); |
| 857 | + OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler() { |
| 858 | + return new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent"); |
853 | 859 | }
|
854 | 860 |
|
855 | 861 | @Bean
|
856 |
| - PasswordEncoder encoder() { |
857 |
| - return NoOpPasswordEncoder.getInstance(); |
858 |
| - } |
859 |
| - |
860 |
| - @Bean |
861 |
| - OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler() { |
862 |
| - return new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent"); |
| 862 | + AuthorizationManagerFactory authz() { |
| 863 | + return new AuthorizationManagerFactory("FACTOR_PASSWORD", "FACTOR_OTT"); |
863 | 864 | }
|
864 | 865 |
|
865 | 866 | }
|
866 | 867 |
|
867 | 868 | @Configuration
|
868 | 869 | @EnableWebSecurity
|
| 870 | + @EnableMethodSecurity |
869 | 871 | static class MfaDslX509Config {
|
870 | 872 |
|
871 | 873 | @Bean
|
872 |
| - SecurityFilterChain filterChain(HttpSecurity http) throws Exception { |
| 874 | + SecurityFilterChain filterChain(HttpSecurity http, AuthorizationManagerFactory authz) throws Exception { |
873 | 875 | // @formatter:off
|
874 | 876 | http
|
875 |
| - .formLogin(Customizer.withDefaults()) |
876 | 877 | .x509(Customizer.withDefaults())
|
| 878 | + .formLogin(Customizer.withDefaults()) |
877 | 879 | .authorizeHttpRequests((authorize) -> authorize
|
878 |
| - .anyRequest().access( |
879 |
| - new HasAllAuthoritiesAuthorizationManager<>("FACTOR_X509", "FACTOR_PASSWORD") |
880 |
| - ) |
| 880 | + .anyRequest().access(authz.authenticated()) |
881 | 881 | );
|
882 | 882 | return http.build();
|
883 | 883 | // @formatter:on
|
884 | 884 | }
|
885 | 885 |
|
886 | 886 | @Bean
|
887 |
| - UserDetailsService users() { |
888 |
| - return new InMemoryUserDetailsManager( |
889 |
| - PasswordEncodedUser.withUsername("rod").password("{noop}password").build()); |
| 887 | + AuthorizationManagerFactory authz() { |
| 888 | + return new AuthorizationManagerFactory("FACTOR_X509", "FACTOR_PASSWORD"); |
| 889 | + } |
| 890 | + |
| 891 | + } |
| 892 | + |
| 893 | + @Configuration |
| 894 | + static class UserConfig { |
| 895 | + |
| 896 | + @Bean |
| 897 | + UserDetails rod() { |
| 898 | + return PasswordEncodedUser.withUsername("rod").password("password").build(); |
| 899 | + } |
| 900 | + |
| 901 | + @Bean |
| 902 | + UserDetailsService users(UserDetails user) { |
| 903 | + return new InMemoryUserDetailsManager(user); |
890 | 904 | }
|
891 | 905 |
|
892 | 906 | }
|
893 | 907 |
|
894 |
| - private static final class HasAllAuthoritiesAuthorizationManager<C> implements AuthorizationManager<C> { |
| 908 | + @RestController |
| 909 | + static class BasicController { |
| 910 | + |
| 911 | + @GetMapping("/profile") |
| 912 | + @PreAuthorize("@authz.hasAuthority('profile:read')") |
| 913 | + String profile() { |
| 914 | + return "profile"; |
| 915 | + } |
| 916 | + |
| 917 | + } |
| 918 | + |
| 919 | + public static class AuthorizationManagerFactory { |
895 | 920 |
|
896 | 921 | private final Collection<String> authorities;
|
897 | 922 |
|
898 |
| - private HasAllAuthoritiesAuthorizationManager(String... authorities) { |
| 923 | + AuthorizationManagerFactory(String... authorities) { |
899 | 924 | this.authorities = List.of(authorities);
|
900 | 925 | }
|
901 | 926 |
|
902 |
| - @Override |
903 |
| - public @Nullable AuthorizationResult authorize(Supplier<Authentication> authentication, C object) { |
| 927 | + public <T> AuthorizationManager<T> authenticated() { |
| 928 | + AuthenticatedAuthorizationManager<T> authenticated = AuthenticatedAuthorizationManager.authenticated(); |
| 929 | + return AuthorizationManagers.allOf(new AuthorizationDecision(false), this::factors, authenticated); |
| 930 | + } |
| 931 | + |
| 932 | + public <T> AuthorizationManager<T> hasAuthority(String authority) { |
| 933 | + AuthorityAuthorizationManager<T> authorized = AuthorityAuthorizationManager.hasAuthority(authority); |
| 934 | + return AuthorizationManagers.allOf(new AuthorizationDecision(false), this::factors, authorized); |
| 935 | + } |
| 936 | + |
| 937 | + private <T> AuthorizationResult factors(Supplier<Authentication> authentication, T context) { |
904 | 938 | List<String> authorities = authentication.get()
|
905 | 939 | .getAuthorities()
|
906 | 940 | .stream()
|
|
0 commit comments