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