diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index ca28e4700e..6fc89dfcf3 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -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] diff --git a/docs/modules/ROOT/pages/servlet/authentication/adaptive.adoc b/docs/modules/ROOT/pages/servlet/authentication/adaptive.adoc new file mode 100644 index 0000000000..ef44b909c0 --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/authentication/adaptive.adoc @@ -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] diff --git a/docs/modules/ROOT/pages/whats-new.adoc b/docs/modules/ROOT/pages/whats-new.adoc index 3958db00af..59f8926dfa 100644 --- a/docs/modules/ROOT/pages/whats-new.adoc +++ b/docs/modules/ROOT/pages/whats-new.adoc @@ -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 diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/authorizationmanagerfactory/AuthorizationManagerFactoryTests.java b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/authorizationmanagerfactory/AuthorizationManagerFactoryTests.java new file mode 100644 index 0000000000..97899bc163 --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/authorizationmanagerfactory/AuthorizationManagerFactoryTests.java @@ -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"; + } + } +} diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/authorizationmanagerfactory/ListAuthoritiesEverywhereConfiguration.java b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/authorizationmanagerfactory/ListAuthoritiesEverywhereConfiguration.java new file mode 100644 index 0000000000..7c5728d807 --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/authorizationmanagerfactory/ListAuthoritiesEverywhereConfiguration.java @@ -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"); + } +} diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/authorizationmanagerfactory/UseAuthorizationManagerFactoryConfiguration.java b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/authorizationmanagerfactory/UseAuthorizationManagerFactoryConfiguration.java new file mode 100644 index 0000000000..0418e87c89 --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/authorizationmanagerfactory/UseAuthorizationManagerFactoryConfiguration.java @@ -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 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"); + } +} diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/customauthorizationmanagerfactory/CustomAuthorizationManagerFactory.java b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/customauthorizationmanagerfactory/CustomAuthorizationManagerFactory.java new file mode 100644 index 0000000000..f00cea15fb --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/customauthorizationmanagerfactory/CustomAuthorizationManagerFactory.java @@ -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 { + @Override + public AuthorizationResult authorize(Supplier 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 authorizationManagerFactory(OptInToMfaAuthorizationManager optIn) { + DefaultAuthorizationManagerFactory defaults = new DefaultAuthorizationManagerFactory<>(); + defaults.setAdditionalAuthorization(optIn); + return defaults; + } + // end::authorizationManagerFactory[] + + @NullMarked + record MyPrincipal(String username, boolean optedIn) implements UserDetails { + @Override + public Collection 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"); + } +} diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/customauthorizationmanagerfactory/CustomAuthorizationManagerFactoryTests.java b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/customauthorizationmanagerfactory/CustomAuthorizationManagerFactoryTests.java new file mode 100644 index 0000000000..f62cfeedff --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/customauthorizationmanagerfactory/CustomAuthorizationManagerFactoryTests.java @@ -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"; + } + } +} diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/multifactorauthentication/ListAuthoritiesConfiguration.java b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/multifactorauthentication/ListAuthoritiesConfiguration.java new file mode 100644 index 0000000000..f6a003c874 --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/multifactorauthentication/ListAuthoritiesConfiguration.java @@ -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"); + } +} diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/multifactorauthentication/MultiFactorAuthenticationTests.java b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/multifactorauthentication/MultiFactorAuthenticationTests.java new file mode 100644 index 0000000000..33e319622d --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/multifactorauthentication/MultiFactorAuthenticationTests.java @@ -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"; + } + } +} diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/obtainingmoreauthorization/MissingAuthorityConfiguration.java b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/obtainingmoreauthorization/MissingAuthorityConfiguration.java new file mode 100644 index 0000000000..4ccc0c2895 --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/obtainingmoreauthorization/MissingAuthorityConfiguration.java @@ -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 authz() { + return new FactorAuthorizationManagerFactory(hasAllAuthorities("FACTOR_X509", "FACTOR_AUTHORIZATION_CODE")); + } + // end::authorizationManagerFactoryBean[] + + // tag::authorizationManagerFactory[] + class FactorAuthorizationManagerFactory implements AuthorizationManagerFactory { + private final AuthorizationManager hasAuthorities; + private final DefaultAuthorizationManagerFactory delegate = + new DefaultAuthorizationManagerFactory<>(); + + FactorAuthorizationManagerFactory(AuthorizationManager hasAuthorities) { + this.hasAuthorities = hasAuthorities; + } + + @Override + public AuthorizationManager permitAll() { + return this.delegate.permitAll(); + } + + @Override + public AuthorizationManager denyAll() { + return this.delegate.denyAll(); + } + + @Override + public AuthorizationManager hasRole(String role) { + return hasAnyRole(role); + } + + @Override + public AuthorizationManager hasAnyRole(String... roles) { + return allOf(new AuthorizationDecision(false), this.hasAuthorities, this.delegate.hasAnyRole(roles)); + } + + @Override + public AuthorizationManager hasAllRoles(String... roles) { + return allOf(new AuthorizationDecision(false), this.hasAuthorities, this.delegate.hasAllRoles(roles)); + } + + @Override + public AuthorizationManager hasAuthority(String authority) { + return hasAnyAuthority(authority); + } + + @Override + public AuthorizationManager hasAnyAuthority(String... authorities) { + return allOf(new AuthorizationDecision(false), this.hasAuthorities, this.delegate.hasAnyAuthority(authorities)); + } + + @Override + public AuthorizationManager hasAllAuthorities(String... authorities) { + return allOf(new AuthorizationDecision(false), this.hasAuthorities, this.delegate.hasAllAuthorities(authorities)); + } + + @Override + public AuthorizationManager authenticated() { + return allOf(new AuthorizationDecision(false), this.hasAuthorities, this.delegate.authenticated()); + } + + @Override + public AuthorizationManager fullyAuthenticated() { + return allOf(new AuthorizationDecision(false), this.hasAuthorities, this.delegate.fullyAuthenticated()); + } + + @Override + public AuthorizationManager rememberMe() { + return allOf(new AuthorizationDecision(false), this.hasAuthorities, this.delegate.rememberMe()); + } + + @Override + public AuthorizationManager 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()); + } +} diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/obtainingmoreauthorization/ObtainingMoreAuthorizationTests.java b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/obtainingmoreauthorization/ObtainingMoreAuthorizationTests.java new file mode 100644 index 0000000000..f91fa8b2ed --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/obtainingmoreauthorization/ObtainingMoreAuthorizationTests.java @@ -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"; + } + } +} diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/obtainingmoreauthorization/ScopeConfiguration.java b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/obtainingmoreauthorization/ScopeConfiguration.java new file mode 100644 index 0000000000..ce1e660ed1 --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/obtainingmoreauthorization/ScopeConfiguration.java @@ -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()); + } +} diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/reauthentication/ReauthenticationTests.java b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/reauthentication/ReauthenticationTests.java new file mode 100644 index 0000000000..7078eac0f2 --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/reauthentication/ReauthenticationTests.java @@ -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"; + } + } +} diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/reauthentication/RequireOttConfiguration.java b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/reauthentication/RequireOttConfiguration.java new file mode 100644 index 0000000000..af23bc19f0 --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/reauthentication/RequireOttConfiguration.java @@ -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"); + } +} diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/reauthentication/SimpleConfiguration.java b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/reauthentication/SimpleConfiguration.java new file mode 100644 index 0000000000..7e667e1f60 --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/reauthentication/SimpleConfiguration.java @@ -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"); + } +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/authorizationmanagerfactory/AuthorizationManagerFactoryTests.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/authorizationmanagerfactory/AuthorizationManagerFactoryTests.kt new file mode 100644 index 0000000000..23080f1cea --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/authorizationmanagerfactory/AuthorizationManagerFactoryTests.kt @@ -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" + } + } +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/authorizationmanagerfactory/ListAuthoritiesEverywhereConfiguration.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/authorizationmanagerfactory/ListAuthoritiesEverywhereConfiguration.kt new file mode 100644 index 0000000000..33e7467755 --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/authorizationmanagerfactory/ListAuthoritiesEverywhereConfiguration.kt @@ -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") + } +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/authorizationmanagerfactory/UseAuthorizationManagerFactoryConfiguration.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/authorizationmanagerfactory/UseAuthorizationManagerFactoryConfiguration.kt new file mode 100644 index 0000000000..20f2ca373a --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/authorizationmanagerfactory/UseAuthorizationManagerFactoryConfiguration.kt @@ -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 { + return DefaultAuthorizationManagerFactory.builder() + .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") + } +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/customauthorizationmanagerfactory/CustomAuthorizationManagerFactory.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/customauthorizationmanagerfactory/CustomAuthorizationManagerFactory.kt new file mode 100644 index 0000000000..16f6415d7b --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/customauthorizationmanagerfactory/CustomAuthorizationManagerFactory.kt @@ -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 { + override fun authorize( + authentication: Supplier, context: Object): AuthorizationResult { + val principal = authentication.get().getPrincipal() as MyPrincipal? + if (principal!!.optedIn) { + val root = object : SecurityExpressionRoot(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 { + val defaults = DefaultAuthorizationManagerFactory() + defaults.setAdditionalAuthorization(optIn) + return defaults + } + // end::authorizationManagerFactory[] + + @NullMarked + class MyPrincipal(val user: String, val optedIn: Boolean) : UserDetails { + override fun getAuthorities(): MutableCollection { + 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") + } +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/customauthorizationmanagerfactory/CustomAuthorizationManagerFactoryTests.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/customauthorizationmanagerfactory/CustomAuthorizationManagerFactoryTests.kt new file mode 100644 index 0000000000..b55dae1c90 --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/customauthorizationmanagerfactory/CustomAuthorizationManagerFactoryTests.kt @@ -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" + } + } +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/multifactorauthentication/ListAuthoritiesConfiguration.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/multifactorauthentication/ListAuthoritiesConfiguration.kt new file mode 100644 index 0000000000..79e40e8c73 --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/multifactorauthentication/ListAuthoritiesConfiguration.kt @@ -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") + } +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/multifactorauthentication/MultiFactorAuthenticationTests.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/multifactorauthentication/MultiFactorAuthenticationTests.kt new file mode 100644 index 0000000000..748b6e050a --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/multifactorauthentication/MultiFactorAuthenticationTests.kt @@ -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" + } + } +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/obtainingmoreauthorization/MissingAuthorityConfiguration.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/obtainingmoreauthorization/MissingAuthorityConfiguration.kt new file mode 100644 index 0000000000..1e4d7431c6 --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/obtainingmoreauthorization/MissingAuthorityConfiguration.kt @@ -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 -> 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 { + return FactorAuthorizationManagerFactory(hasAllAuthorities("FACTOR_X509", "FACTOR_AUTHORIZATION_CODE")) + } + // end::authorizationManagerFactoryBean[] + + // tag::authorizationManagerFactory[] + internal inner class FactorAuthorizationManagerFactory(private val hasAuthorities: AuthorizationManager) : + AuthorizationManagerFactory { + private val delegate = DefaultAuthorizationManagerFactory() + + override fun permitAll(): AuthorizationManager { + return this.delegate.permitAll() + } + + override fun denyAll(): AuthorizationManager { + return this.delegate.denyAll() + } + + override fun hasRole(role: String): AuthorizationManager { + return hasAnyRole(role) + } + + override fun hasAnyRole(vararg roles: String): AuthorizationManager { + return addFactors(this.delegate.hasAnyRole(*roles)) + } + + override fun hasAllRoles(vararg roles: String): AuthorizationManager { + return addFactors(this.delegate.hasAllRoles(*roles)) + } + + override fun hasAuthority(authority: String): AuthorizationManager { + return hasAnyAuthority(authority) + } + + override fun hasAnyAuthority(vararg authorities: String): AuthorizationManager { + return addFactors(this.delegate.hasAnyAuthority(*authorities)) + } + + override fun hasAllAuthorities(vararg authorities: String): AuthorizationManager { + return addFactors(this.delegate.hasAllAuthorities(*authorities)) + } + + override fun authenticated(): AuthorizationManager { + return addFactors(this.delegate.authenticated()) + } + + override fun fullyAuthenticated(): AuthorizationManager { + return addFactors(this.delegate.fullyAuthenticated()) + } + + override fun rememberMe(): AuthorizationManager { + return addFactors(this.delegate.rememberMe()) + } + + override fun anonymous(): AuthorizationManager { + return this.delegate.anonymous() + } + + private fun addFactors(delegate: AuthorizationManager): AuthorizationManager { + return allOf(AuthorizationDecision(false), this.hasAuthorities, delegate) + } + } + // end::authorizationManagerFactory[] + + // end::authenticationEntryPoint[] + @Bean + fun clients(): ClientRegistrationRepository { + return InMemoryClientRegistrationRepository(TestClientRegistrations.clientRegistration().build()) + } +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/obtainingmoreauthorization/ObtainingMoreAuthorizationTests.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/obtainingmoreauthorization/ObtainingMoreAuthorizationTests.kt new file mode 100644 index 0000000000..c7cc92478f --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/obtainingmoreauthorization/ObtainingMoreAuthorizationTests.kt @@ -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" + } + } +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/obtainingmoreauthorization/ScopeConfiguration.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/obtainingmoreauthorization/ScopeConfiguration.kt new file mode 100644 index 0000000000..745456d71d --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/obtainingmoreauthorization/ScopeConfiguration.kt @@ -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()) + } +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/reauthentication/ReauthenticationTests.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/reauthentication/ReauthenticationTests.kt new file mode 100644 index 0000000000..1b7278ce15 --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/reauthentication/ReauthenticationTests.kt @@ -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" + } + } +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/reauthentication/RequireOttConfiguration.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/reauthentication/RequireOttConfiguration.kt new file mode 100644 index 0000000000..cca01a6c85 --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/reauthentication/RequireOttConfiguration.kt @@ -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") + } +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/reauthentication/SimpleConfiguration.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/reauthentication/SimpleConfiguration.kt new file mode 100644 index 0000000000..71c2c5a7a7 --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/reauthentication/SimpleConfiguration.kt @@ -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") + } +}