parent
e66c498d80
commit
df7a7cdc99
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue