mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-10-24 11:18:45 +00:00
Merge branch 'mfa'
Closes gh-2603
This commit is contained in:
commit
28aad8855c
@ -68,6 +68,7 @@ public final class DefaultLoginPageConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
|
||||
@Override
|
||||
public void init(H http) {
|
||||
this.loginPageGeneratingFilter.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
|
||||
this.loginPageGeneratingFilter.setResolveHiddenInputs(DefaultLoginPageConfigurer.this::hiddenInputs);
|
||||
this.logoutPageGeneratingFilter.setResolveHiddenInputs(DefaultLoginPageConfigurer.this::hiddenInputs);
|
||||
http.setSharedObject(DefaultLoginPageGeneratingFilter.class, this.loginPageGeneratingFilter);
|
||||
|
||||
@ -17,15 +17,18 @@
|
||||
package org.springframework.security.config.annotation.web.configurers;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
|
||||
import org.springframework.security.web.access.DelegatingMissingAuthorityAccessDeniedHandler;
|
||||
import org.springframework.security.web.access.ExceptionTranslationFilter;
|
||||
import org.springframework.security.web.access.RequestMatcherDelegatingAccessDeniedHandler;
|
||||
import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint;
|
||||
@ -77,6 +80,8 @@ public final class ExceptionHandlingConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
|
||||
private LinkedHashMap<RequestMatcher, AccessDeniedHandler> defaultDeniedHandlerMappings = new LinkedHashMap<>();
|
||||
|
||||
private DelegatingMissingAuthorityAccessDeniedHandler.@Nullable Builder missingAuthoritiesHandlerBuilder;
|
||||
|
||||
/**
|
||||
* Creates a new instance
|
||||
* @see HttpSecurity#exceptionHandling(Customizer)
|
||||
@ -127,6 +132,43 @@ public final class ExceptionHandlingConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a default {@link AuthenticationEntryPoint} to be used which prefers being
|
||||
* invoked for the provided missing {@link GrantedAuthority}.
|
||||
* @param entryPoint the {@link AuthenticationEntryPoint} to use for the given
|
||||
* {@code authority}
|
||||
* @param authority the authority
|
||||
* @return the {@link ExceptionHandlingConfigurer} for further customizations
|
||||
* @since 7.0
|
||||
*/
|
||||
public ExceptionHandlingConfigurer<H> defaultDeniedHandlerForMissingAuthority(AuthenticationEntryPoint entryPoint,
|
||||
String authority) {
|
||||
if (this.missingAuthoritiesHandlerBuilder == null) {
|
||||
this.missingAuthoritiesHandlerBuilder = DelegatingMissingAuthorityAccessDeniedHandler.builder();
|
||||
}
|
||||
this.missingAuthoritiesHandlerBuilder.addEntryPointFor(entryPoint, authority);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a default {@link AuthenticationEntryPoint} to be used which prefers being
|
||||
* invoked for the provided missing {@link GrantedAuthority}.
|
||||
* @param entryPoint a consumer of a
|
||||
* {@link DelegatingAuthenticationEntryPoint.Builder} to use for the given
|
||||
* {@code authority}
|
||||
* @param authority the authority
|
||||
* @return the {@link ExceptionHandlingConfigurer} for further customizations
|
||||
* @since 7.0
|
||||
*/
|
||||
public ExceptionHandlingConfigurer<H> defaultDeniedHandlerForMissingAuthority(
|
||||
Consumer<DelegatingAuthenticationEntryPoint.Builder> entryPoint, String authority) {
|
||||
if (this.missingAuthoritiesHandlerBuilder == null) {
|
||||
this.missingAuthoritiesHandlerBuilder = DelegatingMissingAuthorityAccessDeniedHandler.builder();
|
||||
}
|
||||
this.missingAuthoritiesHandlerBuilder.addEntryPointFor(entryPoint, authority);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AuthenticationEntryPoint} to be used.
|
||||
*
|
||||
@ -229,6 +271,17 @@ public final class ExceptionHandlingConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
}
|
||||
|
||||
private AccessDeniedHandler createDefaultDeniedHandler(H http) {
|
||||
AccessDeniedHandler defaults = createDefaultAccessDeniedHandler(http);
|
||||
if (this.missingAuthoritiesHandlerBuilder == null) {
|
||||
return defaults;
|
||||
}
|
||||
DelegatingMissingAuthorityAccessDeniedHandler deniedHandler = this.missingAuthoritiesHandlerBuilder.build();
|
||||
deniedHandler.setRequestCache(getRequestCache(http));
|
||||
deniedHandler.setDefaultAccessDeniedHandler(defaults);
|
||||
return deniedHandler;
|
||||
}
|
||||
|
||||
private AccessDeniedHandler createDefaultAccessDeniedHandler(H http) {
|
||||
if (this.defaultDeniedHandlerMappings.isEmpty()) {
|
||||
return new AccessDeniedHandlerImpl();
|
||||
}
|
||||
|
||||
@ -231,6 +231,13 @@ public final class FormLoginConfigurer<H extends HttpSecurityBuilder<H>> extends
|
||||
public void init(H http) throws Exception {
|
||||
super.init(http);
|
||||
initDefaultLoginFilter(http);
|
||||
ExceptionHandlingConfigurer<H> exceptions = http.getConfigurer(ExceptionHandlingConfigurer.class);
|
||||
if (exceptions != null) {
|
||||
AuthenticationEntryPoint entryPoint = getAuthenticationEntryPoint();
|
||||
RequestMatcher requestMatcher = getAuthenticationEntryPointMatcher(http);
|
||||
exceptions.defaultDeniedHandlerForMissingAuthority((ep) -> ep.addEntryPointFor(entryPoint, requestMatcher),
|
||||
"FACTOR_PASSWORD");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -192,8 +192,10 @@ public final class HttpBasicConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
if (exceptionHandling == null) {
|
||||
return;
|
||||
}
|
||||
exceptionHandling.defaultAuthenticationEntryPointFor(postProcess(this.authenticationEntryPoint),
|
||||
preferredMatcher);
|
||||
AuthenticationEntryPoint entryPoint = postProcess(this.authenticationEntryPoint);
|
||||
exceptionHandling.defaultAuthenticationEntryPointFor(entryPoint, preferredMatcher);
|
||||
exceptionHandling.defaultDeniedHandlerForMissingAuthority(
|
||||
(ep) -> ep.addEntryPointFor(entryPoint, preferredMatcher), "FACTOR_PASSWORD");
|
||||
}
|
||||
|
||||
private void registerDefaultLogoutSuccessHandler(B http, RequestMatcher preferredMatcher) {
|
||||
|
||||
@ -27,11 +27,14 @@ import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.security.authentication.ProviderManager;
|
||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.access.intercept.AuthorizationFilter;
|
||||
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
|
||||
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
|
||||
import org.springframework.security.web.authentication.ui.DefaultResourcesFilter;
|
||||
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRpEntity;
|
||||
import org.springframework.security.web.webauthn.authentication.PublicKeyCredentialRequestOptionsFilter;
|
||||
import org.springframework.security.web.webauthn.authentication.WebAuthnAuthenticationFilter;
|
||||
@ -150,6 +153,16 @@ public class WebAuthnConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(H http) throws Exception {
|
||||
ExceptionHandlingConfigurer<H> exceptions = http.getConfigurer(ExceptionHandlingConfigurer.class);
|
||||
if (exceptions != null) {
|
||||
AuthenticationEntryPoint entryPoint = new LoginUrlAuthenticationEntryPoint("/login");
|
||||
exceptions.defaultDeniedHandlerForMissingAuthority(
|
||||
(ep) -> ep.addEntryPointFor(entryPoint, AnyRequestMatcher.INSTANCE), "FACTOR_WEBAUTHN");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(H http) throws Exception {
|
||||
UserDetailsService userDetailsService = getSharedOrBean(http, UserDetailsService.class)
|
||||
|
||||
@ -184,7 +184,9 @@ public final class X509Configurer<H extends HttpSecurityBuilder<H>>
|
||||
.setSharedObject(AuthenticationEntryPoint.class, new Http403ForbiddenEntryPoint());
|
||||
ExceptionHandlingConfigurer<H> exceptions = http.getConfigurer(ExceptionHandlingConfigurer.class);
|
||||
if (exceptions != null) {
|
||||
exceptions.defaultAuthenticationEntryPointFor(new Http403ForbiddenEntryPoint(), AnyRequestMatcher.INSTANCE);
|
||||
AuthenticationEntryPoint forbidden = new Http403ForbiddenEntryPoint();
|
||||
exceptions.defaultDeniedHandlerForMissingAuthority(
|
||||
(ep) -> ep.addEntryPointFor(forbidden, AnyRequestMatcher.INSTANCE), "FACTOR_X509");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -40,6 +40,7 @@ import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer;
|
||||
import org.springframework.security.context.DelegatingApplicationListener;
|
||||
import org.springframework.security.core.Authentication;
|
||||
@ -556,11 +557,18 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
RequestMatcher loginUrlMatcher = new AndRequestMatcher(notXRequestedWith,
|
||||
new NegatedRequestMatcher(defaultLoginPageMatcher), formLoginNotEnabled);
|
||||
// @formatter:off
|
||||
return DelegatingAuthenticationEntryPoint.builder()
|
||||
AuthenticationEntryPoint loginEntryPoint = DelegatingAuthenticationEntryPoint.builder()
|
||||
.addEntryPointFor(loginUrlEntryPoint, loginUrlMatcher)
|
||||
.defaultEntryPoint(getAuthenticationEntryPoint())
|
||||
.build();
|
||||
// @formatter:on
|
||||
ExceptionHandlingConfigurer<B> exceptions = http.getConfigurer(ExceptionHandlingConfigurer.class);
|
||||
if (exceptions != null) {
|
||||
RequestMatcher requestMatcher = getAuthenticationEntryPointMatcher(http);
|
||||
exceptions.defaultDeniedHandlerForMissingAuthority(
|
||||
(ep) -> ep.addEntryPointFor(loginEntryPoint, requestMatcher), "FACTOR_AUTHORIZATION_CODE");
|
||||
}
|
||||
return loginEntryPoint;
|
||||
}
|
||||
|
||||
private RequestMatcher getFormLoginNotEnabledRequestMatcher(B http) {
|
||||
|
||||
@ -327,6 +327,8 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
|
||||
RequestMatcher preferredMatcher = new OrRequestMatcher(
|
||||
Arrays.asList(this.requestMatcher, X_REQUESTED_WITH, restNotHtmlMatcher, allMatcher));
|
||||
exceptionHandling.defaultAuthenticationEntryPointFor(this.authenticationEntryPoint, preferredMatcher);
|
||||
exceptionHandling.defaultDeniedHandlerForMissingAuthority(
|
||||
(ep) -> ep.addEntryPointFor(this.authenticationEntryPoint, preferredMatcher), "FACTOR_BEARER");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -35,8 +35,10 @@ 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.configurers.AbstractAuthenticationFilterConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||
@ -134,6 +136,13 @@ public final class OneTimeTokenLoginConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
AuthenticationProvider authenticationProvider = getAuthenticationProvider();
|
||||
http.authenticationProvider(postProcess(authenticationProvider));
|
||||
intiDefaultLoginFilter(http);
|
||||
ExceptionHandlingConfigurer<H> exceptions = http.getConfigurer(ExceptionHandlingConfigurer.class);
|
||||
if (exceptions != null) {
|
||||
AuthenticationEntryPoint entryPoint = getAuthenticationEntryPoint();
|
||||
RequestMatcher requestMatcher = getAuthenticationEntryPointMatcher(http);
|
||||
exceptions.defaultDeniedHandlerForMissingAuthority((ep) -> ep.addEntryPointFor(entryPoint, requestMatcher),
|
||||
"FACTOR_OTT");
|
||||
}
|
||||
}
|
||||
|
||||
private void intiDefaultLoginFilter(H http) {
|
||||
|
||||
@ -33,6 +33,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest;
|
||||
import org.springframework.security.saml2.provider.service.authentication.OpenSaml5AuthenticationProvider;
|
||||
@ -343,11 +344,18 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
RequestMatcher loginUrlMatcher = new AndRequestMatcher(notXRequestedWith,
|
||||
new NegatedRequestMatcher(defaultLoginPageMatcher));
|
||||
// @formatter:off
|
||||
return DelegatingAuthenticationEntryPoint.builder()
|
||||
AuthenticationEntryPoint loginEntryPoint = DelegatingAuthenticationEntryPoint.builder()
|
||||
.addEntryPointFor(loginUrlEntryPoint, loginUrlMatcher)
|
||||
.defaultEntryPoint(getAuthenticationEntryPoint())
|
||||
.build();
|
||||
// @formatter:on
|
||||
ExceptionHandlingConfigurer<B> exceptions = http.getConfigurer(ExceptionHandlingConfigurer.class);
|
||||
if (exceptions != null) {
|
||||
RequestMatcher requestMatcher = getAuthenticationEntryPointMatcher(http);
|
||||
exceptions.defaultDeniedHandlerForMissingAuthority(
|
||||
(ep) -> ep.addEntryPointFor(loginEntryPoint, requestMatcher), "FACTOR_SAML_RESPONSE");
|
||||
}
|
||||
return loginEntryPoint;
|
||||
}
|
||||
|
||||
private void setAuthenticationRequestRepository(B http,
|
||||
|
||||
@ -22,9 +22,18 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
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.AuthorizationManagers;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.ObjectPostProcessor;
|
||||
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.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
|
||||
@ -34,17 +43,25 @@ import org.springframework.security.config.users.AuthenticationTestConfiguration
|
||||
import org.springframework.security.core.context.SecurityContextChangedListener;
|
||||
import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||
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.UserDetailsService;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
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.SecurityFilterChain;
|
||||
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.LoginUrlAuthenticationEntryPoint;
|
||||
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.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 static org.mockito.ArgumentMatchers.any;
|
||||
@ -57,6 +74,7 @@ import static org.springframework.security.config.Customizer.withDefaults;
|
||||
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.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.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
@ -378,6 +396,62 @@ public class FormLoginConfigurerTests {
|
||||
verify(ObjectPostProcessorConfig.objectPostProcessor).postProcess(any(ExceptionTranslationFilter.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void requestWhenUnauthenticatedThenRequiresTwoSteps() throws Exception {
|
||||
this.spring.register(MfaDslConfig.class, UserConfig.class).autowire();
|
||||
UserDetails user = PasswordEncodedUser.user();
|
||||
this.mockMvc.perform(get("/profile").with(user(user)))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("http://localhost/login?factor=password"));
|
||||
this.mockMvc
|
||||
.perform(post("/ott/generate").param("username", "rod")
|
||||
.with(user(user))
|
||||
.with(SecurityMockMvcRequestPostProcessors.csrf()))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("/ott/sent"));
|
||||
this.mockMvc
|
||||
.perform(post("/login").param("username", "rod")
|
||||
.param("password", "password")
|
||||
.with(SecurityMockMvcRequestPostProcessors.csrf()))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("/"));
|
||||
user = PasswordEncodedUser.withUserDetails(user).authorities("profile:read", "FACTOR_OTT").build();
|
||||
this.mockMvc.perform(get("/profile").with(user(user)))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("http://localhost/login?factor=password"));
|
||||
user = PasswordEncodedUser.withUserDetails(user).authorities("profile:read", "FACTOR_PASSWORD").build();
|
||||
this.mockMvc.perform(get("/profile").with(user(user)))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("http://localhost/login?factor=ott"));
|
||||
user = PasswordEncodedUser.withUserDetails(user)
|
||||
.authorities("profile:read", "FACTOR_PASSWORD", "FACTOR_OTT")
|
||||
.build();
|
||||
this.mockMvc.perform(get("/profile").with(user(user))).andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
void requestWhenUnauthenticatedX509ThenRequiresTwoSteps() throws Exception {
|
||||
this.spring.register(MfaDslX509Config.class, UserConfig.class, BasicMfaController.class).autowire();
|
||||
this.mockMvc.perform(get("/profile")).andExpect(status().is3xxRedirection());
|
||||
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("/profile").with(SecurityMockMvcRequestPostProcessors.x509("rod.cer")))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("http://localhost/login?factor=password"));
|
||||
this.mockMvc
|
||||
.perform(post("/login").param("username", "rod")
|
||||
.param("password", "password")
|
||||
.with(SecurityMockMvcRequestPostProcessors.x509("rod.cer"))
|
||||
.with(SecurityMockMvcRequestPostProcessors.csrf()))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.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
|
||||
@EnableWebSecurity
|
||||
static class RequestCacheConfig {
|
||||
@ -714,4 +788,107 @@ public class FormLoginConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class MfaDslConfig {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http,
|
||||
AuthorizationManagerFactory<RequestAuthorizationContext> authz) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.formLogin(Customizer.withDefaults())
|
||||
.oneTimeTokenLogin(Customizer.withDefaults())
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.requestMatchers("/profile").access(authz.hasAuthority("profile:read"))
|
||||
.anyRequest().access(authz.authenticated())
|
||||
);
|
||||
return http.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Bean
|
||||
OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler() {
|
||||
return new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent");
|
||||
}
|
||||
|
||||
@Bean
|
||||
AuthorizationManagerFactory<?> authz() {
|
||||
return new AuthorizationManagerFactory<>("FACTOR_PASSWORD", "FACTOR_OTT");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableMethodSecurity
|
||||
static class MfaDslX509Config {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http,
|
||||
AuthorizationManagerFactory<RequestAuthorizationContext> authz) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.x509(Customizer.withDefaults())
|
||||
.formLogin(Customizer.withDefaults())
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.anyRequest().access(authz.authenticated())
|
||||
);
|
||||
return http.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Bean
|
||||
AuthorizationManagerFactory<?> authz() {
|
||||
return new AuthorizationManagerFactory<>("FACTOR_X509", "FACTOR_PASSWORD");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class UserConfig {
|
||||
|
||||
@Bean
|
||||
UserDetails rod() {
|
||||
return PasswordEncodedUser.withUsername("rod").password("password").build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
UserDetailsService users(UserDetails user) {
|
||||
return new InMemoryUserDetailsManager(user);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class BasicMfaController {
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -49,6 +49,7 @@
|
||||
***** xref:servlet/authentication/passwords/password-encoder.adoc[PasswordEncoder]
|
||||
***** xref:servlet/authentication/passwords/dao-authentication-provider.adoc[DaoAuthenticationProvider]
|
||||
***** xref:servlet/authentication/passwords/ldap.adoc[LDAP]
|
||||
*** xref:servlet/authentication/adaptive.adoc[Multifactor Authentication]
|
||||
*** xref:servlet/authentication/persistence.adoc[Persistence]
|
||||
*** xref:servlet/authentication/passkeys.adoc[Passkeys]
|
||||
*** xref:servlet/authentication/onetimetoken.adoc[One-Time Token]
|
||||
|
||||
101
docs/modules/ROOT/pages/servlet/authentication/adaptive.adoc
Normal file
101
docs/modules/ROOT/pages/servlet/authentication/adaptive.adoc
Normal file
@ -0,0 +1,101 @@
|
||||
= Adaptive Authentication
|
||||
|
||||
Since authentication needs can vary from person-to-person and even from one login attempt to the next, Spring Security supports adapting authentication requirements to each situation.
|
||||
|
||||
Some of the most common applications of this principal are:
|
||||
|
||||
1. *Re-authentication* - Users need to provide authentication again in order to enter an area of elevated security
|
||||
2. *Multi-factor Authentication* - Users need more than one authentication mechanism to pass in order to access secured resources
|
||||
3. *Authorizing More Scopes* - Users are allowed to consent to a subset of scopes from an OAuth 2.0 Authorization Server.
|
||||
Then, if later on a scope that they did not grant is needed, consent can be re-requested for just that scope.
|
||||
4. *Opting-in to Stronger Authentication Mechanisms* - Users may not be ready yet to start using MFA, but the application wants to allow the subset of security-minded users to opt-in.
|
||||
5. *Requiring Additional Steps for Suspicious Logins* - The application may notice that the user's IP address has changed, that they are behind a VPN, or some other consideration that requires additional verification
|
||||
|
||||
[[re-authentication]]
|
||||
== Re-authentication
|
||||
|
||||
The most common of these is re-authentication.
|
||||
Imagine an application configured in the following way:
|
||||
|
||||
include-code::./SimpleConfiguration[tag=httpSecurity,indent=0]
|
||||
|
||||
By default, this application has two authentication mechanisms that it allows, meaning that the user could use either one and be fully-authenticated.
|
||||
|
||||
If there is a set of endpoints that require a specific factor, we can specify that in `authorizeHttpRequests` as follows:
|
||||
|
||||
include-code::./RequireOttConfiguration[tag=httpSecurity,indent=0]
|
||||
<1> - States that all `/profile/**` endpoints require one-time-token login to be authorized
|
||||
|
||||
Given the above configuration, users can log in with any mechanism that you support.
|
||||
And, if they want to visit the profile page, then Spring Security will redirect them to the One-Time-Token Login page to obtain it.
|
||||
|
||||
In this way, the authority given to a user is directly proportional to the amount of proof given.
|
||||
This adaptive approach allows users to give only the proof needed to perform their intended operations.
|
||||
|
||||
[[multi-factor-authentication]]
|
||||
== Multi-Factor Authentication
|
||||
|
||||
You may require that all users require both One-Time-Token login and Username/Password login to access any part of your site.
|
||||
|
||||
To require both, you can state an authorization rule with `anyRequest` like so:
|
||||
|
||||
include-code::./ListAuthoritiesConfiguration[tag=httpSecurity,indent=0]
|
||||
<1> - This states that both `FACTOR_PASSWORD` and `FACTOR_OTT` are needed to use any part of the application
|
||||
|
||||
Spring Security behind the scenes knows which endpoint to go to depending on which authority is missing.
|
||||
If the user logged in initially with their username and password, then Spring Security redirects to the One-Time-Token Login page.
|
||||
If the user logged in initially with a token, then Spring Security redirects to the Username/Password Login page.
|
||||
|
||||
[[authorization-manager-factory]]
|
||||
=== Requiring MFA For All Endpoints
|
||||
|
||||
Specifying all authorities for each request pattern could be unwanted boilerplate:
|
||||
|
||||
include-code::./ListAuthoritiesEverywhereConfiguration[tag=httpSecurity,indent=0]
|
||||
<1> - Since all authorities need to be specified for each endpoint, deploying MFA in this way can create unwanted boilerplate
|
||||
|
||||
This can be remedied by publishing an `AuthorizationManagerFactory` bean like so:
|
||||
|
||||
include-code::./UseAuthorizationManagerFactoryConfiguration[tag=authorizationManagerFactoryBean,indent=0]
|
||||
|
||||
This yields a more familiar configuration:
|
||||
|
||||
include-code::./UseAuthorizationManagerFactoryConfiguration[tag=httpSecurity,indent=0]
|
||||
|
||||
[[obtaining-more-authorization]]
|
||||
== Authorizing More Scopes
|
||||
|
||||
You can also configure exception handling to direct Spring Security on how to obtain a missing scope.
|
||||
|
||||
Consider an application that requires a specific OAuth 2.0 scope for a given endpoint:
|
||||
|
||||
include-code::./ScopeConfiguration[tag=httpSecurity,indent=0]
|
||||
|
||||
If this is also configured with an `AuthorizationManagerFactory` bean like this one:
|
||||
|
||||
include-code::./MissingAuthorityConfiguration[tag=authorizationManagerFactoryBean,indent=0]
|
||||
|
||||
Then the application will require an X.509 certificate as well as authorization from an OAuth 2.0 authorization server.
|
||||
|
||||
In the event that the user does not consent to `profile:read`, this application as it stands will issue a 403.
|
||||
However, if you have a way for the application to re-ask for consent, then you can implement this in an `AuthenticationEntryPoint` like the following:
|
||||
|
||||
include-code::./MissingAuthorityConfiguration[tag=authenticationEntryPoint,indent=0]
|
||||
|
||||
Then, your filter chain declaration can bind this entry point to the given authority like so:
|
||||
|
||||
include-code::./MissingAuthorityConfiguration[tag=httpSecurity,indent=0]
|
||||
|
||||
[[custom-authorization-manager-factory]]
|
||||
== Programmatically Decide Which Authorities Are Required
|
||||
|
||||
`AuthorizationManager` is the core interface for making authorization decisions.
|
||||
Consider an authorization manager that looks at the logged in user to decide which factors are necessary:
|
||||
|
||||
include-code::./CustomAuthorizationManagerFactory[tag=authorizationManager,indent=0]
|
||||
|
||||
In this case, using One-Time-Token is only required for those who have opted in.
|
||||
|
||||
This can then be enforced by a custom `AuthorizationManagerFactory` implementation:
|
||||
|
||||
include-code::./CustomAuthorizationManagerFactory[tag=authorizationManagerFactory,indent=0]
|
||||
@ -15,6 +15,7 @@ Each section that follows will indicate the more notable removals as well as the
|
||||
|
||||
== Core
|
||||
|
||||
* Added Support for xref:servlet/authentication/adaptive.adoc[Multi-factor Authentication]
|
||||
* Removed `AuthorizationManager#check` in favor of `AuthorizationManager#authorize`
|
||||
* Added javadoc:org.springframework.security.authorization.AllAuthoritiesAuthorizationManager[] and javadoc:org.springframework.security.authorization.AllAuthoritiesReactiveAuthorizationManager[] along with corresponding methods for xref:servlet/authorization/authorize-http-requests.adoc#authorize-requests[Authorizing `HttpServletRequests`] and xref:servlet/authorization/method-security.adoc#using-authorization-expression-fields-and-methods[method security expressions].
|
||||
* Added xref:servlet/authorization/architecture.adoc#authz-authorization-manager-factory[`AuthorizationManagerFactory`] for creating `AuthorizationManager` instances in xref:servlet/authorization/authorize-http-requests.adoc#customizing-authorization-managers[request-based] and xref:servlet/authorization/method-security.adoc#customizing-authorization-managers[method-based] authorization components
|
||||
|
||||
@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.docs.servlet.authentication.authorizationmanagerfactory;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.docs.servlet.authentication.servletx509config.CustomX509Configuration;
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener;
|
||||
import org.springframework.test.context.TestExecutionListeners;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
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.result.MockMvcResultMatchers.redirectedUrl;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* Tests {@link CustomX509Configuration}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@ExtendWith({ SpringExtension.class, SpringTestContextExtension.class })
|
||||
@TestExecutionListeners(WithSecurityContextTestExecutionListener.class)
|
||||
public class AuthorizationManagerFactoryTests {
|
||||
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
|
||||
@Autowired
|
||||
MockMvc mockMvc;
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = { "FACTOR_PASSWORD", "FACTOR_OTT" })
|
||||
void getWhenAuthenticatedWithPasswordAndOttThenPermits() throws Exception {
|
||||
this.spring.register(UseAuthorizationManagerFactoryConfiguration.class, Http200Controller.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(authenticated().withUsername("user"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "FACTOR_PASSWORD")
|
||||
void getWhenAuthenticatedWithPasswordThenRedirectsToOtt() throws Exception {
|
||||
this.spring.register(UseAuthorizationManagerFactoryConfiguration.class, Http200Controller.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/"))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("http://localhost/login?factor=ott"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "FACTOR_OTT")
|
||||
void getWhenAuthenticatedWithOttThenRedirectsToPassword() throws Exception {
|
||||
this.spring.register(UseAuthorizationManagerFactoryConfiguration.class, Http200Controller.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/"))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("http://localhost/login?factor=password"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void getWhenAuthenticatedThenRedirectsToPassword() throws Exception {
|
||||
this.spring.register(UseAuthorizationManagerFactoryConfiguration.class, Http200Controller.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/"))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("http://localhost/login?factor=password"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
void getWhenUnauthenticatedThenRedirectsToBoth() throws Exception {
|
||||
this.spring.register(UseAuthorizationManagerFactoryConfiguration.class, Http200Controller.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/"))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("http://localhost/login"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class Http200Controller {
|
||||
@GetMapping("/**")
|
||||
String ok() {
|
||||
return "ok";
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
package org.springframework.security.docs.servlet.authentication.authorizationmanagerfactory;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler;
|
||||
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler;
|
||||
|
||||
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasAuthority;
|
||||
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole;
|
||||
import static org.springframework.security.authorization.AuthorizationManagers.allOf;
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class ListAuthoritiesEverywhereConfiguration {
|
||||
|
||||
// tag::httpSecurity[]
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.requestMatchers("/admin/**").access(allOf(hasAuthority("FACTOR_PASSWORD"), hasAuthority("FACTOR_OTT"), hasRole("ADMIN"))) // <1>
|
||||
.anyRequest().access(allOf(hasAuthority("FACTOR_PASSWORD"), hasAuthority("FACTOR_OTT")))
|
||||
)
|
||||
.formLogin(Customizer.withDefaults())
|
||||
.oneTimeTokenLogin(Customizer.withDefaults());
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
// end::httpSecurity[]
|
||||
|
||||
@Bean
|
||||
UserDetailsService userDetailsService() {
|
||||
return new InMemoryUserDetailsManager(
|
||||
User.withDefaultPasswordEncoder()
|
||||
.username("user")
|
||||
.password("password")
|
||||
.authorities("app")
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
@Bean
|
||||
OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler() {
|
||||
return new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
package org.springframework.security.docs.servlet.authentication.authorizationmanagerfactory;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.authorization.AuthorizationManagerFactory;
|
||||
import org.springframework.security.authorization.DefaultAuthorizationManagerFactory;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler;
|
||||
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler;
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class UseAuthorizationManagerFactoryConfiguration {
|
||||
|
||||
// tag::httpSecurity[]
|
||||
@Bean
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.requestMatchers("/admin/**").hasRole("ADMIN")
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.formLogin(Customizer.withDefaults())
|
||||
.oneTimeTokenLogin(Customizer.withDefaults());
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
// end::httpSecurity[]
|
||||
|
||||
// tag::authorizationManagerFactoryBean[]
|
||||
@Bean
|
||||
AuthorizationManagerFactory<Object> authz() {
|
||||
return DefaultAuthorizationManagerFactory.builder()
|
||||
.requireAdditionalAuthorities("FACTOR_PASSWORD", "FACTOR_OTT").build();
|
||||
}
|
||||
// end::authorizationManagerFactoryBean[]
|
||||
|
||||
@Bean
|
||||
UserDetailsService userDetailsService() {
|
||||
return new InMemoryUserDetailsManager(
|
||||
User.withDefaultPasswordEncoder()
|
||||
.username("user")
|
||||
.password("password")
|
||||
.authorities("app")
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
@Bean
|
||||
OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler() {
|
||||
return new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,103 @@
|
||||
package org.springframework.security.docs.servlet.authentication.customauthorizationmanagerfactory;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.access.expression.SecurityExpressionOperations;
|
||||
import org.springframework.security.access.expression.SecurityExpressionRoot;
|
||||
import org.springframework.security.authorization.AuthorityAuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationManager;
|
||||
import org.springframework.security.authorization.AuthorizationManagerFactory;
|
||||
import org.springframework.security.authorization.AuthorizationResult;
|
||||
import org.springframework.security.authorization.DefaultAuthorizationManagerFactory;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler;
|
||||
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class CustomAuthorizationManagerFactory {
|
||||
// tag::httpSecurity[]
|
||||
@Bean
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.requestMatchers("/admin/**").hasRole("ADMIN")
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.formLogin(Customizer.withDefaults())
|
||||
.oneTimeTokenLogin(Customizer.withDefaults());
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
// end::httpSecurity[]
|
||||
|
||||
// tag::authorizationManager[]
|
||||
@Component
|
||||
class OptInToMfaAuthorizationManager implements AuthorizationManager<Object> {
|
||||
@Override
|
||||
public AuthorizationResult authorize(Supplier<? extends @Nullable Authentication> authentication, Object context) {
|
||||
MyPrincipal principal = (MyPrincipal) authentication.get().getPrincipal();
|
||||
if (principal.optedIn()) {
|
||||
SecurityExpressionOperations sec = new SecurityExpressionRoot<>(authentication, context) {};
|
||||
return new AuthorityAuthorizationDecision(sec.hasAuthority("FACTOR_OTT"),
|
||||
AuthorityUtils.createAuthorityList("FACTOR_OTT"));
|
||||
}
|
||||
return new AuthorizationDecision(true);
|
||||
}
|
||||
}
|
||||
// end::authorizationManager[]
|
||||
|
||||
// tag::authorizationManagerFactory[]
|
||||
@Bean
|
||||
AuthorizationManagerFactory<Object> authorizationManagerFactory(OptInToMfaAuthorizationManager optIn) {
|
||||
DefaultAuthorizationManagerFactory<Object> defaults = new DefaultAuthorizationManagerFactory<>();
|
||||
defaults.setAdditionalAuthorization(optIn);
|
||||
return defaults;
|
||||
}
|
||||
// end::authorizationManagerFactory[]
|
||||
|
||||
@NullMarked
|
||||
record MyPrincipal(String username, boolean optedIn) implements UserDetails {
|
||||
@Override
|
||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||
return AuthorityUtils.createAuthorityList("app");
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getPassword() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsername() {
|
||||
return this.username;
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
UserDetailsService users() {
|
||||
return (username) -> new MyPrincipal(username, username.equals("optedin"));
|
||||
}
|
||||
|
||||
@Bean
|
||||
OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler() {
|
||||
return new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.docs.servlet.authentication.customauthorizationmanagerfactory;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.docs.servlet.authentication.servletx509config.CustomX509Configuration;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
|
||||
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.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* Tests {@link CustomX509Configuration}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@ExtendWith(SpringTestContextExtension.class)
|
||||
public class CustomAuthorizationManagerFactoryTests {
|
||||
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
|
||||
@Autowired
|
||||
MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
UserDetailsService users;
|
||||
|
||||
@Test
|
||||
void getWhenOptedInThenRedirectsToOtt() throws Exception {
|
||||
this.spring.register(CustomAuthorizationManagerFactory.class, Http200Controller.class).autowire();
|
||||
UserDetails user = this.users.loadUserByUsername("optedin");
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/").with(user(user)))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("http://localhost/login?factor=ott"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
void getWhenNotOptedInThenAllows() throws Exception {
|
||||
this.spring.register(CustomAuthorizationManagerFactory.class, Http200Controller.class).autowire();
|
||||
UserDetails user = this.users.loadUserByUsername("user");
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/").with(user(user)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(authenticated().withUsername("user"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
void getWhenOptedAndHasFactorThenAllows() throws Exception {
|
||||
this.spring.register(CustomAuthorizationManagerFactory.class, Http200Controller.class).autowire();
|
||||
UserDetails user = this.users.loadUserByUsername("optedin");
|
||||
TestingAuthenticationToken token = new TestingAuthenticationToken(user, "", "FACTOR_OTT");
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/").with(authentication(token)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(authenticated().withUsername("optedin"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class Http200Controller {
|
||||
@GetMapping("/**")
|
||||
String ok() {
|
||||
return "ok";
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
package org.springframework.security.docs.servlet.authentication.multifactorauthentication;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler;
|
||||
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler;
|
||||
|
||||
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasAuthority;
|
||||
import static org.springframework.security.authorization.AuthorizationManagers.allOf;
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class ListAuthoritiesConfiguration {
|
||||
|
||||
// tag::httpSecurity[]
|
||||
@Bean
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.anyRequest().access(allOf(hasAuthority("FACTOR_PASSWORD"), hasAuthority("FACTOR_OTT"))) // <1>
|
||||
)
|
||||
.formLogin(Customizer.withDefaults())
|
||||
.oneTimeTokenLogin(Customizer.withDefaults());
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
// end::httpSecurity[]
|
||||
|
||||
@Bean
|
||||
UserDetailsService userDetailsService() {
|
||||
return new InMemoryUserDetailsManager(
|
||||
User.withDefaultPasswordEncoder()
|
||||
.username("user")
|
||||
.password("password")
|
||||
.authorities("app")
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
@Bean
|
||||
OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler() {
|
||||
return new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.docs.servlet.authentication.multifactorauthentication;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.docs.servlet.authentication.servletx509config.CustomX509Configuration;
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener;
|
||||
import org.springframework.test.context.TestExecutionListeners;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
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.result.MockMvcResultMatchers.redirectedUrl;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* Tests {@link CustomX509Configuration}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@ExtendWith({ SpringExtension.class, SpringTestContextExtension.class })
|
||||
@TestExecutionListeners(WithSecurityContextTestExecutionListener.class)
|
||||
public class MultiFactorAuthenticationTests {
|
||||
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
|
||||
@Autowired
|
||||
MockMvc mockMvc;
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = { "FACTOR_PASSWORD", "FACTOR_OTT" })
|
||||
void getWhenAuthenticatedWithPasswordAndOttThenPermits() throws Exception {
|
||||
this.spring.register(ListAuthoritiesConfiguration.class, Http200Controller.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(authenticated().withUsername("user"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "FACTOR_PASSWORD")
|
||||
void getWhenAuthenticatedWithPasswordThenRedirectsToOtt() throws Exception {
|
||||
this.spring.register(ListAuthoritiesConfiguration.class, Http200Controller.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/"))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("http://localhost/login?factor=ott"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "FACTOR_OTT")
|
||||
void getWhenAuthenticatedWithOttThenRedirectsToPassword() throws Exception {
|
||||
this.spring.register(ListAuthoritiesConfiguration.class, Http200Controller.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/"))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("http://localhost/login?factor=password"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void getWhenAuthenticatedThenRedirectsToPassword() throws Exception {
|
||||
this.spring.register(ListAuthoritiesConfiguration.class, Http200Controller.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/"))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("http://localhost/login?factor=password"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
void getWhenUnauthenticatedThenRedirectsToBoth() throws Exception {
|
||||
this.spring.register(ListAuthoritiesConfiguration.class, Http200Controller.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/"))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("http://localhost/login"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class Http200Controller {
|
||||
@GetMapping("/**")
|
||||
String ok() {
|
||||
return "ok";
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,147 @@
|
||||
package org.springframework.security.docs.servlet.authentication.obtainingmoreauthorization;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationManager;
|
||||
import org.springframework.security.authorization.AuthorizationManagerFactory;
|
||||
import org.springframework.security.authorization.DefaultAuthorizationManagerFactory;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import static org.springframework.security.authorization.AllAuthoritiesAuthorizationManager.hasAllAuthorities;
|
||||
import static org.springframework.security.authorization.AuthorizationManagers.allOf;
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class MissingAuthorityConfiguration {
|
||||
|
||||
// tag::httpSecurity[]
|
||||
@Bean
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http, ScopeRetrievingAuthenticationEntryPoint oauth2) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.requestMatchers("/profile/**").hasAuthority("SCOPE_profile:read")
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.x509(Customizer.withDefaults())
|
||||
.oauth2Login(Customizer.withDefaults())
|
||||
.exceptionHandling((exceptions) -> exceptions
|
||||
.defaultDeniedHandlerForMissingAuthority(oauth2, "SCOPE_profile:read")
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
// end::httpSecurity[]
|
||||
|
||||
// tag::authorizationManagerFactoryBean[]
|
||||
@Bean
|
||||
AuthorizationManagerFactory<RequestAuthorizationContext> authz() {
|
||||
return new FactorAuthorizationManagerFactory(hasAllAuthorities("FACTOR_X509", "FACTOR_AUTHORIZATION_CODE"));
|
||||
}
|
||||
// end::authorizationManagerFactoryBean[]
|
||||
|
||||
// tag::authorizationManagerFactory[]
|
||||
class FactorAuthorizationManagerFactory implements AuthorizationManagerFactory<RequestAuthorizationContext> {
|
||||
private final AuthorizationManager<RequestAuthorizationContext> hasAuthorities;
|
||||
private final DefaultAuthorizationManagerFactory<RequestAuthorizationContext> delegate =
|
||||
new DefaultAuthorizationManagerFactory<>();
|
||||
|
||||
FactorAuthorizationManagerFactory(AuthorizationManager<RequestAuthorizationContext> hasAuthorities) {
|
||||
this.hasAuthorities = hasAuthorities;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthorizationManager<RequestAuthorizationContext> permitAll() {
|
||||
return this.delegate.permitAll();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthorizationManager<RequestAuthorizationContext> denyAll() {
|
||||
return this.delegate.denyAll();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthorizationManager<RequestAuthorizationContext> hasRole(String role) {
|
||||
return hasAnyRole(role);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthorizationManager<RequestAuthorizationContext> hasAnyRole(String... roles) {
|
||||
return allOf(new AuthorizationDecision(false), this.hasAuthorities, this.delegate.hasAnyRole(roles));
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthorizationManager<RequestAuthorizationContext> hasAllRoles(String... roles) {
|
||||
return allOf(new AuthorizationDecision(false), this.hasAuthorities, this.delegate.hasAllRoles(roles));
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthorizationManager<RequestAuthorizationContext> hasAuthority(String authority) {
|
||||
return hasAnyAuthority(authority);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthorizationManager<RequestAuthorizationContext> hasAnyAuthority(String... authorities) {
|
||||
return allOf(new AuthorizationDecision(false), this.hasAuthorities, this.delegate.hasAnyAuthority(authorities));
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthorizationManager<RequestAuthorizationContext> hasAllAuthorities(String... authorities) {
|
||||
return allOf(new AuthorizationDecision(false), this.hasAuthorities, this.delegate.hasAllAuthorities(authorities));
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthorizationManager<RequestAuthorizationContext> authenticated() {
|
||||
return allOf(new AuthorizationDecision(false), this.hasAuthorities, this.delegate.authenticated());
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthorizationManager<RequestAuthorizationContext> fullyAuthenticated() {
|
||||
return allOf(new AuthorizationDecision(false), this.hasAuthorities, this.delegate.fullyAuthenticated());
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthorizationManager<RequestAuthorizationContext> rememberMe() {
|
||||
return allOf(new AuthorizationDecision(false), this.hasAuthorities, this.delegate.rememberMe());
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthorizationManager<RequestAuthorizationContext> anonymous() {
|
||||
return this.delegate.anonymous();
|
||||
}
|
||||
}
|
||||
// end::authorizationManagerFactory[]
|
||||
|
||||
// tag::authenticationEntryPoint[]
|
||||
@Component
|
||||
class ScopeRetrievingAuthenticationEntryPoint implements AuthenticationEntryPoint {
|
||||
@Override
|
||||
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
|
||||
throws IOException, ServletException {
|
||||
response.sendRedirect("https://authz.example.org/authorize?scope=profile:read");
|
||||
}
|
||||
}
|
||||
// end::authenticationEntryPoint[]
|
||||
|
||||
@Bean
|
||||
ClientRegistrationRepository clients() {
|
||||
return new InMemoryClientRegistrationRepository(TestClientRegistrations.clientRegistration().build());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.docs.servlet.authentication.obtainingmoreauthorization;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.docs.servlet.authentication.servletx509config.CustomX509Configuration;
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener;
|
||||
import org.springframework.test.context.TestExecutionListeners;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
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.result.MockMvcResultMatchers.redirectedUrl;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* Tests {@link CustomX509Configuration}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@ExtendWith({ SpringExtension.class, SpringTestContextExtension.class })
|
||||
@TestExecutionListeners(WithSecurityContextTestExecutionListener.class)
|
||||
public class ObtainingMoreAuthorizationTests {
|
||||
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
|
||||
@Autowired
|
||||
MockMvc mockMvc;
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void profileWhenScopeConfigurationThenDenies() throws Exception {
|
||||
this.spring.register(ScopeConfiguration.class, Http200Controller.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/profile"))
|
||||
.andExpect(status().isForbidden());
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = { "FACTOR_X509", "FACTOR_AUTHORIZATION_CODE" })
|
||||
void profileWhenMissingAuthorityConfigurationThenRedirectsToAuthorizationServer() throws Exception {
|
||||
this.spring.register(MissingAuthorityConfiguration.class, Http200Controller.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/profile"))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("https://authz.example.org/authorize?scope=profile:read"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = { "SCOPE_profile:read" })
|
||||
void profileWhenMissingX509WithOttThenForbidden() throws Exception {
|
||||
this.spring.register(MissingAuthorityConfiguration.class, Http200Controller.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/profile"))
|
||||
.andExpect(status().isForbidden());
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = { "FACTOR_X509", "FACTOR_AUTHORIZATION_CODE", "SCOPE_profile:read" })
|
||||
void profileWhenAuthenticatedAndHasScopeThenPermits() throws Exception {
|
||||
this.spring.register(MissingAuthorityConfiguration.class, Http200Controller.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/profile"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(authenticated().withUsername("user"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class Http200Controller {
|
||||
@GetMapping("/**")
|
||||
String ok() {
|
||||
return "ok";
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
package org.springframework.security.docs.servlet.authentication.obtainingmoreauthorization;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class ScopeConfiguration {
|
||||
|
||||
// tag::httpSecurity[]
|
||||
@Bean
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.requestMatchers("/profile/**").hasAuthority("SCOPE_profile:read")
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.x509(Customizer.withDefaults())
|
||||
.oauth2Login(Customizer.withDefaults());
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
// end::httpSecurity[]
|
||||
|
||||
@Bean
|
||||
ClientRegistrationRepository clients() {
|
||||
return new InMemoryClientRegistrationRepository(TestClientRegistrations.clientRegistration().build());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.docs.servlet.authentication.reauthentication;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.docs.servlet.authentication.servletx509config.CustomX509Configuration;
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener;
|
||||
import org.springframework.test.context.TestExecutionListeners;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
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.result.MockMvcResultMatchers.redirectedUrl;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* Tests {@link CustomX509Configuration}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@ExtendWith({ SpringExtension.class, SpringTestContextExtension.class })
|
||||
@TestExecutionListeners(WithSecurityContextTestExecutionListener.class)
|
||||
public class ReauthenticationTests {
|
||||
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
|
||||
@Autowired
|
||||
MockMvc mockMvc;
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void formLoginWhenSimpleConfigurationThenPermits() throws Exception {
|
||||
this.spring.register(SimpleConfiguration.class, Http200Controller.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(authenticated().withUsername("user"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void formLoginWhenRequireOttConfigurationThenRedirectsToOtt() throws Exception {
|
||||
this.spring.register(RequireOttConfiguration.class, Http200Controller.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/profile"))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("http://localhost/login?factor=ott"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "FACTOR_OTT")
|
||||
void ottWhenRequireOttConfigurationThenAllows() throws Exception {
|
||||
this.spring.register(RequireOttConfiguration.class, Http200Controller.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/profile"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(authenticated().withUsername("user"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class Http200Controller {
|
||||
@GetMapping("/**")
|
||||
String ok() {
|
||||
return "ok";
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
package org.springframework.security.docs.servlet.authentication.reauthentication;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler;
|
||||
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler;
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class RequireOttConfiguration {
|
||||
|
||||
// tag::httpSecurity[]
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.requestMatchers("/profile/**").hasAuthority("FACTOR_OTT") // <1>
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.formLogin(Customizer.withDefaults())
|
||||
.oneTimeTokenLogin(Customizer.withDefaults());
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
// end::httpSecurity[]
|
||||
|
||||
@Bean
|
||||
UserDetailsService userDetailsService() {
|
||||
return new InMemoryUserDetailsManager(
|
||||
User.withDefaultPasswordEncoder()
|
||||
.username("user")
|
||||
.password("password")
|
||||
.authorities("app")
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
@Bean
|
||||
OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler() {
|
||||
return new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
package org.springframework.security.docs.servlet.authentication.reauthentication;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler;
|
||||
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler;
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class SimpleConfiguration {
|
||||
// tag::httpSecurity[]
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
|
||||
.formLogin(Customizer.withDefaults())
|
||||
.oneTimeTokenLogin(Customizer.withDefaults());
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
// end::httpSecurity[]
|
||||
|
||||
@Bean
|
||||
UserDetailsService userDetailsService() {
|
||||
return new InMemoryUserDetailsManager(
|
||||
User.withDefaultPasswordEncoder()
|
||||
.username("user")
|
||||
.password("password")
|
||||
.authorities("app")
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
@Bean
|
||||
OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler() {
|
||||
return new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.kt.docs.servlet.authentication.authorizationmanagerfactory
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.security.config.test.SpringTestContext
|
||||
import org.springframework.security.config.test.SpringTestContextExtension
|
||||
import org.springframework.security.test.context.support.WithMockUser
|
||||
import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener
|
||||
import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers
|
||||
import org.springframework.test.context.TestExecutionListeners
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||
import org.springframework.test.web.servlet.MockMvc
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
/**
|
||||
* Tests [CustomX509Configuration].
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@ExtendWith(SpringExtension::class, SpringTestContextExtension::class)
|
||||
@TestExecutionListeners(WithSecurityContextTestExecutionListener::class)
|
||||
class AuthorizationManagerFactoryTests {
|
||||
@JvmField
|
||||
val spring: SpringTestContext = SpringTestContext(this)
|
||||
|
||||
@Autowired
|
||||
var mockMvc: MockMvc? = null
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = ["FACTOR_PASSWORD", "FACTOR_OTT"])
|
||||
@Throws(Exception::class)
|
||||
fun getWhenAuthenticatedWithPasswordAndOttThenPermits() {
|
||||
this.spring.register(UseAuthorizationManagerFactoryConfiguration::class.java, Http200Controller::class.java)
|
||||
.autowire()
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
|
||||
.andExpect(MockMvcResultMatchers.status().isOk())
|
||||
.andExpect(SecurityMockMvcResultMatchers.authenticated().withUsername("user"))
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = ["FACTOR_PASSWORD"])
|
||||
@Throws(Exception::class)
|
||||
fun getWhenAuthenticatedWithPasswordThenRedirectsToOtt() {
|
||||
this.spring.register(UseAuthorizationManagerFactoryConfiguration::class.java, Http200Controller::class.java)
|
||||
.autowire()
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
|
||||
.andExpect(MockMvcResultMatchers.status().is3xxRedirection())
|
||||
.andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor=ott"))
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = ["FACTOR_OTT"])
|
||||
@Throws(Exception::class)
|
||||
fun getWhenAuthenticatedWithOttThenRedirectsToPassword() {
|
||||
this.spring.register(UseAuthorizationManagerFactoryConfiguration::class.java, Http200Controller::class.java)
|
||||
.autowire()
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
|
||||
.andExpect(MockMvcResultMatchers.status().is3xxRedirection())
|
||||
.andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor=password"))
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
@Throws(Exception::class)
|
||||
fun getWhenAuthenticatedThenRedirectsToPassword() {
|
||||
this.spring.register(UseAuthorizationManagerFactoryConfiguration::class.java, Http200Controller::class.java)
|
||||
.autowire()
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
|
||||
.andExpect(MockMvcResultMatchers.status().is3xxRedirection())
|
||||
.andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor=password"))
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun getWhenUnauthenticatedThenRedirectsToBoth() {
|
||||
this.spring.register(UseAuthorizationManagerFactoryConfiguration::class.java, Http200Controller::class.java)
|
||||
.autowire()
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
|
||||
.andExpect(MockMvcResultMatchers.status().is3xxRedirection())
|
||||
.andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login"))
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@RestController
|
||||
internal class Http200Controller {
|
||||
@GetMapping("/**")
|
||||
fun ok(): String {
|
||||
return "ok"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
package org.springframework.security.kt.docs.servlet.authentication.authorizationmanagerfactory
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
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.invoke
|
||||
import org.springframework.security.core.userdetails.User
|
||||
import org.springframework.security.core.userdetails.UserDetailsService
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager
|
||||
import org.springframework.security.web.SecurityFilterChain
|
||||
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler
|
||||
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class ListAuthoritiesEverywhereConfiguration {
|
||||
|
||||
// tag::httpSecurity[]
|
||||
@Bean
|
||||
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? {
|
||||
// @formatter:off
|
||||
http {
|
||||
authorizeHttpRequests {
|
||||
authorize("/admin/**", hasAllAuthorities("FACTOR_PASSWORD", "FACTOR_OTT", "ROLE_ADMIN")) // <1>
|
||||
authorize(anyRequest, hasAllAuthorities("FACTOR_PASSWORD", "FACTOR_OTT"))
|
||||
}
|
||||
formLogin { }
|
||||
oneTimeTokenLogin { }
|
||||
}
|
||||
// @formatter:on
|
||||
return http.build()
|
||||
}
|
||||
// end::httpSecurity[]
|
||||
|
||||
|
||||
// end::httpSecurity[]
|
||||
@Bean
|
||||
fun userDetailsService(): UserDetailsService {
|
||||
return InMemoryUserDetailsManager(
|
||||
User.withDefaultPasswordEncoder()
|
||||
.username("user")
|
||||
.password("password")
|
||||
.authorities("app")
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun tokenGenerationSuccessHandler(): OneTimeTokenGenerationSuccessHandler {
|
||||
return RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent")
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
package org.springframework.security.kt.docs.servlet.authentication.authorizationmanagerfactory
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.security.authorization.AuthorizationManagerFactory
|
||||
import org.springframework.security.authorization.DefaultAuthorizationManagerFactory
|
||||
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.invoke
|
||||
import org.springframework.security.core.userdetails.User
|
||||
import org.springframework.security.core.userdetails.UserDetailsService
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager
|
||||
import org.springframework.security.web.SecurityFilterChain
|
||||
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler
|
||||
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
internal class UseAuthorizationManagerFactoryConfiguration {
|
||||
// tag::httpSecurity[]
|
||||
@Bean
|
||||
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? {
|
||||
// @formatter:off
|
||||
http {
|
||||
authorizeHttpRequests {
|
||||
authorize("/admin/**", hasRole("ADMIN"))
|
||||
authorize(anyRequest, authenticated)
|
||||
}
|
||||
formLogin { }
|
||||
oneTimeTokenLogin { }
|
||||
}
|
||||
// @formatter:on
|
||||
return http.build()
|
||||
}
|
||||
// end::httpSecurity[]
|
||||
|
||||
// tag::authorizationManagerFactoryBean[]
|
||||
@Bean
|
||||
fun authz(): AuthorizationManagerFactory<Object> {
|
||||
return DefaultAuthorizationManagerFactory.builder<Object>()
|
||||
.requireAdditionalAuthorities("FACTOR_PASSWORD", "FACTOR_OTT").build()
|
||||
}
|
||||
// end::authorizationManagerFactoryBean[]
|
||||
|
||||
@Bean
|
||||
fun userDetailsService(): UserDetailsService {
|
||||
return InMemoryUserDetailsManager(
|
||||
User.withDefaultPasswordEncoder()
|
||||
.username("user")
|
||||
.password("password")
|
||||
.authorities("app")
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun tokenGenerationSuccessHandler(): OneTimeTokenGenerationSuccessHandler {
|
||||
return RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent")
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,95 @@
|
||||
package org.springframework.security.kt.docs.servlet.authentication.customauthorizationmanagerfactory
|
||||
|
||||
import org.jspecify.annotations.NullMarked
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.security.access.expression.SecurityExpressionRoot
|
||||
import org.springframework.security.authorization.*
|
||||
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.invoke
|
||||
import org.springframework.security.core.Authentication
|
||||
import org.springframework.security.core.GrantedAuthority
|
||||
import org.springframework.security.core.authority.AuthorityUtils
|
||||
import org.springframework.security.core.userdetails.UserDetails
|
||||
import org.springframework.security.core.userdetails.UserDetailsService
|
||||
import org.springframework.security.web.SecurityFilterChain
|
||||
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler
|
||||
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler
|
||||
import org.springframework.stereotype.Component
|
||||
import java.util.function.Supplier
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
internal class CustomAuthorizationManagerFactory {
|
||||
|
||||
// tag::httpSecurity[]
|
||||
@Bean
|
||||
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? {
|
||||
// @formatter:off
|
||||
http {
|
||||
authorizeHttpRequests {
|
||||
authorize("/admin/**", hasRole("ADMIN"))
|
||||
authorize(anyRequest, authenticated)
|
||||
}
|
||||
formLogin { }
|
||||
oneTimeTokenLogin { }
|
||||
}
|
||||
// @formatter:on
|
||||
return http.build()
|
||||
}
|
||||
// end::httpSecurity[]
|
||||
|
||||
// tag::authorizationManager[]
|
||||
@Component
|
||||
internal open class OptInToMfaAuthorizationManager : AuthorizationManager<Object> {
|
||||
override fun authorize(
|
||||
authentication: Supplier<out Authentication?>, context: Object): AuthorizationResult {
|
||||
val principal = authentication.get().getPrincipal() as MyPrincipal?
|
||||
if (principal!!.optedIn) {
|
||||
val root = object : SecurityExpressionRoot<Object>(authentication, context) { }
|
||||
return AuthorityAuthorizationDecision(
|
||||
root.hasAuthority("FACTOR_OTT"),
|
||||
AuthorityUtils.createAuthorityList("FACTOR_OTT")
|
||||
)
|
||||
}
|
||||
return AuthorizationDecision(true)
|
||||
}
|
||||
}
|
||||
// end::authorizationManager[]
|
||||
|
||||
// tag::authorizationManagerFactory[]
|
||||
@Bean
|
||||
fun authorizationManagerFactory(optIn: OptInToMfaAuthorizationManager?): AuthorizationManagerFactory<Object> {
|
||||
val defaults = DefaultAuthorizationManagerFactory<Object>()
|
||||
defaults.setAdditionalAuthorization(optIn)
|
||||
return defaults
|
||||
}
|
||||
// end::authorizationManagerFactory[]
|
||||
|
||||
@NullMarked
|
||||
class MyPrincipal(val user: String, val optedIn: Boolean) : UserDetails {
|
||||
override fun getAuthorities(): MutableCollection<out GrantedAuthority> {
|
||||
return AuthorityUtils.createAuthorityList("app")
|
||||
}
|
||||
|
||||
override fun getPassword(): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun getUsername(): String {
|
||||
return this.user
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun users(): UserDetailsService {
|
||||
return UserDetailsService { username: String? -> MyPrincipal(username!!, username == "optedin") }
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun tokenGenerationSuccessHandler(): OneTimeTokenGenerationSuccessHandler {
|
||||
return RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent")
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.kt.docs.servlet.authentication.customauthorizationmanagerfactory
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken
|
||||
import org.springframework.security.config.test.SpringTestContext
|
||||
import org.springframework.security.config.test.SpringTestContextExtension
|
||||
import org.springframework.security.core.userdetails.UserDetailsService
|
||||
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors
|
||||
import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers
|
||||
import org.springframework.test.web.servlet.MockMvc
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
/**
|
||||
* Tests [CustomX509Configuration].
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@ExtendWith(SpringTestContextExtension::class)
|
||||
class CustomAuthorizationManagerFactoryTests {
|
||||
@JvmField
|
||||
val spring: SpringTestContext = SpringTestContext(this)
|
||||
|
||||
@Autowired
|
||||
var mockMvc: MockMvc? = null
|
||||
|
||||
@Autowired
|
||||
var users: UserDetailsService? = null
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun getWhenOptedInThenRedirectsToOtt() {
|
||||
this.spring.register(CustomAuthorizationManagerFactory::class.java, Http200Controller::class.java).autowire()
|
||||
val user = this.users!!.loadUserByUsername("optedin")
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/").with(SecurityMockMvcRequestPostProcessors.user(user)))
|
||||
.andExpect(MockMvcResultMatchers.status().is3xxRedirection())
|
||||
.andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor=ott"))
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun getWhenNotOptedInThenAllows() {
|
||||
this.spring.register(CustomAuthorizationManagerFactory::class.java, Http200Controller::class.java).autowire()
|
||||
val user = this.users!!.loadUserByUsername("user")
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/").with(SecurityMockMvcRequestPostProcessors.user(user)))
|
||||
.andExpect(MockMvcResultMatchers.status().isOk())
|
||||
.andExpect(SecurityMockMvcResultMatchers.authenticated().withUsername("user"))
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun getWhenOptedAndHasFactorThenAllows() {
|
||||
this.spring.register(CustomAuthorizationManagerFactory::class.java, Http200Controller::class.java).autowire()
|
||||
val user = this.users!!.loadUserByUsername("optedin")
|
||||
val token = TestingAuthenticationToken(user, "", "FACTOR_OTT")
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/").with(SecurityMockMvcRequestPostProcessors.authentication(token)))
|
||||
.andExpect(MockMvcResultMatchers.status().isOk())
|
||||
.andExpect(SecurityMockMvcResultMatchers.authenticated().withUsername("optedin"))
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@RestController
|
||||
internal class Http200Controller {
|
||||
@GetMapping("/**")
|
||||
fun ok(): String {
|
||||
return "ok"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
package org.springframework.security.kt.docs.servlet.authentication.multifactorauthentication
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
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.invoke
|
||||
import org.springframework.security.core.userdetails.User
|
||||
import org.springframework.security.core.userdetails.UserDetailsService
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager
|
||||
import org.springframework.security.web.SecurityFilterChain
|
||||
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler
|
||||
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
internal class ListAuthoritiesConfiguration {
|
||||
|
||||
// tag::httpSecurity[]
|
||||
@Bean
|
||||
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? {
|
||||
// @formatter:off
|
||||
http {
|
||||
authorizeHttpRequests {
|
||||
authorize(anyRequest, hasAllAuthorities("FACTOR_PASSWORD", "FACTOR_OTT"))
|
||||
}
|
||||
formLogin { }
|
||||
oneTimeTokenLogin { }
|
||||
}
|
||||
// @formatter:on
|
||||
return http.build()
|
||||
}
|
||||
// end::httpSecurity[]
|
||||
|
||||
|
||||
// end::httpSecurity[]
|
||||
@Bean
|
||||
fun userDetailsService(): UserDetailsService {
|
||||
return InMemoryUserDetailsManager(
|
||||
User.withDefaultPasswordEncoder()
|
||||
.username("user")
|
||||
.password("password")
|
||||
.authorities("app")
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun tokenGenerationSuccessHandler(): OneTimeTokenGenerationSuccessHandler {
|
||||
return RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent")
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.kt.docs.servlet.authentication.multifactorauthentication
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.security.config.test.SpringTestContext
|
||||
import org.springframework.security.config.test.SpringTestContextExtension
|
||||
import org.springframework.security.test.context.support.WithMockUser
|
||||
import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener
|
||||
import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers
|
||||
import org.springframework.test.context.TestExecutionListeners
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||
import org.springframework.test.web.servlet.MockMvc
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
/**
|
||||
* Tests [CustomX509Configuration].
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@ExtendWith(SpringExtension::class, SpringTestContextExtension::class)
|
||||
@TestExecutionListeners(WithSecurityContextTestExecutionListener::class)
|
||||
class MultiFactorAuthenticationTests {
|
||||
@JvmField
|
||||
val spring: SpringTestContext = SpringTestContext(this)
|
||||
|
||||
@Autowired
|
||||
var mockMvc: MockMvc? = null
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = ["FACTOR_PASSWORD", "FACTOR_OTT"])
|
||||
@Throws(Exception::class)
|
||||
fun getWhenAuthenticatedWithPasswordAndOttThenPermits() {
|
||||
this.spring.register(ListAuthoritiesConfiguration::class.java, Http200Controller::class.java).autowire()
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
|
||||
.andExpect(MockMvcResultMatchers.status().isOk())
|
||||
.andExpect(SecurityMockMvcResultMatchers.authenticated().withUsername("user"))
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = ["FACTOR_PASSWORD"])
|
||||
@Throws(Exception::class)
|
||||
fun getWhenAuthenticatedWithPasswordThenRedirectsToOtt() {
|
||||
this.spring.register(ListAuthoritiesConfiguration::class.java, Http200Controller::class.java).autowire()
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
|
||||
.andExpect(MockMvcResultMatchers.status().is3xxRedirection())
|
||||
.andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor=ott"))
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = ["FACTOR_OTT"])
|
||||
@Throws(Exception::class)
|
||||
fun getWhenAuthenticatedWithOttThenRedirectsToPassword() {
|
||||
this.spring.register(ListAuthoritiesConfiguration::class.java, Http200Controller::class.java).autowire()
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
|
||||
.andExpect(MockMvcResultMatchers.status().is3xxRedirection())
|
||||
.andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor=password"))
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
@Throws(Exception::class)
|
||||
fun getWhenAuthenticatedThenRedirectsToPassword() {
|
||||
this.spring.register(ListAuthoritiesConfiguration::class.java, Http200Controller::class.java).autowire()
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
|
||||
.andExpect(MockMvcResultMatchers.status().is3xxRedirection())
|
||||
.andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor=password"))
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun getWhenUnauthenticatedThenRedirectsToBoth() {
|
||||
this.spring.register(ListAuthoritiesConfiguration::class.java, Http200Controller::class.java).autowire()
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
|
||||
.andExpect(MockMvcResultMatchers.status().is3xxRedirection())
|
||||
.andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login"))
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@RestController
|
||||
internal class Http200Controller {
|
||||
@GetMapping("/**")
|
||||
fun ok(): String {
|
||||
return "ok"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,129 @@
|
||||
package org.springframework.security.kt.docs.servlet.authentication.obtainingmoreauthorization
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import jakarta.servlet.http.HttpServletResponse
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.security.authorization.AllAuthoritiesAuthorizationManager.hasAllAuthorities
|
||||
import org.springframework.security.authorization.AuthorizationDecision
|
||||
import org.springframework.security.authorization.AuthorizationManager
|
||||
import org.springframework.security.authorization.AuthorizationManagerFactory
|
||||
import org.springframework.security.authorization.AuthorizationManagers.allOf
|
||||
import org.springframework.security.authorization.DefaultAuthorizationManagerFactory
|
||||
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.configurers.ExceptionHandlingConfigurer
|
||||
import org.springframework.security.config.annotation.web.invoke
|
||||
import org.springframework.security.core.AuthenticationException
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository
|
||||
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository
|
||||
import org.springframework.security.oauth2.client.registration.TestClientRegistrations
|
||||
import org.springframework.security.web.AuthenticationEntryPoint
|
||||
import org.springframework.security.web.DefaultSecurityFilterChain
|
||||
import org.springframework.security.web.access.intercept.RequestAuthorizationContext
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
internal class MissingAuthorityConfiguration {
|
||||
|
||||
// tag::httpSecurity[]
|
||||
@Bean
|
||||
fun securityFilterChain(http: HttpSecurity, oauth2: ScopeRetrievingAuthenticationEntryPoint): DefaultSecurityFilterChain? {
|
||||
http {
|
||||
authorizeHttpRequests {
|
||||
authorize("/profile/**", hasAuthority("SCOPE_profile:read"))
|
||||
authorize(anyRequest, authenticated)
|
||||
}
|
||||
x509 { }
|
||||
oauth2Login { }
|
||||
}
|
||||
|
||||
http.exceptionHandling { e: ExceptionHandlingConfigurer<HttpSecurity> -> e
|
||||
.defaultDeniedHandlerForMissingAuthority(oauth2, "SCOPE_profile:read")
|
||||
}
|
||||
return http.build()
|
||||
}
|
||||
// end::httpSecurity[]
|
||||
|
||||
// tag::authenticationEntryPoint[]
|
||||
@Component
|
||||
internal class ScopeRetrievingAuthenticationEntryPoint : AuthenticationEntryPoint {
|
||||
override fun commence(request: HttpServletRequest, response: HttpServletResponse, authException: AuthenticationException) {
|
||||
response.sendRedirect("https://authz.example.org/authorize?scope=profile:read")
|
||||
}
|
||||
}
|
||||
// end::authenticationEntryPoint[]
|
||||
|
||||
// tag::authorizationManagerFactoryBean[]
|
||||
@Bean
|
||||
fun authz(): AuthorizationManagerFactory<RequestAuthorizationContext> {
|
||||
return FactorAuthorizationManagerFactory(hasAllAuthorities("FACTOR_X509", "FACTOR_AUTHORIZATION_CODE"))
|
||||
}
|
||||
// end::authorizationManagerFactoryBean[]
|
||||
|
||||
// tag::authorizationManagerFactory[]
|
||||
internal inner class FactorAuthorizationManagerFactory(private val hasAuthorities: AuthorizationManager<RequestAuthorizationContext>) :
|
||||
AuthorizationManagerFactory<RequestAuthorizationContext> {
|
||||
private val delegate = DefaultAuthorizationManagerFactory<RequestAuthorizationContext>()
|
||||
|
||||
override fun permitAll(): AuthorizationManager<RequestAuthorizationContext> {
|
||||
return this.delegate.permitAll()
|
||||
}
|
||||
|
||||
override fun denyAll(): AuthorizationManager<RequestAuthorizationContext> {
|
||||
return this.delegate.denyAll()
|
||||
}
|
||||
|
||||
override fun hasRole(role: String): AuthorizationManager<RequestAuthorizationContext> {
|
||||
return hasAnyRole(role)
|
||||
}
|
||||
|
||||
override fun hasAnyRole(vararg roles: String): AuthorizationManager<RequestAuthorizationContext> {
|
||||
return addFactors(this.delegate.hasAnyRole(*roles))
|
||||
}
|
||||
|
||||
override fun hasAllRoles(vararg roles: String): AuthorizationManager<RequestAuthorizationContext> {
|
||||
return addFactors(this.delegate.hasAllRoles(*roles))
|
||||
}
|
||||
|
||||
override fun hasAuthority(authority: String): AuthorizationManager<RequestAuthorizationContext> {
|
||||
return hasAnyAuthority(authority)
|
||||
}
|
||||
|
||||
override fun hasAnyAuthority(vararg authorities: String): AuthorizationManager<RequestAuthorizationContext> {
|
||||
return addFactors(this.delegate.hasAnyAuthority(*authorities))
|
||||
}
|
||||
|
||||
override fun hasAllAuthorities(vararg authorities: String): AuthorizationManager<RequestAuthorizationContext> {
|
||||
return addFactors(this.delegate.hasAllAuthorities(*authorities))
|
||||
}
|
||||
|
||||
override fun authenticated(): AuthorizationManager<RequestAuthorizationContext> {
|
||||
return addFactors(this.delegate.authenticated())
|
||||
}
|
||||
|
||||
override fun fullyAuthenticated(): AuthorizationManager<RequestAuthorizationContext> {
|
||||
return addFactors(this.delegate.fullyAuthenticated())
|
||||
}
|
||||
|
||||
override fun rememberMe(): AuthorizationManager<RequestAuthorizationContext> {
|
||||
return addFactors(this.delegate.rememberMe())
|
||||
}
|
||||
|
||||
override fun anonymous(): AuthorizationManager<RequestAuthorizationContext> {
|
||||
return this.delegate.anonymous()
|
||||
}
|
||||
|
||||
private fun addFactors(delegate: AuthorizationManager<RequestAuthorizationContext>): AuthorizationManager<RequestAuthorizationContext> {
|
||||
return allOf(AuthorizationDecision(false), this.hasAuthorities, delegate)
|
||||
}
|
||||
}
|
||||
// end::authorizationManagerFactory[]
|
||||
|
||||
// end::authenticationEntryPoint[]
|
||||
@Bean
|
||||
fun clients(): ClientRegistrationRepository {
|
||||
return InMemoryClientRegistrationRepository(TestClientRegistrations.clientRegistration().build())
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.kt.docs.servlet.authentication.obtainingmoreauthorization
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.security.config.test.SpringTestContext
|
||||
import org.springframework.security.config.test.SpringTestContextExtension
|
||||
import org.springframework.security.docs.servlet.authentication.obtainingmoreauthorization.ScopeConfiguration
|
||||
import org.springframework.security.test.context.support.WithMockUser
|
||||
import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener
|
||||
import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers
|
||||
import org.springframework.test.context.TestExecutionListeners
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||
import org.springframework.test.web.servlet.MockMvc
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
/**
|
||||
* Tests [CustomX509Configuration].
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@ExtendWith(SpringExtension::class, SpringTestContextExtension::class)
|
||||
@TestExecutionListeners(WithSecurityContextTestExecutionListener::class)
|
||||
class ObtainingMoreAuthorizationTests {
|
||||
@JvmField
|
||||
val spring: SpringTestContext = SpringTestContext(this)
|
||||
|
||||
@Autowired
|
||||
var mockMvc: MockMvc? = null
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
@Throws(Exception::class)
|
||||
fun profileWhenScopeConfigurationThenDenies() {
|
||||
this.spring.register(ScopeConfiguration::class.java, Http200Controller::class.java).autowire()
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/profile"))
|
||||
.andExpect(MockMvcResultMatchers.status().isForbidden())
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = ["FACTOR_X509", "FACTOR_AUTHORIZATION_CODE"])
|
||||
@Throws(Exception::class)
|
||||
fun profileWhenMissingAuthorityConfigurationThenRedirectsToAuthorizationServer() {
|
||||
this.spring.register(MissingAuthorityConfiguration::class.java, Http200Controller::class.java).autowire()
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/profile"))
|
||||
.andExpect(MockMvcResultMatchers.status().is3xxRedirection())
|
||||
.andExpect(MockMvcResultMatchers.redirectedUrl("https://authz.example.org/authorize?scope=profile:read"))
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = ["SCOPE_profile:read"])
|
||||
@Throws(Exception::class)
|
||||
fun profileWhenMissingX509WithOttThenForbidden() {
|
||||
this.spring.register(MissingAuthorityConfiguration::class.java, Http200Controller::class.java).autowire()
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/profile"))
|
||||
.andExpect(MockMvcResultMatchers.status().isForbidden())
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = ["FACTOR_X509", "FACTOR_AUTHORIZATION_CODE", "SCOPE_profile:read"])
|
||||
@Throws(
|
||||
Exception::class
|
||||
)
|
||||
fun profileWhenAuthenticatedAndHasScopeThenPermits() {
|
||||
this.spring.register(MissingAuthorityConfiguration::class.java, Http200Controller::class.java).autowire()
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/profile"))
|
||||
.andExpect(MockMvcResultMatchers.status().isOk())
|
||||
.andExpect(SecurityMockMvcResultMatchers.authenticated().withUsername("user"))
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@RestController
|
||||
internal class Http200Controller {
|
||||
@GetMapping("/**")
|
||||
fun ok(): String {
|
||||
return "ok"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
package org.springframework.security.kt.docs.servlet.authentication.obtainingmoreauthorization
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
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.invoke
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository
|
||||
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository
|
||||
import org.springframework.security.oauth2.client.registration.TestClientRegistrations
|
||||
import org.springframework.security.web.SecurityFilterChain
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class ScopeConfiguration {
|
||||
// tag::httpSecurity[]
|
||||
@Bean
|
||||
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? {
|
||||
// @formatter:off
|
||||
http {
|
||||
authorizeHttpRequests {
|
||||
authorize("/profile/**", hasAuthority("SCOPE_profile:read"))
|
||||
authorize(anyRequest, authenticated)
|
||||
}
|
||||
x509 { }
|
||||
oauth2Login { }
|
||||
}
|
||||
// @formatter:on
|
||||
return http.build()
|
||||
}
|
||||
// end::httpSecurity[]
|
||||
|
||||
// end::httpSecurity[]
|
||||
@Bean
|
||||
fun clients(): ClientRegistrationRepository {
|
||||
return InMemoryClientRegistrationRepository(TestClientRegistrations.clientRegistration().build())
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.kt.docs.servlet.authentication.reauthentication
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.security.config.test.SpringTestContext
|
||||
import org.springframework.security.config.test.SpringTestContextExtension
|
||||
import org.springframework.security.docs.servlet.authentication.reauthentication.RequireOttConfiguration
|
||||
import org.springframework.security.docs.servlet.authentication.reauthentication.SimpleConfiguration
|
||||
import org.springframework.security.test.context.support.WithMockUser
|
||||
import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener
|
||||
import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers
|
||||
import org.springframework.test.context.TestExecutionListeners
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||
import org.springframework.test.web.servlet.MockMvc
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
/**
|
||||
* Tests [CustomX509Configuration].
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@ExtendWith(SpringExtension::class, SpringTestContextExtension::class)
|
||||
@TestExecutionListeners(WithSecurityContextTestExecutionListener::class)
|
||||
class ReauthenticationTests {
|
||||
@JvmField
|
||||
val spring: SpringTestContext = SpringTestContext(this)
|
||||
|
||||
@Autowired
|
||||
var mockMvc: MockMvc? = null
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
@Throws(Exception::class)
|
||||
fun formLoginWhenSimpleConfigurationThenPermits() {
|
||||
this.spring.register(SimpleConfiguration::class.java, Http200Controller::class.java).autowire()
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
|
||||
.andExpect(MockMvcResultMatchers.status().isOk())
|
||||
.andExpect(SecurityMockMvcResultMatchers.authenticated().withUsername("user"))
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
@Throws(Exception::class)
|
||||
fun formLoginWhenRequireOttConfigurationThenRedirectsToOtt() {
|
||||
this.spring.register(RequireOttConfiguration::class.java, Http200Controller::class.java).autowire()
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/profile"))
|
||||
.andExpect(MockMvcResultMatchers.status().is3xxRedirection())
|
||||
.andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor=ott"))
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = ["FACTOR_OTT"])
|
||||
@Throws(Exception::class)
|
||||
fun ottWhenRequireOttConfigurationThenAllows() {
|
||||
this.spring.register(RequireOttConfiguration::class.java, Http200Controller::class.java).autowire()
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/profile"))
|
||||
.andExpect(MockMvcResultMatchers.status().isOk())
|
||||
.andExpect(SecurityMockMvcResultMatchers.authenticated().withUsername("user"))
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@RestController
|
||||
internal class Http200Controller {
|
||||
@GetMapping("/**")
|
||||
fun ok(): String {
|
||||
return "ok"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
package org.springframework.security.kt.docs.servlet.authentication.reauthentication
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
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.invoke
|
||||
import org.springframework.security.core.userdetails.User
|
||||
import org.springframework.security.core.userdetails.UserDetailsService
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager
|
||||
import org.springframework.security.web.SecurityFilterChain
|
||||
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler
|
||||
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class RequireOttConfiguration {
|
||||
|
||||
// tag::httpSecurity[]
|
||||
@Bean
|
||||
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? {
|
||||
// @formatter:off
|
||||
http {
|
||||
authorizeHttpRequests {
|
||||
authorize("/profile/**", hasAuthority("FACTOR_OTT")) // <1>
|
||||
authorize(anyRequest, authenticated)
|
||||
}
|
||||
formLogin { }
|
||||
oneTimeTokenLogin { }
|
||||
}
|
||||
// @formatter:on
|
||||
return http.build()
|
||||
}
|
||||
// end::httpSecurity[]
|
||||
|
||||
// end::httpSecurity[]
|
||||
@Bean
|
||||
fun userDetailsService(): UserDetailsService {
|
||||
return InMemoryUserDetailsManager(
|
||||
User.withDefaultPasswordEncoder()
|
||||
.username("user")
|
||||
.password("password")
|
||||
.authorities("app")
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun tokenGenerationSuccessHandler(): OneTimeTokenGenerationSuccessHandler {
|
||||
return RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent")
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
package org.springframework.security.kt.docs.servlet.authentication.reauthentication
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
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.invoke
|
||||
import org.springframework.security.core.userdetails.User
|
||||
import org.springframework.security.core.userdetails.UserDetailsService
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager
|
||||
import org.springframework.security.web.SecurityFilterChain
|
||||
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler
|
||||
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class SimpleConfiguration {
|
||||
// tag::httpSecurity[]
|
||||
@Bean
|
||||
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? {
|
||||
// @formatter:off
|
||||
http {
|
||||
authorizeHttpRequests {
|
||||
authorize(anyRequest, authenticated)
|
||||
}
|
||||
formLogin { }
|
||||
oneTimeTokenLogin { }
|
||||
}
|
||||
// @formatter:on
|
||||
return http.build()
|
||||
}
|
||||
// end::httpSecurity[]
|
||||
|
||||
// end::httpSecurity[]
|
||||
@Bean
|
||||
fun userDetailsService(): UserDetailsService {
|
||||
return InMemoryUserDetailsManager(
|
||||
User.withDefaultPasswordEncoder()
|
||||
.username("user")
|
||||
.password("password")
|
||||
.authorities("app")
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun tokenGenerationSuccessHandler(): OneTimeTokenGenerationSuccessHandler {
|
||||
return RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent")
|
||||
}
|
||||
}
|
||||
@ -16,6 +16,9 @@
|
||||
|
||||
package org.springframework.security.web;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.web.access.WebInvocationPrivilegeEvaluator;
|
||||
|
||||
/**
|
||||
@ -52,6 +55,17 @@ public final class WebAttributes {
|
||||
public static final String WEB_INVOCATION_PRIVILEGE_EVALUATOR_ATTRIBUTE = WebAttributes.class.getName()
|
||||
+ ".WEB_INVOCATION_PRIVILEGE_EVALUATOR_ATTRIBUTE";
|
||||
|
||||
/**
|
||||
* Used to set a {@code Collection} of {@link GrantedAuthority} instances into the
|
||||
* {@link HttpServletRequest}.
|
||||
* <p>
|
||||
* Represents what authorities are missing to be authorized for the current request
|
||||
*
|
||||
* @since 7.0
|
||||
* @see org.springframework.security.web.access.DelegatingMissingAuthorityAccessDeniedHandler
|
||||
*/
|
||||
public static final String MISSING_AUTHORITIES = WebAttributes.class + ".MISSING_AUTHORITIES";
|
||||
|
||||
private WebAttributes() {
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,209 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.web.access;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.authentication.InsufficientAuthenticationException;
|
||||
import org.springframework.security.authorization.AuthorityAuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationDeniedException;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.WebAttributes;
|
||||
import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint;
|
||||
import org.springframework.security.web.savedrequest.NullRequestCache;
|
||||
import org.springframework.security.web.savedrequest.RequestCache;
|
||||
import org.springframework.security.web.util.ThrowableAnalyzer;
|
||||
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An {@link AccessDeniedHandler} that adapts {@link AuthenticationEntryPoint}s based on
|
||||
* missing {@link GrantedAuthority}s. These authorities are specified in an
|
||||
* {@link AuthorityAuthorizationDecision} inside an {@link AuthorizationDeniedException}.
|
||||
*
|
||||
* <p>
|
||||
* This is helpful in adaptive authentication scenarios where an
|
||||
* {@link org.springframework.security.authorization.AuthorizationManager} indicates
|
||||
* additional authorities needed to access a given resource.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* For example, if an
|
||||
* {@link org.springframework.security.authorization.AuthorizationManager} states that to
|
||||
* access the home page, the user needs the {@code FACTOR_OTT} authority, then this
|
||||
* handler can be configured in the following way to redirect to the one-time-token login
|
||||
* page:
|
||||
* </p>
|
||||
*
|
||||
* <code>
|
||||
* AccessDeniedHandler handler = DelegatingMissingAuthorityAccessDeniedHandler.builder()
|
||||
* .addEntryPointFor(new LoginUrlAuthenticationEntryPoint("/login"), "FACTOR_OTT")
|
||||
* .addEntryPointFor(new MyCustomEntryPoint(), "FACTOR_PASSWORD")
|
||||
* .build();
|
||||
* </code>
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 7.0
|
||||
* @see AuthorizationDeniedException
|
||||
* @see AuthorityAuthorizationDecision
|
||||
* @see org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer
|
||||
*/
|
||||
public final class DelegatingMissingAuthorityAccessDeniedHandler implements AccessDeniedHandler {
|
||||
|
||||
private final ThrowableAnalyzer throwableAnalyzer = new ThrowableAnalyzer();
|
||||
|
||||
private final Map<String, AuthenticationEntryPoint> entryPoints;
|
||||
|
||||
private RequestCache requestCache = new NullRequestCache();
|
||||
|
||||
private AccessDeniedHandler defaultAccessDeniedHandler = new AccessDeniedHandlerImpl();
|
||||
|
||||
private DelegatingMissingAuthorityAccessDeniedHandler(Map<String, AuthenticationEntryPoint> entryPoints) {
|
||||
Assert.notEmpty(entryPoints, "entryPoints cannot be empty");
|
||||
this.entryPoints = entryPoints;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException denied)
|
||||
throws IOException, ServletException {
|
||||
Collection<GrantedAuthority> authorities = missingAuthorities(denied);
|
||||
for (GrantedAuthority needed : authorities) {
|
||||
AuthenticationEntryPoint entryPoint = this.entryPoints.get(needed.getAuthority());
|
||||
if (entryPoint == null) {
|
||||
continue;
|
||||
}
|
||||
this.requestCache.saveRequest(request, response);
|
||||
request.setAttribute(WebAttributes.MISSING_AUTHORITIES, List.of(needed));
|
||||
String message = String.format("Missing Authorities %s", List.of(needed));
|
||||
AuthenticationException ex = new InsufficientAuthenticationException(message, denied);
|
||||
entryPoint.commence(request, response, ex);
|
||||
return;
|
||||
}
|
||||
this.defaultAccessDeniedHandler.handle(request, response, denied);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this {@link AccessDeniedHandler} for {@link AccessDeniedException}s that this
|
||||
* handler doesn't support. By default, this uses {@link AccessDeniedHandlerImpl}.
|
||||
* @param defaultAccessDeniedHandler the default {@link AccessDeniedHandler} to use
|
||||
*/
|
||||
public void setDefaultAccessDeniedHandler(AccessDeniedHandler defaultAccessDeniedHandler) {
|
||||
Assert.notNull(defaultAccessDeniedHandler, "defaultAccessDeniedHandler cannot be null");
|
||||
this.defaultAccessDeniedHandler = defaultAccessDeniedHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this {@link RequestCache} to remember the current request.
|
||||
* <p>
|
||||
* Uses {@link NullRequestCache} by default
|
||||
* </p>
|
||||
* @param requestCache the {@link RequestCache} to use
|
||||
*/
|
||||
public void setRequestCache(RequestCache requestCache) {
|
||||
Assert.notNull(requestCache, "requestCachgrantedaue cannot be null");
|
||||
this.requestCache = requestCache;
|
||||
}
|
||||
|
||||
private Collection<GrantedAuthority> missingAuthorities(AccessDeniedException ex) {
|
||||
AuthorizationDeniedException denied = findAuthorizationDeniedException(ex);
|
||||
if (denied == null) {
|
||||
return List.of();
|
||||
}
|
||||
if (!(denied.getAuthorizationResult() instanceof AuthorityAuthorizationDecision authorization)) {
|
||||
return List.of();
|
||||
}
|
||||
return authorization.getAuthorities();
|
||||
}
|
||||
|
||||
private @Nullable AuthorizationDeniedException findAuthorizationDeniedException(AccessDeniedException ex) {
|
||||
if (ex instanceof AuthorizationDeniedException denied) {
|
||||
return denied;
|
||||
}
|
||||
Throwable[] chain = this.throwableAnalyzer.determineCauseChain(ex);
|
||||
return (AuthorizationDeniedException) this.throwableAnalyzer
|
||||
.getFirstThrowableOfType(AuthorizationDeniedException.class, chain);
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for configuring the set of authority/entry-point pairs
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 7.0
|
||||
*/
|
||||
public static final class Builder {
|
||||
|
||||
private final Map<String, DelegatingAuthenticationEntryPoint.Builder> entryPointBuilderByAuthority = new LinkedHashMap<>();
|
||||
|
||||
private Builder() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this {@link AuthenticationEntryPoint} when the given
|
||||
* {@code missingAuthority} is missing from the authenticated user
|
||||
* @param entryPoint the {@link AuthenticationEntryPoint} for the given authority
|
||||
* @param missingAuthority the authority
|
||||
* @return the {@link Builder} for further configurations
|
||||
*/
|
||||
public Builder addEntryPointFor(AuthenticationEntryPoint entryPoint, String missingAuthority) {
|
||||
DelegatingAuthenticationEntryPoint.Builder builder = DelegatingAuthenticationEntryPoint.builder()
|
||||
.addEntryPointFor(entryPoint, AnyRequestMatcher.INSTANCE);
|
||||
this.entryPointBuilderByAuthority.put(missingAuthority, builder);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this {@link AuthenticationEntryPoint} when the given
|
||||
* {@code missingAuthority} is missing from the authenticated user
|
||||
* @param entryPoint a consumer to configure the underlying
|
||||
* {@link DelegatingAuthenticationEntryPoint}
|
||||
* @param missingAuthority the authority
|
||||
* @return the {@link Builder} for further configurations
|
||||
*/
|
||||
public Builder addEntryPointFor(Consumer<DelegatingAuthenticationEntryPoint.Builder> entryPoint,
|
||||
String missingAuthority) {
|
||||
entryPoint.accept(this.entryPointBuilderByAuthority.computeIfAbsent(missingAuthority,
|
||||
(k) -> DelegatingAuthenticationEntryPoint.builder()));
|
||||
return this;
|
||||
}
|
||||
|
||||
public DelegatingMissingAuthorityAccessDeniedHandler build() {
|
||||
Map<String, AuthenticationEntryPoint> entryPointByAuthority = new LinkedHashMap<>();
|
||||
this.entryPointBuilderByAuthority.forEach((key, value) -> entryPointByAuthority.put(key, value.build()));
|
||||
return new DelegatingMissingAuthorityAccessDeniedHandler(entryPointByAuthority);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -196,7 +196,8 @@ public class ExceptionTranslationFilter extends GenericFilterBean implements Mes
|
||||
}
|
||||
AuthenticationException ex = new InsufficientAuthenticationException(
|
||||
this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication",
|
||||
"Full authentication is required to access this resource"));
|
||||
"Full authentication is required to access this resource"),
|
||||
exception);
|
||||
ex.setAuthenticationRequest(authentication);
|
||||
sendStartAuthentication(request, response, chain, ex);
|
||||
}
|
||||
|
||||
@ -17,6 +17,8 @@
|
||||
package org.springframework.security.web.authentication;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.Locale;
|
||||
|
||||
import jakarta.servlet.RequestDispatcher;
|
||||
import jakarta.servlet.ServletException;
|
||||
@ -30,16 +32,20 @@ import org.jspecify.annotations.Nullable;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.DefaultRedirectStrategy;
|
||||
import org.springframework.security.web.PortMapper;
|
||||
import org.springframework.security.web.PortMapperImpl;
|
||||
import org.springframework.security.web.RedirectStrategy;
|
||||
import org.springframework.security.web.WebAttributes;
|
||||
import org.springframework.security.web.access.ExceptionTranslationFilter;
|
||||
import org.springframework.security.web.util.RedirectUrlBuilder;
|
||||
import org.springframework.security.web.util.UrlUtils;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
* Used by the {@link ExceptionTranslationFilter} to commence a form login authentication
|
||||
@ -68,6 +74,8 @@ public class LoginUrlAuthenticationEntryPoint implements AuthenticationEntryPoin
|
||||
|
||||
private static final Log logger = LogFactory.getLog(LoginUrlAuthenticationEntryPoint.class);
|
||||
|
||||
private static final String FACTOR_PREFIX = "FACTOR_";
|
||||
|
||||
private PortMapper portMapper = new PortMapperImpl();
|
||||
|
||||
private String loginFormUrl;
|
||||
@ -107,9 +115,29 @@ public class LoginUrlAuthenticationEntryPoint implements AuthenticationEntryPoin
|
||||
* @param exception the exception
|
||||
* @return the URL (cannot be null or empty; defaults to {@link #getLoginFormUrl()})
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
protected String determineUrlToUseForThisRequest(HttpServletRequest request, HttpServletResponse response,
|
||||
AuthenticationException exception) {
|
||||
return getLoginFormUrl();
|
||||
Collection<GrantedAuthority> authorities = getAttribute(request, WebAttributes.MISSING_AUTHORITIES,
|
||||
Collection.class);
|
||||
if (CollectionUtils.isEmpty(authorities)) {
|
||||
return getLoginFormUrl();
|
||||
}
|
||||
Collection<String> factors = authorities.stream()
|
||||
.filter((a) -> a.getAuthority().startsWith(FACTOR_PREFIX))
|
||||
.map((a) -> a.getAuthority().substring(FACTOR_PREFIX.length()).toLowerCase(Locale.ROOT))
|
||||
.toList();
|
||||
return UriComponentsBuilder.fromUriString(getLoginFormUrl()).queryParam("factor", factors).toUriString();
|
||||
}
|
||||
|
||||
private static <T> @Nullable T getAttribute(HttpServletRequest request, String name, Class<T> clazz) {
|
||||
Object value = request.getAttribute(name);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String message = String.format("Found %s in %s, but expecting a %s", value.getClass(), name, clazz);
|
||||
Assert.isInstanceOf(clazz, value, message);
|
||||
return (T) value;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -18,9 +18,12 @@ package org.springframework.security.web.authentication.ui;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
@ -31,10 +34,14 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.filter.GenericFilterBean;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
* For internal use with namespace configuration in the case where a user doesn't
|
||||
@ -52,6 +59,9 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
||||
|
||||
public static final String ERROR_PARAMETER_NAME = "error";
|
||||
|
||||
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
|
||||
.getContextHolderStrategy();
|
||||
|
||||
private @Nullable String loginPageUrl;
|
||||
|
||||
private @Nullable String logoutSuccessUrl;
|
||||
@ -78,6 +88,10 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
||||
|
||||
private @Nullable String rememberMeParameter;
|
||||
|
||||
private final String factorParameter = "factor";
|
||||
|
||||
private final Collection<String> allowedParameters = List.of(this.factorParameter);
|
||||
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
private Map<String, String> oauth2AuthenticationUrlToClientName;
|
||||
|
||||
@ -109,6 +123,18 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this {@link SecurityContextHolderStrategy} to retrieve authenticated users.
|
||||
* <p>
|
||||
* Uses {@link SecurityContextHolder#getContextHolderStrategy()} by default.
|
||||
* @param securityContextHolderStrategy the strategy to use
|
||||
* @since 7.0
|
||||
*/
|
||||
public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
|
||||
Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
|
||||
this.securityContextHolderStrategy = securityContextHolderStrategy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a Function used to resolve a Map of the hidden inputs where the key is the
|
||||
* name of the input and the value is the value of the input. Typically this is used
|
||||
@ -223,16 +249,43 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
||||
String errorMsg = "Invalid credentials";
|
||||
String contextPath = request.getContextPath();
|
||||
|
||||
return HtmlTemplates.fromTemplate(LOGIN_PAGE_TEMPLATE)
|
||||
HtmlTemplates.Builder builder = HtmlTemplates.fromTemplate(LOGIN_PAGE_TEMPLATE)
|
||||
.withRawHtml("contextPath", contextPath)
|
||||
.withRawHtml("javaScript", renderJavaScript(request, contextPath))
|
||||
.withRawHtml("formLogin", renderFormLogin(request, loginError, logoutSuccess, contextPath, errorMsg))
|
||||
.withRawHtml("oneTimeTokenLogin",
|
||||
renderOneTimeTokenLogin(request, loginError, logoutSuccess, contextPath, errorMsg))
|
||||
.withRawHtml("oauth2Login", renderOAuth2Login(loginError, logoutSuccess, errorMsg, contextPath))
|
||||
.withRawHtml("saml2Login", renderSaml2Login(loginError, logoutSuccess, errorMsg, contextPath))
|
||||
.withRawHtml("passkeyLogin", renderPasskeyLogin())
|
||||
.render();
|
||||
.withRawHtml("javaScript", "")
|
||||
.withRawHtml("formLogin", "")
|
||||
.withRawHtml("oneTimeTokenLogin", "")
|
||||
.withRawHtml("oauth2Login", "")
|
||||
.withRawHtml("saml2Login", "")
|
||||
.withRawHtml("passkeyLogin", "");
|
||||
|
||||
Predicate<String> wantsAuthority = wantsAuthority(request);
|
||||
if (wantsAuthority.test("webauthn")) {
|
||||
builder.withRawHtml("javaScript", renderJavaScript(request, contextPath))
|
||||
.withRawHtml("passkeyLogin", renderPasskeyLogin());
|
||||
}
|
||||
if (wantsAuthority.test("password")) {
|
||||
builder.withRawHtml("formLogin",
|
||||
renderFormLogin(request, loginError, logoutSuccess, contextPath, errorMsg));
|
||||
}
|
||||
if (wantsAuthority.test("ott")) {
|
||||
builder.withRawHtml("oneTimeTokenLogin",
|
||||
renderOneTimeTokenLogin(request, loginError, logoutSuccess, contextPath, errorMsg));
|
||||
}
|
||||
if (wantsAuthority.test("authorization_code")) {
|
||||
builder.withRawHtml("oauth2Login", renderOAuth2Login(loginError, logoutSuccess, errorMsg, contextPath));
|
||||
}
|
||||
if (wantsAuthority.test("saml_response")) {
|
||||
builder.withRawHtml("saml2Login", renderSaml2Login(loginError, logoutSuccess, errorMsg, contextPath));
|
||||
}
|
||||
return builder.render();
|
||||
}
|
||||
|
||||
private Predicate<String> wantsAuthority(HttpServletRequest request) {
|
||||
String[] authorities = request.getParameterValues(this.factorParameter);
|
||||
if (authorities == null) {
|
||||
return (authority) -> true;
|
||||
}
|
||||
return List.of(authorities)::contains;
|
||||
}
|
||||
|
||||
private String renderJavaScript(HttpServletRequest request, String contextPath) {
|
||||
@ -271,6 +324,13 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
||||
return "";
|
||||
}
|
||||
|
||||
String username = getUsername();
|
||||
String usernameInput = ((username != null)
|
||||
? HtmlTemplates.fromTemplate(FORM_READONLY_USERNAME_INPUT).withValue("username", username)
|
||||
: HtmlTemplates.fromTemplate(FORM_USERNAME_INPUT))
|
||||
.withValue("usernameParameter", this.usernameParameter)
|
||||
.render();
|
||||
|
||||
String hiddenInputs = this.resolveHiddenInputs.apply(request)
|
||||
.entrySet()
|
||||
.stream()
|
||||
@ -281,7 +341,7 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
||||
.withValue("loginUrl", contextPath + this.authenticationUrl)
|
||||
.withRawHtml("errorMessage", renderError(loginError, errorMsg))
|
||||
.withRawHtml("logoutMessage", renderSuccess(logoutSuccess))
|
||||
.withValue("usernameParameter", this.usernameParameter)
|
||||
.withRawHtml("usernameInput", usernameInput)
|
||||
.withValue("passwordParameter", this.passwordParameter)
|
||||
.withRawHtml("rememberMeInput", renderRememberMe(this.rememberMeParameter))
|
||||
.withRawHtml("hiddenInputs", hiddenInputs)
|
||||
@ -301,11 +361,17 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
||||
.map((inputKeyValue) -> renderHiddenInput(inputKeyValue.getKey(), inputKeyValue.getValue()))
|
||||
.collect(Collectors.joining("\n"));
|
||||
|
||||
String username = getUsername();
|
||||
String usernameInput = (username != null)
|
||||
? HtmlTemplates.fromTemplate(ONE_TIME_READONLY_USERNAME_INPUT).withValue("username", username).render()
|
||||
: ONE_TIME_USERNAME_INPUT;
|
||||
|
||||
return HtmlTemplates.fromTemplate(ONE_TIME_TEMPLATE)
|
||||
.withValue("generateOneTimeTokenUrl", contextPath + this.generateOneTimeTokenUrl)
|
||||
.withRawHtml("errorMessage", renderError(loginError, errorMsg))
|
||||
.withRawHtml("logoutMessage", renderSuccess(logoutSuccess))
|
||||
.withRawHtml("hiddenInputs", hiddenInputs)
|
||||
.withRawHtml("usernameInput", usernameInput)
|
||||
.render();
|
||||
}
|
||||
|
||||
@ -374,6 +440,14 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
||||
.render();
|
||||
}
|
||||
|
||||
private @Nullable String getUsername() {
|
||||
Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication();
|
||||
if (authentication != null && authentication.isAuthenticated()) {
|
||||
return authentication.getName();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isLogoutSuccess(HttpServletRequest request) {
|
||||
return this.logoutSuccessUrl != null && matches(request, this.logoutSuccessUrl);
|
||||
}
|
||||
@ -413,10 +487,19 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
||||
if (request.getQueryString() != null) {
|
||||
uri += "?" + request.getQueryString();
|
||||
}
|
||||
if ("".equals(request.getContextPath())) {
|
||||
return uri.equals(url);
|
||||
UriComponentsBuilder addAllowed = UriComponentsBuilder.fromUriString(url);
|
||||
for (String parameter : this.allowedParameters) {
|
||||
String[] values = request.getParameterValues(parameter);
|
||||
if (values != null) {
|
||||
for (String value : values) {
|
||||
addAllowed.queryParam(parameter, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return uri.equals(request.getContextPath() + url);
|
||||
if ("".equals(request.getContextPath())) {
|
||||
return uri.equals(addAllowed.toUriString());
|
||||
}
|
||||
return uri.equals(request.getContextPath() + addAllowed.toUriString());
|
||||
}
|
||||
|
||||
private static final String CSRF_HEADERS = """
|
||||
@ -466,7 +549,7 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
||||
{{errorMessage}}{{logoutMessage}}
|
||||
<p>
|
||||
<label for="username" class="screenreader">Username</label>
|
||||
<input type="text" id="username" name="{{usernameParameter}}" placeholder="Username" required autofocus>
|
||||
{{usernameInput}}
|
||||
</p>
|
||||
<p>
|
||||
<label for="password" class="screenreader">Password</label>
|
||||
@ -477,6 +560,14 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
||||
<button type="submit" class="primary">Sign in</button>
|
||||
</form>""";
|
||||
|
||||
private static final String FORM_READONLY_USERNAME_INPUT = """
|
||||
<input type="text" id="username" name="{{usernameParameter}}" value="{{username}}" placeholder="Username" required readonly>
|
||||
""";
|
||||
|
||||
private static final String FORM_USERNAME_INPUT = """
|
||||
<input type="text" id="username" name="{{usernameParameter}}" placeholder="Username" required autofocus>
|
||||
""";
|
||||
|
||||
private static final String HIDDEN_HTML_INPUT_TEMPLATE = """
|
||||
<input name="{{name}}" type="hidden" value="{{value}}" />
|
||||
""";
|
||||
@ -509,11 +600,19 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
||||
{{errorMessage}}{{logoutMessage}}
|
||||
<p>
|
||||
<label for="ott-username" class="screenreader">Username</label>
|
||||
<input type="text" id="ott-username" name="username" placeholder="Username" required>
|
||||
{{usernameInput}}
|
||||
</p>
|
||||
{{hiddenInputs}}
|
||||
<button class="primary" type="submit" form="ott-form">Send Token</button>
|
||||
</form>
|
||||
""";
|
||||
|
||||
private static final String ONE_TIME_READONLY_USERNAME_INPUT = """
|
||||
<input type="text" id="ott-username" name="username" value="{{username}}" placeholder="Username" required readonly>
|
||||
""";
|
||||
|
||||
private static final String ONE_TIME_USERNAME_INPUT = """
|
||||
<input type="text" id="ott-username" name="username" placeholder="Username" required>
|
||||
""";
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,145 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.web.access;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.authorization.AuthorityAuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationDeniedException;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.savedrequest.RequestCache;
|
||||
import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DelegatingMissingAuthorityAccessDeniedHandlerTests {
|
||||
|
||||
DelegatingMissingAuthorityAccessDeniedHandler.Builder builder;
|
||||
|
||||
MockHttpServletRequest request;
|
||||
|
||||
MockHttpServletResponse response;
|
||||
|
||||
@Mock
|
||||
AuthenticationEntryPoint factorEntryPoint;
|
||||
|
||||
@Mock
|
||||
AccessDeniedHandler defaultAccessDeniedHandler;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
this.builder = DelegatingMissingAuthorityAccessDeniedHandler.builder();
|
||||
this.builder.addEntryPointFor(this.factorEntryPoint, "FACTOR");
|
||||
this.request = new MockHttpServletRequest();
|
||||
this.response = new MockHttpServletResponse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenKnownAuthorityThenCommences() throws Exception {
|
||||
AccessDeniedHandler accessDeniedHandler = this.builder.build();
|
||||
accessDeniedHandler.handle(this.request, this.response, missingAuthorities("FACTOR"));
|
||||
verify(this.factorEntryPoint).commence(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenUnknownAuthorityThenDefaultCommences() throws Exception {
|
||||
DelegatingMissingAuthorityAccessDeniedHandler accessDeniedHandler = this.builder.build();
|
||||
accessDeniedHandler.setDefaultAccessDeniedHandler(this.defaultAccessDeniedHandler);
|
||||
accessDeniedHandler.handle(this.request, this.response, missingAuthorities("ROLE_USER"));
|
||||
verify(this.defaultAccessDeniedHandler).handle(any(), any(), any());
|
||||
verifyNoInteractions(this.factorEntryPoint);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenNoAuthoritiesFoundThenDefaultCommences() throws Exception {
|
||||
DelegatingMissingAuthorityAccessDeniedHandler accessDeniedHandler = this.builder.build();
|
||||
accessDeniedHandler.setDefaultAccessDeniedHandler(this.defaultAccessDeniedHandler);
|
||||
accessDeniedHandler.handle(this.request, this.response, new AccessDeniedException("access denied"));
|
||||
verify(this.defaultAccessDeniedHandler).handle(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenMultipleAuthoritiesThenFirstMatchCommences() throws Exception {
|
||||
AuthenticationEntryPoint passwordEntryPoint = mock(AuthenticationEntryPoint.class);
|
||||
this.builder.addEntryPointFor(passwordEntryPoint, "PASSWORD");
|
||||
AccessDeniedHandler accessDeniedHandler = this.builder.build();
|
||||
accessDeniedHandler.handle(this.request, this.response, missingAuthorities("PASSWORD", "FACTOR"));
|
||||
verify(passwordEntryPoint).commence(any(), any(), any());
|
||||
accessDeniedHandler.handle(this.request, this.response, missingAuthorities("FACTOR", "PASSWORD"));
|
||||
verify(this.factorEntryPoint).commence(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenCustomRequestCacheThenUses() throws Exception {
|
||||
RequestCache requestCache = mock(RequestCache.class);
|
||||
DelegatingMissingAuthorityAccessDeniedHandler accessDeniedHandler = this.builder.build();
|
||||
accessDeniedHandler.setRequestCache(requestCache);
|
||||
accessDeniedHandler.handle(this.request, this.response, missingAuthorities("FACTOR"));
|
||||
verify(requestCache).saveRequest(any(), any());
|
||||
verify(this.factorEntryPoint).commence(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenKnownAuthorityButNoRequestMatchThenCommences() throws Exception {
|
||||
AuthenticationEntryPoint passwordEntryPoint = mock(AuthenticationEntryPoint.class);
|
||||
RequestMatcher xhr = new RequestHeaderRequestMatcher("X-Requested-With");
|
||||
this.builder.addEntryPointFor((ep) -> ep.addEntryPointFor(passwordEntryPoint, xhr), "PASSWORD");
|
||||
AccessDeniedHandler accessDeniedHandler = this.builder.build();
|
||||
accessDeniedHandler.handle(this.request, this.response, missingAuthorities("PASSWORD"));
|
||||
verify(passwordEntryPoint).commence(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenMultipleEntryPointsThenFirstRequestMatchCommences() throws Exception {
|
||||
AuthenticationEntryPoint basicPasswordEntryPoint = mock(AuthenticationEntryPoint.class);
|
||||
AuthenticationEntryPoint formPasswordEntryPoint = mock(AuthenticationEntryPoint.class);
|
||||
RequestMatcher xhr = new RequestHeaderRequestMatcher("X-Requested-With");
|
||||
this.builder.addEntryPointFor(
|
||||
(ep) -> ep.addEntryPointFor(basicPasswordEntryPoint, xhr).defaultEntryPoint(formPasswordEntryPoint),
|
||||
"PASSWORD");
|
||||
AccessDeniedHandler accessDeniedHandler = this.builder.build();
|
||||
accessDeniedHandler.handle(this.request, this.response, missingAuthorities("PASSWORD"));
|
||||
verify(formPasswordEntryPoint).commence(any(), any(), any());
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader("X-Requested-With", "XmlHttpRequest");
|
||||
accessDeniedHandler.handle(request, this.response, missingAuthorities("PASSWORD"));
|
||||
verify(basicPasswordEntryPoint).commence(any(), any(), any());
|
||||
}
|
||||
|
||||
AuthorizationDeniedException missingAuthorities(String... authorities) {
|
||||
Collection<GrantedAuthority> granted = AuthorityUtils.createAuthorityList(authorities);
|
||||
AuthorityAuthorizationDecision decision = new AuthorityAuthorizationDecision(false, granted);
|
||||
return new AuthorizationDeniedException("access denied", decision);
|
||||
}
|
||||
|
||||
}
|
||||
@ -26,10 +26,15 @@ import org.junit.jupiter.api.Test;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.TestAuthentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||
import org.springframework.security.core.context.SecurityContextImpl;
|
||||
import org.springframework.security.web.WebAttributes;
|
||||
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
|
||||
import org.springframework.security.web.servlet.TestMockHttpServletRequests;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
@ -191,6 +196,83 @@ public class DefaultLoginPageGeneratingFilterTests {
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generateWhenOneTimeTokenRequestedThenOttForm() throws Exception {
|
||||
DefaultLoginPageGeneratingFilter filter = new DefaultLoginPageGeneratingFilter();
|
||||
filter.setLoginPageUrl(DefaultLoginPageGeneratingFilter.DEFAULT_LOGIN_PAGE_URL);
|
||||
filter.setFormLoginEnabled(true);
|
||||
filter.setOneTimeTokenEnabled(true);
|
||||
filter.setOneTimeTokenGenerationUrl("/ott/authenticate");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
filter.doFilter(TestMockHttpServletRequests.get("/login?factor=ott").build(), response, this.chain);
|
||||
assertThat(response.getContentAsString()).contains("Request a One-Time Token");
|
||||
assertThat(response.getContentAsString()).contains("""
|
||||
<form id="ott-form" class="login-form" method="post" action="/ott/authenticate">
|
||||
<h2>Request a One-Time Token</h2>
|
||||
|
||||
<p>
|
||||
<label for="ott-username" class="screenreader">Username</label>
|
||||
<input type="text" id="ott-username" name="username" placeholder="Username" required>
|
||||
</p>
|
||||
|
||||
<button class="primary" type="submit" form="ott-form">Send Token</button>
|
||||
</form>
|
||||
""");
|
||||
assertThat(response.getContentAsString()).doesNotContain("Password");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generateWhenTwoAuthoritiesRequestedThenBothForms() throws Exception {
|
||||
DefaultLoginPageGeneratingFilter filter = new DefaultLoginPageGeneratingFilter();
|
||||
filter.setLoginPageUrl(DefaultLoginPageGeneratingFilter.DEFAULT_LOGIN_PAGE_URL);
|
||||
filter.setFormLoginEnabled(true);
|
||||
filter.setUsernameParameter("username");
|
||||
filter.setPasswordParameter("password");
|
||||
filter.setOneTimeTokenEnabled(true);
|
||||
filter.setOneTimeTokenGenerationUrl("/ott/authenticate");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
filter.doFilter(TestMockHttpServletRequests.get("/login?factor=ott&factor=password").build(), response,
|
||||
this.chain);
|
||||
assertThat(response.getContentAsString()).contains("Request a One-Time Token");
|
||||
assertThat(response.getContentAsString()).contains("""
|
||||
<form id="ott-form" class="login-form" method="post" action="/ott/authenticate">
|
||||
<h2>Request a One-Time Token</h2>
|
||||
|
||||
<p>
|
||||
<label for="ott-username" class="screenreader">Username</label>
|
||||
<input type="text" id="ott-username" name="username" placeholder="Username" required>
|
||||
</p>
|
||||
|
||||
<button class="primary" type="submit" form="ott-form">Send Token</button>
|
||||
</form>
|
||||
""");
|
||||
assertThat(response.getContentAsString()).contains("Password");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generateWhenAuthenticatedThenReadOnlyUsername() throws Exception {
|
||||
SecurityContextHolderStrategy strategy = mock(SecurityContextHolderStrategy.class);
|
||||
DefaultLoginPageGeneratingFilter filter = new DefaultLoginPageGeneratingFilter();
|
||||
filter.setLoginPageUrl(DefaultLoginPageGeneratingFilter.DEFAULT_LOGIN_PAGE_URL);
|
||||
filter.setFormLoginEnabled(true);
|
||||
filter.setUsernameParameter("username");
|
||||
filter.setPasswordParameter("password");
|
||||
filter.setOneTimeTokenEnabled(true);
|
||||
filter.setOneTimeTokenGenerationUrl("/ott/authenticate");
|
||||
filter.setSecurityContextHolderStrategy(strategy);
|
||||
given(strategy.getContext()).willReturn(new SecurityContextImpl(TestAuthentication.authenticatedUser()));
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
filter.doFilter(TestMockHttpServletRequests.get("/login").build(), response, this.chain);
|
||||
assertThat(response.getContentAsString()).contains("Request a One-Time Token");
|
||||
assertThat(response.getContentAsString()).contains(
|
||||
"""
|
||||
<input type="text" id="ott-username" name="username" value="user" placeholder="Username" required readonly>
|
||||
""");
|
||||
assertThat(response.getContentAsString()).contains("""
|
||||
<input type="text" id="username" name="username" value="user" placeholder="Username" required readonly>
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void generatesThenRenders() throws ServletException, IOException {
|
||||
DefaultLoginPageGeneratingFilter filter = new DefaultLoginPageGeneratingFilter(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user