|
|
|
@ -16,6 +16,12 @@ |
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
|
|
|
@ -23,6 +29,10 @@ 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.authentication.UsernamePasswordAuthenticationToken; |
|
|
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; |
|
|
|
|
|
|
|
import org.springframework.security.authorization.AuthorityAuthorizationDecision; |
|
|
|
|
|
|
|
import org.springframework.security.authorization.AuthorizationManager; |
|
|
|
|
|
|
|
import org.springframework.security.authorization.AuthorizationResult; |
|
|
|
|
|
|
|
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.web.builders.HttpSecurity; |
|
|
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity; |
|
|
|
@ -31,22 +41,32 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur |
|
|
|
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.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.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.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; |
|
|
|
|
|
|
|
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler; |
|
|
|
|
|
|
|
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.servlet.config.annotation.EnableWebMvc; |
|
|
|
import org.springframework.web.servlet.config.annotation.EnableWebMvc; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import static org.hamcrest.Matchers.containsString; |
|
|
|
import static org.mockito.ArgumentMatchers.any; |
|
|
|
import static org.mockito.ArgumentMatchers.any; |
|
|
|
import static org.mockito.BDDMockito.given; |
|
|
|
import static org.mockito.BDDMockito.given; |
|
|
|
import static org.mockito.Mockito.atLeastOnce; |
|
|
|
import static org.mockito.Mockito.atLeastOnce; |
|
|
|
@ -60,6 +80,7 @@ import static org.springframework.security.test.web.servlet.request.SecurityMock |
|
|
|
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; |
|
|
|
|
|
|
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; |
|
|
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl; |
|
|
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl; |
|
|
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; |
|
|
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; |
|
|
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
|
|
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
|
|
|
@ -378,6 +399,61 @@ public class FormLoginConfigurerTests { |
|
|
|
verify(ObjectPostProcessorConfig.objectPostProcessor).postProcess(any(ExceptionTranslationFilter.class)); |
|
|
|
verify(ObjectPostProcessorConfig.objectPostProcessor).postProcess(any(ExceptionTranslationFilter.class)); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Test |
|
|
|
|
|
|
|
void requestWhenUnauthenticatedThenRequiresTwoSteps() throws Exception { |
|
|
|
|
|
|
|
this.spring.register(MfaDslConfig.class).autowire(); |
|
|
|
|
|
|
|
UserDetails user = PasswordEncodedUser.user(); |
|
|
|
|
|
|
|
this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.user(user))) |
|
|
|
|
|
|
|
.andExpect(status().is3xxRedirection()) |
|
|
|
|
|
|
|
.andExpect(redirectedUrl("http://localhost/login")); |
|
|
|
|
|
|
|
this.mockMvc |
|
|
|
|
|
|
|
.perform(post("/ott/generate").param("username", "user") |
|
|
|
|
|
|
|
.with(SecurityMockMvcRequestPostProcessors.user(user)) |
|
|
|
|
|
|
|
.with(SecurityMockMvcRequestPostProcessors.csrf())) |
|
|
|
|
|
|
|
.andExpect(status().is3xxRedirection()) |
|
|
|
|
|
|
|
.andExpect(redirectedUrl("/ott/sent")); |
|
|
|
|
|
|
|
this.mockMvc |
|
|
|
|
|
|
|
.perform(post("/login").param("username", user.getUsername()) |
|
|
|
|
|
|
|
.param("password", user.getPassword()) |
|
|
|
|
|
|
|
.with(SecurityMockMvcRequestPostProcessors.csrf())) |
|
|
|
|
|
|
|
.andExpect(status().is3xxRedirection()) |
|
|
|
|
|
|
|
.andExpect(redirectedUrl("/")); |
|
|
|
|
|
|
|
user = PasswordEncodedUser.withUserDetails(user).authorities("profile:read", "FACTOR_OTT").build(); |
|
|
|
|
|
|
|
this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.user(user))) |
|
|
|
|
|
|
|
.andExpect(status().is3xxRedirection()) |
|
|
|
|
|
|
|
.andExpect(redirectedUrl("http://localhost/login")); |
|
|
|
|
|
|
|
user = PasswordEncodedUser.withUserDetails(user).authorities("profile:read", "FACTOR_PASSWORD").build(); |
|
|
|
|
|
|
|
this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.user(user))) |
|
|
|
|
|
|
|
.andExpect(status().isOk()) |
|
|
|
|
|
|
|
.andExpect(content().string(containsString("/ott/generate"))); |
|
|
|
|
|
|
|
user = PasswordEncodedUser.withUserDetails(user) |
|
|
|
|
|
|
|
.authorities("profile:read", "FACTOR_PASSWORD", "FACTOR_OTT") |
|
|
|
|
|
|
|
.build(); |
|
|
|
|
|
|
|
this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.user(user))) |
|
|
|
|
|
|
|
.andExpect(status().isNotFound()); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Test |
|
|
|
|
|
|
|
void requestWhenUnauthenticatedX509ThenRequiresTwoSteps() throws Exception { |
|
|
|
|
|
|
|
this.spring.register(MfaDslX509Config.class).autowire(); |
|
|
|
|
|
|
|
this.mockMvc.perform(get("/")).andExpect(status().isForbidden()); |
|
|
|
|
|
|
|
this.mockMvc.perform(get("/login")).andExpect(status().isOk()); |
|
|
|
|
|
|
|
this.mockMvc.perform(get("/").with(SecurityMockMvcRequestPostProcessors.x509("rod.cer"))) |
|
|
|
|
|
|
|
.andExpect(status().is3xxRedirection()) |
|
|
|
|
|
|
|
.andExpect(redirectedUrl("http://localhost/login")); |
|
|
|
|
|
|
|
UserDetails user = PasswordEncodedUser.withUsername("rod") |
|
|
|
|
|
|
|
.password("password") |
|
|
|
|
|
|
|
.authorities("AUTHN_FORM") |
|
|
|
|
|
|
|
.build(); |
|
|
|
|
|
|
|
this.mockMvc |
|
|
|
|
|
|
|
.perform(post("/login").param("username", user.getUsername()) |
|
|
|
|
|
|
|
.param("password", user.getPassword()) |
|
|
|
|
|
|
|
.with(SecurityMockMvcRequestPostProcessors.x509("rod.cer")) |
|
|
|
|
|
|
|
.with(SecurityMockMvcRequestPostProcessors.csrf())) |
|
|
|
|
|
|
|
.andExpect(status().is3xxRedirection()) |
|
|
|
|
|
|
|
.andExpect(redirectedUrl("/")); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@Configuration |
|
|
|
@Configuration |
|
|
|
@EnableWebSecurity |
|
|
|
@EnableWebSecurity |
|
|
|
static class RequestCacheConfig { |
|
|
|
static class RequestCacheConfig { |
|
|
|
@ -714,4 +790,90 @@ public class FormLoginConfigurerTests { |
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Configuration |
|
|
|
|
|
|
|
@EnableWebSecurity |
|
|
|
|
|
|
|
static class MfaDslConfig { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Bean |
|
|
|
|
|
|
|
SecurityFilterChain filterChain(HttpSecurity http) throws Exception { |
|
|
|
|
|
|
|
// @formatter:off
|
|
|
|
|
|
|
|
http |
|
|
|
|
|
|
|
.formLogin(Customizer.withDefaults()) |
|
|
|
|
|
|
|
.oneTimeTokenLogin(Customizer.withDefaults()) |
|
|
|
|
|
|
|
.authorizeHttpRequests((authorize) -> authorize |
|
|
|
|
|
|
|
.requestMatchers("/profile").access( |
|
|
|
|
|
|
|
new HasAllAuthoritiesAuthorizationManager<>("profile:read", "FACTOR_PASSWORD", "FACTOR_OTT") |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
.anyRequest().access(new HasAllAuthoritiesAuthorizationManager<>("FACTOR_PASSWORD", "FACTOR_OTT")) |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
return http.build(); |
|
|
|
|
|
|
|
// @formatter:on
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Bean |
|
|
|
|
|
|
|
UserDetailsService users() { |
|
|
|
|
|
|
|
return new InMemoryUserDetailsManager(PasswordEncodedUser.user()); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Bean |
|
|
|
|
|
|
|
PasswordEncoder encoder() { |
|
|
|
|
|
|
|
return NoOpPasswordEncoder.getInstance(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Bean |
|
|
|
|
|
|
|
OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler() { |
|
|
|
|
|
|
|
return new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent"); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Configuration |
|
|
|
|
|
|
|
@EnableWebSecurity |
|
|
|
|
|
|
|
static class MfaDslX509Config { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Bean |
|
|
|
|
|
|
|
SecurityFilterChain filterChain(HttpSecurity http) throws Exception { |
|
|
|
|
|
|
|
// @formatter:off
|
|
|
|
|
|
|
|
http |
|
|
|
|
|
|
|
.formLogin(Customizer.withDefaults()) |
|
|
|
|
|
|
|
.x509(Customizer.withDefaults()) |
|
|
|
|
|
|
|
.authorizeHttpRequests((authorize) -> authorize |
|
|
|
|
|
|
|
.anyRequest().access( |
|
|
|
|
|
|
|
new HasAllAuthoritiesAuthorizationManager<>("FACTOR_X509", "FACTOR_PASSWORD") |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
return http.build(); |
|
|
|
|
|
|
|
// @formatter:on
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Bean |
|
|
|
|
|
|
|
UserDetailsService users() { |
|
|
|
|
|
|
|
return new InMemoryUserDetailsManager( |
|
|
|
|
|
|
|
PasswordEncodedUser.withUsername("rod").password("{noop}password").build()); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static final class HasAllAuthoritiesAuthorizationManager<C> implements AuthorizationManager<C> { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private final Collection<String> authorities; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private HasAllAuthoritiesAuthorizationManager(String... authorities) { |
|
|
|
|
|
|
|
this.authorities = List.of(authorities); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Override |
|
|
|
|
|
|
|
public @Nullable AuthorizationResult authorize(Supplier<Authentication> authentication, C object) { |
|
|
|
|
|
|
|
List<String> authorities = authentication.get() |
|
|
|
|
|
|
|
.getAuthorities() |
|
|
|
|
|
|
|
.stream() |
|
|
|
|
|
|
|
.map(GrantedAuthority::getAuthority) |
|
|
|
|
|
|
|
.toList(); |
|
|
|
|
|
|
|
List<String> needed = new ArrayList<>(this.authorities); |
|
|
|
|
|
|
|
needed.removeIf(authorities::contains); |
|
|
|
|
|
|
|
return new AuthorityAuthorizationDecision(needed.isEmpty(), AuthorityUtils.createAuthorityList(needed)); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|