|
28 | 28 | import org.springframework.beans.factory.annotation.Autowired; |
29 | 29 | import org.springframework.context.annotation.Bean; |
30 | 30 | import org.springframework.context.annotation.Configuration; |
| 31 | +import org.springframework.security.access.prepost.PreAuthorize; |
31 | 32 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; |
| 33 | +import org.springframework.security.authorization.AuthenticatedAuthorizationManager; |
32 | 34 | import org.springframework.security.authorization.AuthorityAuthorizationDecision; |
| 35 | +import org.springframework.security.authorization.AuthorityAuthorizationManager; |
| 36 | +import org.springframework.security.authorization.AuthorizationDecision; |
33 | 37 | import org.springframework.security.authorization.AuthorizationManager; |
| 38 | +import org.springframework.security.authorization.AuthorizationManagers; |
34 | 39 | import org.springframework.security.authorization.AuthorizationResult; |
35 | 40 | import org.springframework.security.config.Customizer; |
36 | 41 | import org.springframework.security.config.ObjectPostProcessor; |
37 | 42 | import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig; |
| 43 | +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; |
38 | 44 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; |
39 | 45 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; |
40 | 46 | import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; |
|
47 | 53 | import org.springframework.security.core.context.SecurityContextChangedListener; |
48 | 54 | import org.springframework.security.core.context.SecurityContextHolderStrategy; |
49 | 55 | import org.springframework.security.core.userdetails.PasswordEncodedUser; |
| 56 | +import org.springframework.security.core.userdetails.User; |
50 | 57 | import org.springframework.security.core.userdetails.UserDetails; |
51 | 58 | import org.springframework.security.core.userdetails.UserDetailsService; |
52 | | -import org.springframework.security.crypto.password.NoOpPasswordEncoder; |
53 | | -import org.springframework.security.crypto.password.PasswordEncoder; |
54 | 59 | import org.springframework.security.provisioning.InMemoryUserDetailsManager; |
55 | 60 | import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders; |
56 | 61 | import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors; |
|
64 | 69 | import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler; |
65 | 70 | import org.springframework.security.web.savedrequest.RequestCache; |
66 | 71 | import org.springframework.test.web.servlet.MockMvc; |
| 72 | +import org.springframework.web.bind.annotation.GetMapping; |
| 73 | +import org.springframework.web.bind.annotation.RestController; |
67 | 74 | import org.springframework.web.servlet.config.annotation.EnableWebMvc; |
68 | 75 |
|
69 | 76 | import static org.hamcrest.Matchers.containsString; |
|
77 | 84 | import static org.springframework.security.config.annotation.SecurityContextChangedListenerArgumentMatchers.setAuthentication; |
78 | 85 | import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin; |
79 | 86 | import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.logout; |
| 87 | +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; |
80 | 88 | import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated; |
81 | 89 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; |
82 | 90 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; |
@@ -401,57 +409,58 @@ public void configureWhenRegisteringObjectPostProcessorThenInvokedOnExceptionTra |
401 | 409 |
|
402 | 410 | @Test |
403 | 411 | void requestWhenUnauthenticatedThenRequiresTwoSteps() throws Exception { |
404 | | - this.spring.register(MfaDslConfig.class).autowire(); |
| 412 | + this.spring.register(MfaDslConfig.class, UserConfig.class).autowire(); |
405 | 413 | UserDetails user = PasswordEncodedUser.user(); |
406 | | - this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.user(user))) |
| 414 | + this.mockMvc.perform(get("/profile").with(user(user))) |
407 | 415 | .andExpect(status().is3xxRedirection()) |
408 | 416 | .andExpect(redirectedUrl("http://localhost/login")); |
409 | 417 | this.mockMvc |
410 | | - .perform(post("/ott/generate").param("username", "user") |
411 | | - .with(SecurityMockMvcRequestPostProcessors.user(user)) |
| 418 | + .perform(post("/ott/generate").param("username", "rod") |
| 419 | + .with(user(user)) |
412 | 420 | .with(SecurityMockMvcRequestPostProcessors.csrf())) |
413 | 421 | .andExpect(status().is3xxRedirection()) |
414 | 422 | .andExpect(redirectedUrl("/ott/sent")); |
415 | 423 | this.mockMvc |
416 | | - .perform(post("/login").param("username", user.getUsername()) |
417 | | - .param("password", user.getPassword()) |
| 424 | + .perform(post("/login").param("username", "rod") |
| 425 | + .param("password", "password") |
418 | 426 | .with(SecurityMockMvcRequestPostProcessors.csrf())) |
419 | 427 | .andExpect(status().is3xxRedirection()) |
420 | 428 | .andExpect(redirectedUrl("/")); |
421 | 429 | user = PasswordEncodedUser.withUserDetails(user).authorities("profile:read", "FACTOR_OTT").build(); |
422 | | - this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.user(user))) |
| 430 | + this.mockMvc.perform(get("/profile").with(user(user))) |
423 | 431 | .andExpect(status().is3xxRedirection()) |
424 | 432 | .andExpect(redirectedUrl("http://localhost/login")); |
425 | 433 | user = PasswordEncodedUser.withUserDetails(user).authorities("profile:read", "FACTOR_PASSWORD").build(); |
426 | | - this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.user(user))) |
| 434 | + this.mockMvc.perform(get("/profile").with(user(user))) |
427 | 435 | .andExpect(status().isOk()) |
428 | 436 | .andExpect(content().string(containsString("/ott/generate"))); |
429 | 437 | user = PasswordEncodedUser.withUserDetails(user) |
430 | 438 | .authorities("profile:read", "FACTOR_PASSWORD", "FACTOR_OTT") |
431 | 439 | .build(); |
432 | | - this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.user(user))) |
433 | | - .andExpect(status().isNotFound()); |
| 440 | + this.mockMvc.perform(get("/profile").with(user(user))).andExpect(status().isNotFound()); |
434 | 441 | } |
435 | 442 |
|
436 | 443 | @Test |
437 | 444 | void requestWhenUnauthenticatedX509ThenRequiresTwoSteps() throws Exception { |
438 | | - this.spring.register(MfaDslX509Config.class).autowire(); |
439 | | - this.mockMvc.perform(get("/")).andExpect(status().isForbidden()); |
| 445 | + this.spring.register(MfaDslX509Config.class, UserConfig.class, BasicController.class).autowire(); |
| 446 | + this.mockMvc.perform(get("/profile")).andExpect(status().isForbidden()); |
| 447 | + this.mockMvc.perform(get("/profile").with(user(User.withUsername("rod").authorities("profile:read").build()))) |
| 448 | + .andExpect(status().isForbidden()); |
440 | 449 | this.mockMvc.perform(get("/login")).andExpect(status().isOk()); |
441 | | - this.mockMvc.perform(get("/").with(SecurityMockMvcRequestPostProcessors.x509("rod.cer"))) |
| 450 | + this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.x509("rod.cer"))) |
442 | 451 | .andExpect(status().is3xxRedirection()) |
443 | 452 | .andExpect(redirectedUrl("http://localhost/login")); |
444 | | - UserDetails user = PasswordEncodedUser.withUsername("rod") |
445 | | - .password("password") |
446 | | - .authorities("AUTHN_FORM") |
447 | | - .build(); |
448 | 453 | this.mockMvc |
449 | | - .perform(post("/login").param("username", user.getUsername()) |
450 | | - .param("password", user.getPassword()) |
| 454 | + .perform(post("/login").param("username", "rod") |
| 455 | + .param("password", "password") |
451 | 456 | .with(SecurityMockMvcRequestPostProcessors.x509("rod.cer")) |
452 | 457 | .with(SecurityMockMvcRequestPostProcessors.csrf())) |
453 | 458 | .andExpect(status().is3xxRedirection()) |
454 | 459 | .andExpect(redirectedUrl("/")); |
| 460 | + UserDetails authorized = PasswordEncodedUser.withUsername("rod") |
| 461 | + .authorities("profile:read", "FACTOR_X509", "FACTOR_PASSWORD") |
| 462 | + .build(); |
| 463 | + this.mockMvc.perform(get("/profile").with(user(authorized))).andExpect(status().isOk()); |
455 | 464 | } |
456 | 465 |
|
457 | 466 | @Configuration |
@@ -795,75 +804,102 @@ public <O> O postProcess(O object) { |
795 | 804 | static class MfaDslConfig { |
796 | 805 |
|
797 | 806 | @Bean |
798 | | - SecurityFilterChain filterChain(HttpSecurity http) throws Exception { |
| 807 | + SecurityFilterChain filterChain(HttpSecurity http, AuthorizationManagerFactory authz) throws Exception { |
799 | 808 | // @formatter:off |
800 | 809 | http |
801 | 810 | .formLogin(Customizer.withDefaults()) |
802 | 811 | .oneTimeTokenLogin(Customizer.withDefaults()) |
803 | 812 | .authorizeHttpRequests((authorize) -> authorize |
804 | | - .requestMatchers("/profile").access( |
805 | | - new HasAllAuthoritiesAuthorizationManager<>("profile:read", "FACTOR_PASSWORD", "FACTOR_OTT") |
806 | | - ) |
807 | | - .anyRequest().access(new HasAllAuthoritiesAuthorizationManager<>("FACTOR_PASSWORD", "FACTOR_OTT")) |
| 813 | + .requestMatchers("/profile").access(authz.hasAuthority("profile:read")) |
| 814 | + .anyRequest().access(authz.authenticated()) |
808 | 815 | ); |
809 | 816 | return http.build(); |
810 | 817 | // @formatter:on |
811 | 818 | } |
812 | 819 |
|
813 | 820 | @Bean |
814 | | - UserDetailsService users() { |
815 | | - return new InMemoryUserDetailsManager(PasswordEncodedUser.user()); |
| 821 | + OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler() { |
| 822 | + return new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent"); |
816 | 823 | } |
817 | 824 |
|
818 | 825 | @Bean |
819 | | - PasswordEncoder encoder() { |
820 | | - return NoOpPasswordEncoder.getInstance(); |
821 | | - } |
822 | | - |
823 | | - @Bean |
824 | | - OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler() { |
825 | | - return new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent"); |
| 826 | + AuthorizationManagerFactory authz() { |
| 827 | + return new AuthorizationManagerFactory("FACTOR_PASSWORD", "FACTOR_OTT"); |
826 | 828 | } |
827 | 829 |
|
828 | 830 | } |
829 | 831 |
|
830 | 832 | @Configuration |
831 | 833 | @EnableWebSecurity |
| 834 | + @EnableMethodSecurity |
832 | 835 | static class MfaDslX509Config { |
833 | 836 |
|
834 | 837 | @Bean |
835 | | - SecurityFilterChain filterChain(HttpSecurity http) throws Exception { |
| 838 | + SecurityFilterChain filterChain(HttpSecurity http, AuthorizationManagerFactory authz) throws Exception { |
836 | 839 | // @formatter:off |
837 | 840 | http |
838 | | - .formLogin(Customizer.withDefaults()) |
839 | 841 | .x509(Customizer.withDefaults()) |
| 842 | + .formLogin(Customizer.withDefaults()) |
840 | 843 | .authorizeHttpRequests((authorize) -> authorize |
841 | | - .anyRequest().access( |
842 | | - new HasAllAuthoritiesAuthorizationManager<>("FACTOR_X509", "FACTOR_PASSWORD") |
843 | | - ) |
| 844 | + .anyRequest().access(authz.authenticated()) |
844 | 845 | ); |
845 | 846 | return http.build(); |
846 | 847 | // @formatter:on |
847 | 848 | } |
848 | 849 |
|
849 | 850 | @Bean |
850 | | - UserDetailsService users() { |
851 | | - return new InMemoryUserDetailsManager( |
852 | | - PasswordEncodedUser.withUsername("rod").password("{noop}password").build()); |
| 851 | + AuthorizationManagerFactory authz() { |
| 852 | + return new AuthorizationManagerFactory("FACTOR_X509", "FACTOR_PASSWORD"); |
| 853 | + } |
| 854 | + |
| 855 | + } |
| 856 | + |
| 857 | + @Configuration |
| 858 | + static class UserConfig { |
| 859 | + |
| 860 | + @Bean |
| 861 | + UserDetails rod() { |
| 862 | + return PasswordEncodedUser.withUsername("rod").password("password").build(); |
| 863 | + } |
| 864 | + |
| 865 | + @Bean |
| 866 | + UserDetailsService users(UserDetails user) { |
| 867 | + return new InMemoryUserDetailsManager(user); |
853 | 868 | } |
854 | 869 |
|
855 | 870 | } |
856 | 871 |
|
857 | | - private static final class HasAllAuthoritiesAuthorizationManager<C> implements AuthorizationManager<C> { |
| 872 | + @RestController |
| 873 | + static class BasicController { |
| 874 | + |
| 875 | + @GetMapping("/profile") |
| 876 | + @PreAuthorize("@authz.hasAuthority('profile:read')") |
| 877 | + String profile() { |
| 878 | + return "profile"; |
| 879 | + } |
| 880 | + |
| 881 | + } |
| 882 | + |
| 883 | + public static class AuthorizationManagerFactory { |
858 | 884 |
|
859 | 885 | private final Collection<String> authorities; |
860 | 886 |
|
861 | | - private HasAllAuthoritiesAuthorizationManager(String... authorities) { |
| 887 | + AuthorizationManagerFactory(String... authorities) { |
862 | 888 | this.authorities = List.of(authorities); |
863 | 889 | } |
864 | 890 |
|
865 | | - @Override |
866 | | - public @Nullable AuthorizationResult authorize(Supplier<Authentication> authentication, C object) { |
| 891 | + public <T> AuthorizationManager<T> authenticated() { |
| 892 | + AuthenticatedAuthorizationManager<T> authenticated = AuthenticatedAuthorizationManager.authenticated(); |
| 893 | + return AuthorizationManagers.allOf(new AuthorizationDecision(false), this::factors, authenticated); |
| 894 | + } |
| 895 | + |
| 896 | + public <T> AuthorizationManager<T> hasAuthority(String authority) { |
| 897 | + AuthorityAuthorizationManager<T> authorized = AuthorityAuthorizationManager.hasAuthority(authority); |
| 898 | + return AuthorizationManagers.allOf(new AuthorizationDecision(false), this::factors, authorized); |
| 899 | + } |
| 900 | + |
| 901 | + private AuthorizationResult factors(Supplier<? extends @Nullable Authentication> authentication, |
| 902 | + Object context) { |
867 | 903 | List<String> authorities = authentication.get() |
868 | 904 | .getAuthorities() |
869 | 905 | .stream() |
|
0 commit comments