Update Test for Method Security

Issue gh-17936
This commit is contained in:
Josh Cummings 2025-09-19 10:02:29 -06:00
parent e66c498d80
commit df7a7cdc99
No known key found for this signature in database
GPG Key ID: 869B37A20E876129
1 changed files with 85 additions and 70 deletions

View File

@ -16,47 +16,43 @@
package org.springframework.security.config.annotation.web.configurers; package org.springframework.security.config.annotation.web.configurers;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.Supplier;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authorization.AuthorityAuthorizationDecision; import org.springframework.security.authorization.AllAuthoritiesAuthorizationManager;
import org.springframework.security.authorization.AuthenticatedAuthorizationManager;
import org.springframework.security.authorization.AuthorityAuthorizationManager;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.AuthorizationResult; import org.springframework.security.authorization.AuthorizationManagers;
import org.springframework.security.config.Customizer; import org.springframework.security.config.Customizer;
import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.ObjectPostProcessor;
import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig; import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.config.users.AuthenticationTestConfiguration; import org.springframework.security.config.users.AuthenticationTestConfiguration;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextChangedListener; import org.springframework.security.core.context.SecurityContextChangedListener;
import org.springframework.security.core.context.SecurityContextHolderStrategy; import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.core.userdetails.PasswordEncodedUser; 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.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders; import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders;
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors; import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors;
import org.springframework.security.web.PortMapper; import org.springframework.security.web.PortMapper;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.ExceptionTranslationFilter; import org.springframework.security.web.access.ExceptionTranslationFilter;
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@ -64,6 +60,8 @@ import org.springframework.security.web.authentication.ott.OneTimeTokenGeneratio
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler; import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler;
import org.springframework.security.web.savedrequest.RequestCache; import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
@ -77,6 +75,7 @@ import static org.springframework.security.config.Customizer.withDefaults;
import static org.springframework.security.config.annotation.SecurityContextChangedListenerArgumentMatchers.setAuthentication; import static org.springframework.security.config.annotation.SecurityContextChangedListenerArgumentMatchers.setAuthentication;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.logout; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.logout;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated; import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@ -401,57 +400,58 @@ public class FormLoginConfigurerTests {
@Test @Test
void requestWhenUnauthenticatedThenRequiresTwoSteps() throws Exception { void requestWhenUnauthenticatedThenRequiresTwoSteps() throws Exception {
this.spring.register(MfaDslConfig.class).autowire(); this.spring.register(MfaDslConfig.class, UserConfig.class).autowire();
UserDetails user = PasswordEncodedUser.user(); UserDetails user = PasswordEncodedUser.user();
this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.user(user))) this.mockMvc.perform(get("/profile").with(user(user)))
.andExpect(status().is3xxRedirection()) .andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("http://localhost/login")); .andExpect(redirectedUrl("http://localhost/login"));
this.mockMvc this.mockMvc
.perform(post("/ott/generate").param("username", "user") .perform(post("/ott/generate").param("username", "rod")
.with(SecurityMockMvcRequestPostProcessors.user(user)) .with(user(user))
.with(SecurityMockMvcRequestPostProcessors.csrf())) .with(SecurityMockMvcRequestPostProcessors.csrf()))
.andExpect(status().is3xxRedirection()) .andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/ott/sent")); .andExpect(redirectedUrl("/ott/sent"));
this.mockMvc this.mockMvc
.perform(post("/login").param("username", user.getUsername()) .perform(post("/login").param("username", "rod")
.param("password", user.getPassword()) .param("password", "password")
.with(SecurityMockMvcRequestPostProcessors.csrf())) .with(SecurityMockMvcRequestPostProcessors.csrf()))
.andExpect(status().is3xxRedirection()) .andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/")); .andExpect(redirectedUrl("/"));
user = PasswordEncodedUser.withUserDetails(user).authorities("profile:read", "FACTOR_OTT").build(); user = PasswordEncodedUser.withUserDetails(user).authorities("profile:read", "FACTOR_OTT").build();
this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.user(user))) this.mockMvc.perform(get("/profile").with(user(user)))
.andExpect(status().is3xxRedirection()) .andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("http://localhost/login")); .andExpect(redirectedUrl("http://localhost/login"));
user = PasswordEncodedUser.withUserDetails(user).authorities("profile:read", "FACTOR_PASSWORD").build(); user = PasswordEncodedUser.withUserDetails(user).authorities("profile:read", "FACTOR_PASSWORD").build();
this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.user(user))) this.mockMvc.perform(get("/profile").with(user(user)))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(content().string(containsString("/ott/generate"))); .andExpect(content().string(containsString("/ott/generate")));
user = PasswordEncodedUser.withUserDetails(user) user = PasswordEncodedUser.withUserDetails(user)
.authorities("profile:read", "FACTOR_PASSWORD", "FACTOR_OTT") .authorities("profile:read", "FACTOR_PASSWORD", "FACTOR_OTT")
.build(); .build();
this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.user(user))) this.mockMvc.perform(get("/profile").with(user(user))).andExpect(status().isNotFound());
.andExpect(status().isNotFound());
} }
@Test @Test
void requestWhenUnauthenticatedX509ThenRequiresTwoSteps() throws Exception { void requestWhenUnauthenticatedX509ThenRequiresTwoSteps() throws Exception {
this.spring.register(MfaDslX509Config.class).autowire(); this.spring.register(MfaDslX509Config.class, UserConfig.class, org.springframework.security.config.annotation.web.configurers.FormLoginConfigurerTests.BasicMfaController.class).autowire();
this.mockMvc.perform(get("/")).andExpect(status().isForbidden()); this.mockMvc.perform(get("/profile")).andExpect(status().isForbidden());
this.mockMvc.perform(get("/profile").with(user(User.withUsername("rod").authorities("profile:read").build())))
.andExpect(status().isForbidden());
this.mockMvc.perform(get("/login")).andExpect(status().isOk()); this.mockMvc.perform(get("/login")).andExpect(status().isOk());
this.mockMvc.perform(get("/").with(SecurityMockMvcRequestPostProcessors.x509("rod.cer"))) this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.x509("rod.cer")))
.andExpect(status().is3xxRedirection()) .andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("http://localhost/login")); .andExpect(redirectedUrl("http://localhost/login"));
UserDetails user = PasswordEncodedUser.withUsername("rod")
.password("password")
.authorities("AUTHN_FORM")
.build();
this.mockMvc this.mockMvc
.perform(post("/login").param("username", user.getUsername()) .perform(post("/login").param("username", "rod")
.param("password", user.getPassword()) .param("password", "password")
.with(SecurityMockMvcRequestPostProcessors.x509("rod.cer")) .with(SecurityMockMvcRequestPostProcessors.x509("rod.cer"))
.with(SecurityMockMvcRequestPostProcessors.csrf())) .with(SecurityMockMvcRequestPostProcessors.csrf()))
.andExpect(status().is3xxRedirection()) .andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/")); .andExpect(redirectedUrl("/"));
UserDetails authorized = PasswordEncodedUser.withUsername("rod")
.authorities("profile:read", "FACTOR_X509", "FACTOR_PASSWORD")
.build();
this.mockMvc.perform(get("/profile").with(user(authorized))).andExpect(status().isOk());
} }
@Configuration @Configuration
@ -795,83 +795,98 @@ public class FormLoginConfigurerTests {
static class MfaDslConfig { static class MfaDslConfig {
@Bean @Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception { SecurityFilterChain filterChain(HttpSecurity http, AuthorizationManagerFactory<RequestAuthorizationContext> authz) throws Exception {
// @formatter:off // @formatter:off
http http
.formLogin(Customizer.withDefaults()) .formLogin(Customizer.withDefaults())
.oneTimeTokenLogin(Customizer.withDefaults()) .oneTimeTokenLogin(Customizer.withDefaults())
.authorizeHttpRequests((authorize) -> authorize .authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/profile").access( .requestMatchers("/profile").access(authz.hasAuthority("profile:read"))
new HasAllAuthoritiesAuthorizationManager<>("profile:read", "FACTOR_PASSWORD", "FACTOR_OTT") .anyRequest().access(authz.authenticated())
)
.anyRequest().access(new HasAllAuthoritiesAuthorizationManager<>("FACTOR_PASSWORD", "FACTOR_OTT"))
); );
return http.build(); return http.build();
// @formatter:on // @formatter:on
} }
@Bean
UserDetailsService users() {
return new InMemoryUserDetailsManager(PasswordEncodedUser.user());
}
@Bean
PasswordEncoder encoder() {
return NoOpPasswordEncoder.getInstance();
}
@Bean @Bean
OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler() { OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler() {
return new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent"); return new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent");
} }
@Bean
AuthorizationManagerFactory<?> authz() {
return new AuthorizationManagerFactory<>("FACTOR_PASSWORD", "FACTOR_OTT");
}
} }
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@EnableMethodSecurity
static class MfaDslX509Config { static class MfaDslX509Config {
@Bean @Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception { SecurityFilterChain filterChain(HttpSecurity http, AuthorizationManagerFactory<RequestAuthorizationContext> authz) throws Exception {
// @formatter:off // @formatter:off
http http
.formLogin(Customizer.withDefaults())
.x509(Customizer.withDefaults()) .x509(Customizer.withDefaults())
.formLogin(Customizer.withDefaults())
.authorizeHttpRequests((authorize) -> authorize .authorizeHttpRequests((authorize) -> authorize
.anyRequest().access( .anyRequest().access(authz.authenticated())
new HasAllAuthoritiesAuthorizationManager<>("FACTOR_X509", "FACTOR_PASSWORD")
)
); );
return http.build(); return http.build();
// @formatter:on // @formatter:on
} }
@Bean @Bean
UserDetailsService users() { AuthorizationManagerFactory<?> authz() {
return new InMemoryUserDetailsManager( return new AuthorizationManagerFactory<>("FACTOR_X509", "FACTOR_PASSWORD");
PasswordEncodedUser.withUsername("rod").password("{noop}password").build());
} }
} }
private static final class HasAllAuthoritiesAuthorizationManager<C> implements AuthorizationManager<C> { @Configuration
static class UserConfig {
private final Collection<String> authorities; @Bean
UserDetails rod() {
private HasAllAuthoritiesAuthorizationManager(String... authorities) { return PasswordEncodedUser.withUsername("rod").password("password").build();
this.authorities = List.of(authorities);
} }
@Override @Bean
public @Nullable AuthorizationResult authorize(Supplier<Authentication> authentication, C object) { UserDetailsService users(UserDetails user) {
List<String> authorities = authentication.get() return new InMemoryUserDetailsManager(user);
.getAuthorities() }
.stream()
.map(GrantedAuthority::getAuthority) }
.toList();
List<String> needed = new ArrayList<>(this.authorities); @RestController
needed.removeIf(authorities::contains); static class BasicMfaController {
return new AuthorityAuthorizationDecision(needed.isEmpty(), AuthorityUtils.createAuthorityList(needed));
@GetMapping("/profile")
@PreAuthorize("@authz.hasAuthority('profile:read')")
String profile() {
return "profile";
}
}
public static class AuthorizationManagerFactory<T> {
private final AuthorizationManager<T> authorities;
AuthorizationManagerFactory(String... authorities) {
this.authorities = AllAuthoritiesAuthorizationManager.hasAllAuthorities(authorities);
}
public AuthorizationManager<T> authenticated() {
AuthenticatedAuthorizationManager<T> authenticated = AuthenticatedAuthorizationManager.authenticated();
return AuthorizationManagers.allOf(new AuthorizationDecision(false), this.authorities, authenticated);
}
public AuthorizationManager<T> hasAuthority(String authority) {
AuthorityAuthorizationManager<T> authorized = AuthorityAuthorizationManager.hasAuthority(authority);
return AuthorizationManagers.allOf(new AuthorizationDecision(false), this.authorities, authorized);
} }
} }