29 changed files with 2224 additions and 0 deletions
@ -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] |
||||||
@ -0,0 +1,114 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2004-present the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.security.docs.servlet.authentication.authorizationmanagerfactory; |
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test; |
||||||
|
import org.junit.jupiter.api.extension.ExtendWith; |
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||||
|
import org.springframework.security.config.test.SpringTestContext; |
||||||
|
import org.springframework.security.config.test.SpringTestContextExtension; |
||||||
|
import org.springframework.security.docs.servlet.authentication.servletx509config.CustomX509Configuration; |
||||||
|
import org.springframework.security.test.context.support.WithMockUser; |
||||||
|
import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; |
||||||
|
import org.springframework.test.context.TestExecutionListeners; |
||||||
|
import org.springframework.test.context.junit.jupiter.SpringExtension; |
||||||
|
import org.springframework.test.web.servlet.MockMvc; |
||||||
|
import org.springframework.web.bind.annotation.GetMapping; |
||||||
|
import org.springframework.web.bind.annotation.RestController; |
||||||
|
|
||||||
|
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated; |
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; |
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; |
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
||||||
|
|
||||||
|
/** |
||||||
|
* Tests {@link CustomX509Configuration}. |
||||||
|
* |
||||||
|
* @author Rob Winch |
||||||
|
*/ |
||||||
|
@ExtendWith({ SpringExtension.class, SpringTestContextExtension.class }) |
||||||
|
@TestExecutionListeners(WithSecurityContextTestExecutionListener.class) |
||||||
|
public class AuthorizationManagerFactoryTests { |
||||||
|
|
||||||
|
public final SpringTestContext spring = new SpringTestContext(this); |
||||||
|
|
||||||
|
@Autowired |
||||||
|
MockMvc mockMvc; |
||||||
|
|
||||||
|
@Test |
||||||
|
@WithMockUser(authorities = { "FACTOR_PASSWORD", "FACTOR_OTT" }) |
||||||
|
void getWhenAuthenticatedWithPasswordAndOttThenPermits() throws Exception { |
||||||
|
this.spring.register(UseAuthorizationManagerFactoryConfiguration.class, Http200Controller.class).autowire(); |
||||||
|
// @formatter:off
|
||||||
|
this.mockMvc.perform(get("/")) |
||||||
|
.andExpect(status().isOk()) |
||||||
|
.andExpect(authenticated().withUsername("user")); |
||||||
|
// @formatter:on
|
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
@WithMockUser(authorities = "FACTOR_PASSWORD") |
||||||
|
void getWhenAuthenticatedWithPasswordThenRedirectsToOtt() throws Exception { |
||||||
|
this.spring.register(UseAuthorizationManagerFactoryConfiguration.class, Http200Controller.class).autowire(); |
||||||
|
// @formatter:off
|
||||||
|
this.mockMvc.perform(get("/")) |
||||||
|
.andExpect(status().is3xxRedirection()) |
||||||
|
.andExpect(redirectedUrl("http://localhost/login?factor=ott")); |
||||||
|
// @formatter:on
|
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
@WithMockUser(authorities = "FACTOR_OTT") |
||||||
|
void getWhenAuthenticatedWithOttThenRedirectsToPassword() throws Exception { |
||||||
|
this.spring.register(UseAuthorizationManagerFactoryConfiguration.class, Http200Controller.class).autowire(); |
||||||
|
// @formatter:off
|
||||||
|
this.mockMvc.perform(get("/")) |
||||||
|
.andExpect(status().is3xxRedirection()) |
||||||
|
.andExpect(redirectedUrl("http://localhost/login?factor=password")); |
||||||
|
// @formatter:on
|
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
@WithMockUser |
||||||
|
void getWhenAuthenticatedThenRedirectsToPassword() throws Exception { |
||||||
|
this.spring.register(UseAuthorizationManagerFactoryConfiguration.class, Http200Controller.class).autowire(); |
||||||
|
// @formatter:off
|
||||||
|
this.mockMvc.perform(get("/")) |
||||||
|
.andExpect(status().is3xxRedirection()) |
||||||
|
.andExpect(redirectedUrl("http://localhost/login?factor=password")); |
||||||
|
// @formatter:on
|
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void getWhenUnauthenticatedThenRedirectsToBoth() throws Exception { |
||||||
|
this.spring.register(UseAuthorizationManagerFactoryConfiguration.class, Http200Controller.class).autowire(); |
||||||
|
// @formatter:off
|
||||||
|
this.mockMvc.perform(get("/")) |
||||||
|
.andExpect(status().is3xxRedirection()) |
||||||
|
.andExpect(redirectedUrl("http://localhost/login")); |
||||||
|
// @formatter:on
|
||||||
|
} |
||||||
|
|
||||||
|
@RestController |
||||||
|
static class Http200Controller { |
||||||
|
@GetMapping("/**") |
||||||
|
String ok() { |
||||||
|
return "ok"; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,54 @@ |
|||||||
|
package org.springframework.security.docs.servlet.authentication.authorizationmanagerfactory; |
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean; |
||||||
|
import org.springframework.context.annotation.Configuration; |
||||||
|
import org.springframework.security.config.Customizer; |
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity; |
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; |
||||||
|
import org.springframework.security.core.userdetails.User; |
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService; |
||||||
|
import org.springframework.security.provisioning.InMemoryUserDetailsManager; |
||||||
|
import org.springframework.security.web.SecurityFilterChain; |
||||||
|
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler; |
||||||
|
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler; |
||||||
|
|
||||||
|
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasAuthority; |
||||||
|
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole; |
||||||
|
import static org.springframework.security.authorization.AuthorizationManagers.allOf; |
||||||
|
|
||||||
|
@EnableWebSecurity |
||||||
|
@Configuration(proxyBeanMethods = false) |
||||||
|
public class ListAuthoritiesEverywhereConfiguration { |
||||||
|
|
||||||
|
// tag::httpSecurity[]
|
||||||
|
@Bean |
||||||
|
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { |
||||||
|
// @formatter:off
|
||||||
|
http |
||||||
|
.authorizeHttpRequests((authorize) -> authorize |
||||||
|
.requestMatchers("/admin/**").access(allOf(hasAuthority("FACTOR_PASSWORD"), hasAuthority("FACTOR_OTT"), hasRole("ADMIN"))) // <1>
|
||||||
|
.anyRequest().access(allOf(hasAuthority("FACTOR_PASSWORD"), hasAuthority("FACTOR_OTT"))) |
||||||
|
) |
||||||
|
.formLogin(Customizer.withDefaults()) |
||||||
|
.oneTimeTokenLogin(Customizer.withDefaults()); |
||||||
|
// @formatter:on
|
||||||
|
return http.build(); |
||||||
|
} |
||||||
|
// end::httpSecurity[]
|
||||||
|
|
||||||
|
@Bean |
||||||
|
UserDetailsService userDetailsService() { |
||||||
|
return new InMemoryUserDetailsManager( |
||||||
|
User.withDefaultPasswordEncoder() |
||||||
|
.username("user") |
||||||
|
.password("password") |
||||||
|
.authorities("app") |
||||||
|
.build() |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
@Bean |
||||||
|
OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler() { |
||||||
|
return new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent"); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,60 @@ |
|||||||
|
package org.springframework.security.docs.servlet.authentication.authorizationmanagerfactory; |
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean; |
||||||
|
import org.springframework.context.annotation.Configuration; |
||||||
|
import org.springframework.security.authorization.AuthorizationManagerFactory; |
||||||
|
import org.springframework.security.authorization.DefaultAuthorizationManagerFactory; |
||||||
|
import org.springframework.security.config.Customizer; |
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity; |
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; |
||||||
|
import org.springframework.security.core.userdetails.User; |
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService; |
||||||
|
import org.springframework.security.provisioning.InMemoryUserDetailsManager; |
||||||
|
import org.springframework.security.web.SecurityFilterChain; |
||||||
|
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler; |
||||||
|
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler; |
||||||
|
|
||||||
|
@EnableWebSecurity |
||||||
|
@Configuration(proxyBeanMethods = false) |
||||||
|
class UseAuthorizationManagerFactoryConfiguration { |
||||||
|
|
||||||
|
// tag::httpSecurity[]
|
||||||
|
@Bean |
||||||
|
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { |
||||||
|
// @formatter:off
|
||||||
|
http |
||||||
|
.authorizeHttpRequests((authorize) -> authorize |
||||||
|
.requestMatchers("/admin/**").hasRole("ADMIN") |
||||||
|
.anyRequest().authenticated() |
||||||
|
) |
||||||
|
.formLogin(Customizer.withDefaults()) |
||||||
|
.oneTimeTokenLogin(Customizer.withDefaults()); |
||||||
|
// @formatter:on
|
||||||
|
return http.build(); |
||||||
|
} |
||||||
|
// end::httpSecurity[]
|
||||||
|
|
||||||
|
// tag::authorizationManagerFactoryBean[]
|
||||||
|
@Bean |
||||||
|
AuthorizationManagerFactory<Object> authz() { |
||||||
|
return DefaultAuthorizationManagerFactory.builder() |
||||||
|
.requireAdditionalAuthorities("FACTOR_PASSWORD", "FACTOR_OTT").build(); |
||||||
|
} |
||||||
|
// end::authorizationManagerFactoryBean[]
|
||||||
|
|
||||||
|
@Bean |
||||||
|
UserDetailsService userDetailsService() { |
||||||
|
return new InMemoryUserDetailsManager( |
||||||
|
User.withDefaultPasswordEncoder() |
||||||
|
.username("user") |
||||||
|
.password("password") |
||||||
|
.authorities("app") |
||||||
|
.build() |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
@Bean |
||||||
|
OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler() { |
||||||
|
return new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent"); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,103 @@ |
|||||||
|
package org.springframework.security.docs.servlet.authentication.customauthorizationmanagerfactory; |
||||||
|
|
||||||
|
import java.util.Collection; |
||||||
|
import java.util.function.Supplier; |
||||||
|
|
||||||
|
import org.jspecify.annotations.NullMarked; |
||||||
|
import org.jspecify.annotations.Nullable; |
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean; |
||||||
|
import org.springframework.context.annotation.Configuration; |
||||||
|
import org.springframework.security.access.expression.SecurityExpressionOperations; |
||||||
|
import org.springframework.security.access.expression.SecurityExpressionRoot; |
||||||
|
import org.springframework.security.authorization.AuthorityAuthorizationDecision; |
||||||
|
import org.springframework.security.authorization.AuthorizationDecision; |
||||||
|
import org.springframework.security.authorization.AuthorizationManager; |
||||||
|
import org.springframework.security.authorization.AuthorizationManagerFactory; |
||||||
|
import org.springframework.security.authorization.AuthorizationResult; |
||||||
|
import org.springframework.security.authorization.DefaultAuthorizationManagerFactory; |
||||||
|
import org.springframework.security.config.Customizer; |
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity; |
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; |
||||||
|
import org.springframework.security.core.Authentication; |
||||||
|
import org.springframework.security.core.GrantedAuthority; |
||||||
|
import org.springframework.security.core.authority.AuthorityUtils; |
||||||
|
import org.springframework.security.core.userdetails.UserDetails; |
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService; |
||||||
|
import org.springframework.security.web.SecurityFilterChain; |
||||||
|
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler; |
||||||
|
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler; |
||||||
|
import org.springframework.stereotype.Component; |
||||||
|
|
||||||
|
@EnableWebSecurity |
||||||
|
@Configuration(proxyBeanMethods = false) |
||||||
|
class CustomAuthorizationManagerFactory { |
||||||
|
// tag::httpSecurity[]
|
||||||
|
@Bean |
||||||
|
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { |
||||||
|
// @formatter:off
|
||||||
|
http |
||||||
|
.authorizeHttpRequests((authorize) -> authorize |
||||||
|
.requestMatchers("/admin/**").hasRole("ADMIN") |
||||||
|
.anyRequest().authenticated() |
||||||
|
) |
||||||
|
.formLogin(Customizer.withDefaults()) |
||||||
|
.oneTimeTokenLogin(Customizer.withDefaults()); |
||||||
|
// @formatter:on
|
||||||
|
return http.build(); |
||||||
|
} |
||||||
|
// end::httpSecurity[]
|
||||||
|
|
||||||
|
// tag::authorizationManager[]
|
||||||
|
@Component |
||||||
|
class OptInToMfaAuthorizationManager implements AuthorizationManager<Object> { |
||||||
|
@Override |
||||||
|
public AuthorizationResult authorize(Supplier<? extends @Nullable Authentication> authentication, Object context) { |
||||||
|
MyPrincipal principal = (MyPrincipal) authentication.get().getPrincipal(); |
||||||
|
if (principal.optedIn()) { |
||||||
|
SecurityExpressionOperations sec = new SecurityExpressionRoot<>(authentication, context) {}; |
||||||
|
return new AuthorityAuthorizationDecision(sec.hasAuthority("FACTOR_OTT"), |
||||||
|
AuthorityUtils.createAuthorityList("FACTOR_OTT")); |
||||||
|
} |
||||||
|
return new AuthorizationDecision(true); |
||||||
|
} |
||||||
|
} |
||||||
|
// end::authorizationManager[]
|
||||||
|
|
||||||
|
// tag::authorizationManagerFactory[]
|
||||||
|
@Bean |
||||||
|
AuthorizationManagerFactory<Object> authorizationManagerFactory(OptInToMfaAuthorizationManager optIn) { |
||||||
|
DefaultAuthorizationManagerFactory<Object> defaults = new DefaultAuthorizationManagerFactory<>(); |
||||||
|
defaults.setAdditionalAuthorization(optIn); |
||||||
|
return defaults; |
||||||
|
} |
||||||
|
// end::authorizationManagerFactory[]
|
||||||
|
|
||||||
|
@NullMarked |
||||||
|
record MyPrincipal(String username, boolean optedIn) implements UserDetails { |
||||||
|
@Override |
||||||
|
public Collection<? extends GrantedAuthority> getAuthorities() { |
||||||
|
return AuthorityUtils.createAuthorityList("app"); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public @Nullable String getPassword() { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String getUsername() { |
||||||
|
return this.username; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Bean |
||||||
|
UserDetailsService users() { |
||||||
|
return (username) -> new MyPrincipal(username, username.equals("optedin")); |
||||||
|
} |
||||||
|
|
||||||
|
@Bean |
||||||
|
OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler() { |
||||||
|
return new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent"); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,97 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2004-present the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.security.docs.servlet.authentication.customauthorizationmanagerfactory; |
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test; |
||||||
|
import org.junit.jupiter.api.extension.ExtendWith; |
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||||
|
import org.springframework.security.authentication.TestingAuthenticationToken; |
||||||
|
import org.springframework.security.config.test.SpringTestContext; |
||||||
|
import org.springframework.security.config.test.SpringTestContextExtension; |
||||||
|
import org.springframework.security.core.userdetails.UserDetails; |
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService; |
||||||
|
import org.springframework.security.docs.servlet.authentication.servletx509config.CustomX509Configuration; |
||||||
|
import org.springframework.test.web.servlet.MockMvc; |
||||||
|
import org.springframework.web.bind.annotation.GetMapping; |
||||||
|
import org.springframework.web.bind.annotation.RestController; |
||||||
|
|
||||||
|
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; |
||||||
|
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; |
||||||
|
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated; |
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; |
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; |
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
||||||
|
|
||||||
|
/** |
||||||
|
* Tests {@link CustomX509Configuration}. |
||||||
|
* |
||||||
|
* @author Rob Winch |
||||||
|
*/ |
||||||
|
@ExtendWith(SpringTestContextExtension.class) |
||||||
|
public class CustomAuthorizationManagerFactoryTests { |
||||||
|
|
||||||
|
public final SpringTestContext spring = new SpringTestContext(this); |
||||||
|
|
||||||
|
@Autowired |
||||||
|
MockMvc mockMvc; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
UserDetailsService users; |
||||||
|
|
||||||
|
@Test |
||||||
|
void getWhenOptedInThenRedirectsToOtt() throws Exception { |
||||||
|
this.spring.register(CustomAuthorizationManagerFactory.class, Http200Controller.class).autowire(); |
||||||
|
UserDetails user = this.users.loadUserByUsername("optedin"); |
||||||
|
// @formatter:off
|
||||||
|
this.mockMvc.perform(get("/").with(user(user))) |
||||||
|
.andExpect(status().is3xxRedirection()) |
||||||
|
.andExpect(redirectedUrl("http://localhost/login?factor=ott")); |
||||||
|
// @formatter:on
|
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void getWhenNotOptedInThenAllows() throws Exception { |
||||||
|
this.spring.register(CustomAuthorizationManagerFactory.class, Http200Controller.class).autowire(); |
||||||
|
UserDetails user = this.users.loadUserByUsername("user"); |
||||||
|
// @formatter:off
|
||||||
|
this.mockMvc.perform(get("/").with(user(user))) |
||||||
|
.andExpect(status().isOk()) |
||||||
|
.andExpect(authenticated().withUsername("user")); |
||||||
|
// @formatter:on
|
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void getWhenOptedAndHasFactorThenAllows() throws Exception { |
||||||
|
this.spring.register(CustomAuthorizationManagerFactory.class, Http200Controller.class).autowire(); |
||||||
|
UserDetails user = this.users.loadUserByUsername("optedin"); |
||||||
|
TestingAuthenticationToken token = new TestingAuthenticationToken(user, "", "FACTOR_OTT"); |
||||||
|
// @formatter:off
|
||||||
|
this.mockMvc.perform(get("/").with(authentication(token))) |
||||||
|
.andExpect(status().isOk()) |
||||||
|
.andExpect(authenticated().withUsername("optedin")); |
||||||
|
// @formatter:on
|
||||||
|
} |
||||||
|
|
||||||
|
@RestController |
||||||
|
static class Http200Controller { |
||||||
|
@GetMapping("/**") |
||||||
|
String ok() { |
||||||
|
return "ok"; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,52 @@ |
|||||||
|
package org.springframework.security.docs.servlet.authentication.multifactorauthentication; |
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean; |
||||||
|
import org.springframework.context.annotation.Configuration; |
||||||
|
import org.springframework.security.config.Customizer; |
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity; |
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; |
||||||
|
import org.springframework.security.core.userdetails.User; |
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService; |
||||||
|
import org.springframework.security.provisioning.InMemoryUserDetailsManager; |
||||||
|
import org.springframework.security.web.SecurityFilterChain; |
||||||
|
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler; |
||||||
|
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler; |
||||||
|
|
||||||
|
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasAuthority; |
||||||
|
import static org.springframework.security.authorization.AuthorizationManagers.allOf; |
||||||
|
|
||||||
|
@EnableWebSecurity |
||||||
|
@Configuration(proxyBeanMethods = false) |
||||||
|
class ListAuthoritiesConfiguration { |
||||||
|
|
||||||
|
// tag::httpSecurity[]
|
||||||
|
@Bean |
||||||
|
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { |
||||||
|
// @formatter:off
|
||||||
|
http |
||||||
|
.authorizeHttpRequests((authorize) -> authorize |
||||||
|
.anyRequest().access(allOf(hasAuthority("FACTOR_PASSWORD"), hasAuthority("FACTOR_OTT"))) // <1>
|
||||||
|
) |
||||||
|
.formLogin(Customizer.withDefaults()) |
||||||
|
.oneTimeTokenLogin(Customizer.withDefaults()); |
||||||
|
// @formatter:on
|
||||||
|
return http.build(); |
||||||
|
} |
||||||
|
// end::httpSecurity[]
|
||||||
|
|
||||||
|
@Bean |
||||||
|
UserDetailsService userDetailsService() { |
||||||
|
return new InMemoryUserDetailsManager( |
||||||
|
User.withDefaultPasswordEncoder() |
||||||
|
.username("user") |
||||||
|
.password("password") |
||||||
|
.authorities("app") |
||||||
|
.build() |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
@Bean |
||||||
|
OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler() { |
||||||
|
return new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent"); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,114 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2004-present the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.security.docs.servlet.authentication.multifactorauthentication; |
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test; |
||||||
|
import org.junit.jupiter.api.extension.ExtendWith; |
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||||
|
import org.springframework.security.config.test.SpringTestContext; |
||||||
|
import org.springframework.security.config.test.SpringTestContextExtension; |
||||||
|
import org.springframework.security.docs.servlet.authentication.servletx509config.CustomX509Configuration; |
||||||
|
import org.springframework.security.test.context.support.WithMockUser; |
||||||
|
import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; |
||||||
|
import org.springframework.test.context.TestExecutionListeners; |
||||||
|
import org.springframework.test.context.junit.jupiter.SpringExtension; |
||||||
|
import org.springframework.test.web.servlet.MockMvc; |
||||||
|
import org.springframework.web.bind.annotation.GetMapping; |
||||||
|
import org.springframework.web.bind.annotation.RestController; |
||||||
|
|
||||||
|
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated; |
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; |
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; |
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
||||||
|
|
||||||
|
/** |
||||||
|
* Tests {@link CustomX509Configuration}. |
||||||
|
* |
||||||
|
* @author Rob Winch |
||||||
|
*/ |
||||||
|
@ExtendWith({ SpringExtension.class, SpringTestContextExtension.class }) |
||||||
|
@TestExecutionListeners(WithSecurityContextTestExecutionListener.class) |
||||||
|
public class MultiFactorAuthenticationTests { |
||||||
|
|
||||||
|
public final SpringTestContext spring = new SpringTestContext(this); |
||||||
|
|
||||||
|
@Autowired |
||||||
|
MockMvc mockMvc; |
||||||
|
|
||||||
|
@Test |
||||||
|
@WithMockUser(authorities = { "FACTOR_PASSWORD", "FACTOR_OTT" }) |
||||||
|
void getWhenAuthenticatedWithPasswordAndOttThenPermits() throws Exception { |
||||||
|
this.spring.register(ListAuthoritiesConfiguration.class, Http200Controller.class).autowire(); |
||||||
|
// @formatter:off
|
||||||
|
this.mockMvc.perform(get("/")) |
||||||
|
.andExpect(status().isOk()) |
||||||
|
.andExpect(authenticated().withUsername("user")); |
||||||
|
// @formatter:on
|
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
@WithMockUser(authorities = "FACTOR_PASSWORD") |
||||||
|
void getWhenAuthenticatedWithPasswordThenRedirectsToOtt() throws Exception { |
||||||
|
this.spring.register(ListAuthoritiesConfiguration.class, Http200Controller.class).autowire(); |
||||||
|
// @formatter:off
|
||||||
|
this.mockMvc.perform(get("/")) |
||||||
|
.andExpect(status().is3xxRedirection()) |
||||||
|
.andExpect(redirectedUrl("http://localhost/login?factor=ott")); |
||||||
|
// @formatter:on
|
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
@WithMockUser(authorities = "FACTOR_OTT") |
||||||
|
void getWhenAuthenticatedWithOttThenRedirectsToPassword() throws Exception { |
||||||
|
this.spring.register(ListAuthoritiesConfiguration.class, Http200Controller.class).autowire(); |
||||||
|
// @formatter:off
|
||||||
|
this.mockMvc.perform(get("/")) |
||||||
|
.andExpect(status().is3xxRedirection()) |
||||||
|
.andExpect(redirectedUrl("http://localhost/login?factor=password")); |
||||||
|
// @formatter:on
|
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
@WithMockUser |
||||||
|
void getWhenAuthenticatedThenRedirectsToPassword() throws Exception { |
||||||
|
this.spring.register(ListAuthoritiesConfiguration.class, Http200Controller.class).autowire(); |
||||||
|
// @formatter:off
|
||||||
|
this.mockMvc.perform(get("/")) |
||||||
|
.andExpect(status().is3xxRedirection()) |
||||||
|
.andExpect(redirectedUrl("http://localhost/login?factor=password")); |
||||||
|
// @formatter:on
|
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void getWhenUnauthenticatedThenRedirectsToBoth() throws Exception { |
||||||
|
this.spring.register(ListAuthoritiesConfiguration.class, Http200Controller.class).autowire(); |
||||||
|
// @formatter:off
|
||||||
|
this.mockMvc.perform(get("/")) |
||||||
|
.andExpect(status().is3xxRedirection()) |
||||||
|
.andExpect(redirectedUrl("http://localhost/login")); |
||||||
|
// @formatter:on
|
||||||
|
} |
||||||
|
|
||||||
|
@RestController |
||||||
|
static class Http200Controller { |
||||||
|
@GetMapping("/**") |
||||||
|
String ok() { |
||||||
|
return "ok"; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,147 @@ |
|||||||
|
package org.springframework.security.docs.servlet.authentication.obtainingmoreauthorization; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
|
||||||
|
import jakarta.servlet.ServletException; |
||||||
|
import jakarta.servlet.http.HttpServletRequest; |
||||||
|
import jakarta.servlet.http.HttpServletResponse; |
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean; |
||||||
|
import org.springframework.context.annotation.Configuration; |
||||||
|
import org.springframework.security.authorization.AuthorizationDecision; |
||||||
|
import org.springframework.security.authorization.AuthorizationManager; |
||||||
|
import org.springframework.security.authorization.AuthorizationManagerFactory; |
||||||
|
import org.springframework.security.authorization.DefaultAuthorizationManagerFactory; |
||||||
|
import org.springframework.security.config.Customizer; |
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity; |
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; |
||||||
|
import org.springframework.security.core.AuthenticationException; |
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; |
||||||
|
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; |
||||||
|
import org.springframework.security.oauth2.client.registration.TestClientRegistrations; |
||||||
|
import org.springframework.security.web.AuthenticationEntryPoint; |
||||||
|
import org.springframework.security.web.SecurityFilterChain; |
||||||
|
import org.springframework.security.web.access.intercept.RequestAuthorizationContext; |
||||||
|
import org.springframework.stereotype.Component; |
||||||
|
|
||||||
|
import static org.springframework.security.authorization.AllAuthoritiesAuthorizationManager.hasAllAuthorities; |
||||||
|
import static org.springframework.security.authorization.AuthorizationManagers.allOf; |
||||||
|
|
||||||
|
@EnableWebSecurity |
||||||
|
@Configuration(proxyBeanMethods = false) |
||||||
|
class MissingAuthorityConfiguration { |
||||||
|
|
||||||
|
// tag::httpSecurity[]
|
||||||
|
@Bean |
||||||
|
SecurityFilterChain securityFilterChain(HttpSecurity http, ScopeRetrievingAuthenticationEntryPoint oauth2) throws Exception { |
||||||
|
// @formatter:off
|
||||||
|
http |
||||||
|
.authorizeHttpRequests((authorize) -> authorize |
||||||
|
.requestMatchers("/profile/**").hasAuthority("SCOPE_profile:read") |
||||||
|
.anyRequest().authenticated() |
||||||
|
) |
||||||
|
.x509(Customizer.withDefaults()) |
||||||
|
.oauth2Login(Customizer.withDefaults()) |
||||||
|
.exceptionHandling((exceptions) -> exceptions |
||||||
|
.defaultDeniedHandlerForMissingAuthority(oauth2, "SCOPE_profile:read") |
||||||
|
); |
||||||
|
// @formatter:on
|
||||||
|
return http.build(); |
||||||
|
} |
||||||
|
// end::httpSecurity[]
|
||||||
|
|
||||||
|
// tag::authorizationManagerFactoryBean[]
|
||||||
|
@Bean |
||||||
|
AuthorizationManagerFactory<RequestAuthorizationContext> authz() { |
||||||
|
return new FactorAuthorizationManagerFactory(hasAllAuthorities("FACTOR_X509", "FACTOR_AUTHORIZATION_CODE")); |
||||||
|
} |
||||||
|
// end::authorizationManagerFactoryBean[]
|
||||||
|
|
||||||
|
// tag::authorizationManagerFactory[]
|
||||||
|
class FactorAuthorizationManagerFactory implements AuthorizationManagerFactory<RequestAuthorizationContext> { |
||||||
|
private final AuthorizationManager<RequestAuthorizationContext> hasAuthorities; |
||||||
|
private final DefaultAuthorizationManagerFactory<RequestAuthorizationContext> delegate = |
||||||
|
new DefaultAuthorizationManagerFactory<>(); |
||||||
|
|
||||||
|
FactorAuthorizationManagerFactory(AuthorizationManager<RequestAuthorizationContext> hasAuthorities) { |
||||||
|
this.hasAuthorities = hasAuthorities; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public AuthorizationManager<RequestAuthorizationContext> permitAll() { |
||||||
|
return this.delegate.permitAll(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public AuthorizationManager<RequestAuthorizationContext> denyAll() { |
||||||
|
return this.delegate.denyAll(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public AuthorizationManager<RequestAuthorizationContext> hasRole(String role) { |
||||||
|
return hasAnyRole(role); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public AuthorizationManager<RequestAuthorizationContext> hasAnyRole(String... roles) { |
||||||
|
return allOf(new AuthorizationDecision(false), this.hasAuthorities, this.delegate.hasAnyRole(roles)); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public AuthorizationManager<RequestAuthorizationContext> hasAllRoles(String... roles) { |
||||||
|
return allOf(new AuthorizationDecision(false), this.hasAuthorities, this.delegate.hasAllRoles(roles)); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public AuthorizationManager<RequestAuthorizationContext> hasAuthority(String authority) { |
||||||
|
return hasAnyAuthority(authority); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public AuthorizationManager<RequestAuthorizationContext> hasAnyAuthority(String... authorities) { |
||||||
|
return allOf(new AuthorizationDecision(false), this.hasAuthorities, this.delegate.hasAnyAuthority(authorities)); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public AuthorizationManager<RequestAuthorizationContext> hasAllAuthorities(String... authorities) { |
||||||
|
return allOf(new AuthorizationDecision(false), this.hasAuthorities, this.delegate.hasAllAuthorities(authorities)); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public AuthorizationManager<RequestAuthorizationContext> authenticated() { |
||||||
|
return allOf(new AuthorizationDecision(false), this.hasAuthorities, this.delegate.authenticated()); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public AuthorizationManager<RequestAuthorizationContext> fullyAuthenticated() { |
||||||
|
return allOf(new AuthorizationDecision(false), this.hasAuthorities, this.delegate.fullyAuthenticated()); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public AuthorizationManager<RequestAuthorizationContext> rememberMe() { |
||||||
|
return allOf(new AuthorizationDecision(false), this.hasAuthorities, this.delegate.rememberMe()); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public AuthorizationManager<RequestAuthorizationContext> anonymous() { |
||||||
|
return this.delegate.anonymous(); |
||||||
|
} |
||||||
|
} |
||||||
|
// end::authorizationManagerFactory[]
|
||||||
|
|
||||||
|
// tag::authenticationEntryPoint[]
|
||||||
|
@Component |
||||||
|
class ScopeRetrievingAuthenticationEntryPoint implements AuthenticationEntryPoint { |
||||||
|
@Override |
||||||
|
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) |
||||||
|
throws IOException, ServletException { |
||||||
|
response.sendRedirect("https://authz.example.org/authorize?scope=profile:read"); |
||||||
|
} |
||||||
|
} |
||||||
|
// end::authenticationEntryPoint[]
|
||||||
|
|
||||||
|
@Bean |
||||||
|
ClientRegistrationRepository clients() { |
||||||
|
return new InMemoryClientRegistrationRepository(TestClientRegistrations.clientRegistration().build()); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,102 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2004-present the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.security.docs.servlet.authentication.obtainingmoreauthorization; |
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test; |
||||||
|
import org.junit.jupiter.api.extension.ExtendWith; |
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||||
|
import org.springframework.security.config.test.SpringTestContext; |
||||||
|
import org.springframework.security.config.test.SpringTestContextExtension; |
||||||
|
import org.springframework.security.docs.servlet.authentication.servletx509config.CustomX509Configuration; |
||||||
|
import org.springframework.security.test.context.support.WithMockUser; |
||||||
|
import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; |
||||||
|
import org.springframework.test.context.TestExecutionListeners; |
||||||
|
import org.springframework.test.context.junit.jupiter.SpringExtension; |
||||||
|
import org.springframework.test.web.servlet.MockMvc; |
||||||
|
import org.springframework.web.bind.annotation.GetMapping; |
||||||
|
import org.springframework.web.bind.annotation.RestController; |
||||||
|
|
||||||
|
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated; |
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; |
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; |
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
||||||
|
|
||||||
|
/** |
||||||
|
* Tests {@link CustomX509Configuration}. |
||||||
|
* |
||||||
|
* @author Rob Winch |
||||||
|
*/ |
||||||
|
@ExtendWith({ SpringExtension.class, SpringTestContextExtension.class }) |
||||||
|
@TestExecutionListeners(WithSecurityContextTestExecutionListener.class) |
||||||
|
public class ObtainingMoreAuthorizationTests { |
||||||
|
|
||||||
|
public final SpringTestContext spring = new SpringTestContext(this); |
||||||
|
|
||||||
|
@Autowired |
||||||
|
MockMvc mockMvc; |
||||||
|
|
||||||
|
@Test |
||||||
|
@WithMockUser |
||||||
|
void profileWhenScopeConfigurationThenDenies() throws Exception { |
||||||
|
this.spring.register(ScopeConfiguration.class, Http200Controller.class).autowire(); |
||||||
|
// @formatter:off
|
||||||
|
this.mockMvc.perform(get("/profile")) |
||||||
|
.andExpect(status().isForbidden()); |
||||||
|
// @formatter:on
|
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
@WithMockUser(authorities = { "FACTOR_X509", "FACTOR_AUTHORIZATION_CODE" }) |
||||||
|
void profileWhenMissingAuthorityConfigurationThenRedirectsToAuthorizationServer() throws Exception { |
||||||
|
this.spring.register(MissingAuthorityConfiguration.class, Http200Controller.class).autowire(); |
||||||
|
// @formatter:off
|
||||||
|
this.mockMvc.perform(get("/profile")) |
||||||
|
.andExpect(status().is3xxRedirection()) |
||||||
|
.andExpect(redirectedUrl("https://authz.example.org/authorize?scope=profile:read")); |
||||||
|
// @formatter:on
|
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
@WithMockUser(authorities = { "SCOPE_profile:read" }) |
||||||
|
void profileWhenMissingX509WithOttThenForbidden() throws Exception { |
||||||
|
this.spring.register(MissingAuthorityConfiguration.class, Http200Controller.class).autowire(); |
||||||
|
// @formatter:off
|
||||||
|
this.mockMvc.perform(get("/profile")) |
||||||
|
.andExpect(status().isForbidden()); |
||||||
|
// @formatter:on
|
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
@WithMockUser(authorities = { "FACTOR_X509", "FACTOR_AUTHORIZATION_CODE", "SCOPE_profile:read" }) |
||||||
|
void profileWhenAuthenticatedAndHasScopeThenPermits() throws Exception { |
||||||
|
this.spring.register(MissingAuthorityConfiguration.class, Http200Controller.class).autowire(); |
||||||
|
// @formatter:off
|
||||||
|
this.mockMvc.perform(get("/profile")) |
||||||
|
.andExpect(status().isOk()) |
||||||
|
.andExpect(authenticated().withUsername("user")); |
||||||
|
// @formatter:on
|
||||||
|
} |
||||||
|
|
||||||
|
@RestController |
||||||
|
static class Http200Controller { |
||||||
|
@GetMapping("/**") |
||||||
|
String ok() { |
||||||
|
return "ok"; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,37 @@ |
|||||||
|
package org.springframework.security.docs.servlet.authentication.obtainingmoreauthorization; |
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean; |
||||||
|
import org.springframework.context.annotation.Configuration; |
||||||
|
import org.springframework.security.config.Customizer; |
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity; |
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; |
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; |
||||||
|
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; |
||||||
|
import org.springframework.security.oauth2.client.registration.TestClientRegistrations; |
||||||
|
import org.springframework.security.web.SecurityFilterChain; |
||||||
|
|
||||||
|
@EnableWebSecurity |
||||||
|
@Configuration(proxyBeanMethods = false) |
||||||
|
public class ScopeConfiguration { |
||||||
|
|
||||||
|
// tag::httpSecurity[]
|
||||||
|
@Bean |
||||||
|
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { |
||||||
|
// @formatter:off
|
||||||
|
http |
||||||
|
.authorizeHttpRequests((authorize) -> authorize |
||||||
|
.requestMatchers("/profile/**").hasAuthority("SCOPE_profile:read") |
||||||
|
.anyRequest().authenticated() |
||||||
|
) |
||||||
|
.x509(Customizer.withDefaults()) |
||||||
|
.oauth2Login(Customizer.withDefaults()); |
||||||
|
// @formatter:on
|
||||||
|
return http.build(); |
||||||
|
} |
||||||
|
// end::httpSecurity[]
|
||||||
|
|
||||||
|
@Bean |
||||||
|
ClientRegistrationRepository clients() { |
||||||
|
return new InMemoryClientRegistrationRepository(TestClientRegistrations.clientRegistration().build()); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,93 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2004-present the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.security.docs.servlet.authentication.reauthentication; |
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test; |
||||||
|
import org.junit.jupiter.api.extension.ExtendWith; |
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||||
|
import org.springframework.security.config.test.SpringTestContext; |
||||||
|
import org.springframework.security.config.test.SpringTestContextExtension; |
||||||
|
import org.springframework.security.docs.servlet.authentication.servletx509config.CustomX509Configuration; |
||||||
|
import org.springframework.security.test.context.support.WithMockUser; |
||||||
|
import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; |
||||||
|
import org.springframework.test.context.TestExecutionListeners; |
||||||
|
import org.springframework.test.context.junit.jupiter.SpringExtension; |
||||||
|
import org.springframework.test.web.servlet.MockMvc; |
||||||
|
import org.springframework.web.bind.annotation.GetMapping; |
||||||
|
import org.springframework.web.bind.annotation.RestController; |
||||||
|
|
||||||
|
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated; |
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; |
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; |
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
||||||
|
|
||||||
|
/** |
||||||
|
* Tests {@link CustomX509Configuration}. |
||||||
|
* |
||||||
|
* @author Rob Winch |
||||||
|
*/ |
||||||
|
@ExtendWith({ SpringExtension.class, SpringTestContextExtension.class }) |
||||||
|
@TestExecutionListeners(WithSecurityContextTestExecutionListener.class) |
||||||
|
public class ReauthenticationTests { |
||||||
|
|
||||||
|
public final SpringTestContext spring = new SpringTestContext(this); |
||||||
|
|
||||||
|
@Autowired |
||||||
|
MockMvc mockMvc; |
||||||
|
|
||||||
|
@Test |
||||||
|
@WithMockUser |
||||||
|
void formLoginWhenSimpleConfigurationThenPermits() throws Exception { |
||||||
|
this.spring.register(SimpleConfiguration.class, Http200Controller.class).autowire(); |
||||||
|
// @formatter:off
|
||||||
|
this.mockMvc.perform(get("/")) |
||||||
|
.andExpect(status().isOk()) |
||||||
|
.andExpect(authenticated().withUsername("user")); |
||||||
|
// @formatter:on
|
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
@WithMockUser |
||||||
|
void formLoginWhenRequireOttConfigurationThenRedirectsToOtt() throws Exception { |
||||||
|
this.spring.register(RequireOttConfiguration.class, Http200Controller.class).autowire(); |
||||||
|
// @formatter:off
|
||||||
|
this.mockMvc.perform(get("/profile")) |
||||||
|
.andExpect(status().is3xxRedirection()) |
||||||
|
.andExpect(redirectedUrl("http://localhost/login?factor=ott")); |
||||||
|
// @formatter:on
|
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
@WithMockUser(authorities = "FACTOR_OTT") |
||||||
|
void ottWhenRequireOttConfigurationThenAllows() throws Exception { |
||||||
|
this.spring.register(RequireOttConfiguration.class, Http200Controller.class).autowire(); |
||||||
|
// @formatter:off
|
||||||
|
this.mockMvc.perform(get("/profile")) |
||||||
|
.andExpect(status().isOk()) |
||||||
|
.andExpect(authenticated().withUsername("user")); |
||||||
|
// @formatter:on
|
||||||
|
} |
||||||
|
|
||||||
|
@RestController |
||||||
|
static class Http200Controller { |
||||||
|
@GetMapping("/**") |
||||||
|
String ok() { |
||||||
|
return "ok"; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,50 @@ |
|||||||
|
package org.springframework.security.docs.servlet.authentication.reauthentication; |
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean; |
||||||
|
import org.springframework.context.annotation.Configuration; |
||||||
|
import org.springframework.security.config.Customizer; |
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity; |
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; |
||||||
|
import org.springframework.security.core.userdetails.User; |
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService; |
||||||
|
import org.springframework.security.provisioning.InMemoryUserDetailsManager; |
||||||
|
import org.springframework.security.web.SecurityFilterChain; |
||||||
|
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler; |
||||||
|
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler; |
||||||
|
|
||||||
|
@EnableWebSecurity |
||||||
|
@Configuration(proxyBeanMethods = false) |
||||||
|
public class RequireOttConfiguration { |
||||||
|
|
||||||
|
// tag::httpSecurity[]
|
||||||
|
@Bean |
||||||
|
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { |
||||||
|
// @formatter:off
|
||||||
|
http |
||||||
|
.authorizeHttpRequests((authorize) -> authorize |
||||||
|
.requestMatchers("/profile/**").hasAuthority("FACTOR_OTT") // <1>
|
||||||
|
.anyRequest().authenticated() |
||||||
|
) |
||||||
|
.formLogin(Customizer.withDefaults()) |
||||||
|
.oneTimeTokenLogin(Customizer.withDefaults()); |
||||||
|
// @formatter:on
|
||||||
|
return http.build(); |
||||||
|
} |
||||||
|
// end::httpSecurity[]
|
||||||
|
|
||||||
|
@Bean |
||||||
|
UserDetailsService userDetailsService() { |
||||||
|
return new InMemoryUserDetailsManager( |
||||||
|
User.withDefaultPasswordEncoder() |
||||||
|
.username("user") |
||||||
|
.password("password") |
||||||
|
.authorities("app") |
||||||
|
.build() |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
@Bean |
||||||
|
OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler() { |
||||||
|
return new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent"); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,46 @@ |
|||||||
|
package org.springframework.security.docs.servlet.authentication.reauthentication; |
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean; |
||||||
|
import org.springframework.context.annotation.Configuration; |
||||||
|
import org.springframework.security.config.Customizer; |
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity; |
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; |
||||||
|
import org.springframework.security.core.userdetails.User; |
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService; |
||||||
|
import org.springframework.security.provisioning.InMemoryUserDetailsManager; |
||||||
|
import org.springframework.security.web.SecurityFilterChain; |
||||||
|
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler; |
||||||
|
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler; |
||||||
|
|
||||||
|
@EnableWebSecurity |
||||||
|
@Configuration(proxyBeanMethods = false) |
||||||
|
public class SimpleConfiguration { |
||||||
|
// tag::httpSecurity[]
|
||||||
|
@Bean |
||||||
|
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { |
||||||
|
// @formatter:off
|
||||||
|
http |
||||||
|
.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated()) |
||||||
|
.formLogin(Customizer.withDefaults()) |
||||||
|
.oneTimeTokenLogin(Customizer.withDefaults()); |
||||||
|
// @formatter:on
|
||||||
|
return http.build(); |
||||||
|
} |
||||||
|
// end::httpSecurity[]
|
||||||
|
|
||||||
|
@Bean |
||||||
|
UserDetailsService userDetailsService() { |
||||||
|
return new InMemoryUserDetailsManager( |
||||||
|
User.withDefaultPasswordEncoder() |
||||||
|
.username("user") |
||||||
|
.password("password") |
||||||
|
.authorities("app") |
||||||
|
.build() |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
@Bean |
||||||
|
OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler() { |
||||||
|
return new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent"); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,119 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2004-present the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.security.kt.docs.servlet.authentication.authorizationmanagerfactory |
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test |
||||||
|
import org.junit.jupiter.api.extension.ExtendWith |
||||||
|
import org.springframework.beans.factory.annotation.Autowired |
||||||
|
import org.springframework.security.config.test.SpringTestContext |
||||||
|
import org.springframework.security.config.test.SpringTestContextExtension |
||||||
|
import org.springframework.security.test.context.support.WithMockUser |
||||||
|
import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener |
||||||
|
import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers |
||||||
|
import org.springframework.test.context.TestExecutionListeners |
||||||
|
import org.springframework.test.context.junit.jupiter.SpringExtension |
||||||
|
import org.springframework.test.web.servlet.MockMvc |
||||||
|
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders |
||||||
|
import org.springframework.test.web.servlet.result.MockMvcResultMatchers |
||||||
|
import org.springframework.web.bind.annotation.GetMapping |
||||||
|
import org.springframework.web.bind.annotation.RestController |
||||||
|
|
||||||
|
/** |
||||||
|
* Tests [CustomX509Configuration]. |
||||||
|
* |
||||||
|
* @author Rob Winch |
||||||
|
*/ |
||||||
|
@ExtendWith(SpringExtension::class, SpringTestContextExtension::class) |
||||||
|
@TestExecutionListeners(WithSecurityContextTestExecutionListener::class) |
||||||
|
class AuthorizationManagerFactoryTests { |
||||||
|
@JvmField |
||||||
|
val spring: SpringTestContext = SpringTestContext(this) |
||||||
|
|
||||||
|
@Autowired |
||||||
|
var mockMvc: MockMvc? = null |
||||||
|
|
||||||
|
@Test |
||||||
|
@WithMockUser(authorities = ["FACTOR_PASSWORD", "FACTOR_OTT"]) |
||||||
|
@Throws(Exception::class) |
||||||
|
fun getWhenAuthenticatedWithPasswordAndOttThenPermits() { |
||||||
|
this.spring.register(UseAuthorizationManagerFactoryConfiguration::class.java, Http200Controller::class.java) |
||||||
|
.autowire() |
||||||
|
// @formatter:off |
||||||
|
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/")) |
||||||
|
.andExpect(MockMvcResultMatchers.status().isOk()) |
||||||
|
.andExpect(SecurityMockMvcResultMatchers.authenticated().withUsername("user")) |
||||||
|
// @formatter:on |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
@WithMockUser(authorities = ["FACTOR_PASSWORD"]) |
||||||
|
@Throws(Exception::class) |
||||||
|
fun getWhenAuthenticatedWithPasswordThenRedirectsToOtt() { |
||||||
|
this.spring.register(UseAuthorizationManagerFactoryConfiguration::class.java, Http200Controller::class.java) |
||||||
|
.autowire() |
||||||
|
// @formatter:off |
||||||
|
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/")) |
||||||
|
.andExpect(MockMvcResultMatchers.status().is3xxRedirection()) |
||||||
|
.andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor=ott")) |
||||||
|
// @formatter:on |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
@WithMockUser(authorities = ["FACTOR_OTT"]) |
||||||
|
@Throws(Exception::class) |
||||||
|
fun getWhenAuthenticatedWithOttThenRedirectsToPassword() { |
||||||
|
this.spring.register(UseAuthorizationManagerFactoryConfiguration::class.java, Http200Controller::class.java) |
||||||
|
.autowire() |
||||||
|
// @formatter:off |
||||||
|
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/")) |
||||||
|
.andExpect(MockMvcResultMatchers.status().is3xxRedirection()) |
||||||
|
.andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor=password")) |
||||||
|
// @formatter:on |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
@WithMockUser |
||||||
|
@Throws(Exception::class) |
||||||
|
fun getWhenAuthenticatedThenRedirectsToPassword() { |
||||||
|
this.spring.register(UseAuthorizationManagerFactoryConfiguration::class.java, Http200Controller::class.java) |
||||||
|
.autowire() |
||||||
|
// @formatter:off |
||||||
|
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/")) |
||||||
|
.andExpect(MockMvcResultMatchers.status().is3xxRedirection()) |
||||||
|
.andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor=password")) |
||||||
|
// @formatter:on |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
@Throws(Exception::class) |
||||||
|
fun getWhenUnauthenticatedThenRedirectsToBoth() { |
||||||
|
this.spring.register(UseAuthorizationManagerFactoryConfiguration::class.java, Http200Controller::class.java) |
||||||
|
.autowire() |
||||||
|
// @formatter:off |
||||||
|
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/")) |
||||||
|
.andExpect(MockMvcResultMatchers.status().is3xxRedirection()) |
||||||
|
.andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login")) |
||||||
|
// @formatter:on |
||||||
|
} |
||||||
|
|
||||||
|
@RestController |
||||||
|
internal class Http200Controller { |
||||||
|
@GetMapping("/**") |
||||||
|
fun ok(): String { |
||||||
|
return "ok" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,53 @@ |
|||||||
|
package org.springframework.security.kt.docs.servlet.authentication.authorizationmanagerfactory |
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean |
||||||
|
import org.springframework.context.annotation.Configuration |
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity |
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity |
||||||
|
import org.springframework.security.config.annotation.web.invoke |
||||||
|
import org.springframework.security.core.userdetails.User |
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService |
||||||
|
import org.springframework.security.provisioning.InMemoryUserDetailsManager |
||||||
|
import org.springframework.security.web.SecurityFilterChain |
||||||
|
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler |
||||||
|
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler |
||||||
|
|
||||||
|
@EnableWebSecurity |
||||||
|
@Configuration(proxyBeanMethods = false) |
||||||
|
class ListAuthoritiesEverywhereConfiguration { |
||||||
|
|
||||||
|
// tag::httpSecurity[] |
||||||
|
@Bean |
||||||
|
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? { |
||||||
|
// @formatter:off |
||||||
|
http { |
||||||
|
authorizeHttpRequests { |
||||||
|
authorize("/admin/**", hasAllAuthorities("FACTOR_PASSWORD", "FACTOR_OTT", "ROLE_ADMIN")) // <1> |
||||||
|
authorize(anyRequest, hasAllAuthorities("FACTOR_PASSWORD", "FACTOR_OTT")) |
||||||
|
} |
||||||
|
formLogin { } |
||||||
|
oneTimeTokenLogin { } |
||||||
|
} |
||||||
|
// @formatter:on |
||||||
|
return http.build() |
||||||
|
} |
||||||
|
// end::httpSecurity[] |
||||||
|
|
||||||
|
|
||||||
|
// end::httpSecurity[] |
||||||
|
@Bean |
||||||
|
fun userDetailsService(): UserDetailsService { |
||||||
|
return InMemoryUserDetailsManager( |
||||||
|
User.withDefaultPasswordEncoder() |
||||||
|
.username("user") |
||||||
|
.password("password") |
||||||
|
.authorities("app") |
||||||
|
.build() |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
@Bean |
||||||
|
fun tokenGenerationSuccessHandler(): OneTimeTokenGenerationSuccessHandler { |
||||||
|
return RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent") |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,60 @@ |
|||||||
|
package org.springframework.security.kt.docs.servlet.authentication.authorizationmanagerfactory |
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean |
||||||
|
import org.springframework.context.annotation.Configuration |
||||||
|
import org.springframework.security.authorization.AuthorizationManagerFactory |
||||||
|
import org.springframework.security.authorization.DefaultAuthorizationManagerFactory |
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity |
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity |
||||||
|
import org.springframework.security.config.annotation.web.invoke |
||||||
|
import org.springframework.security.core.userdetails.User |
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService |
||||||
|
import org.springframework.security.provisioning.InMemoryUserDetailsManager |
||||||
|
import org.springframework.security.web.SecurityFilterChain |
||||||
|
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler |
||||||
|
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler |
||||||
|
|
||||||
|
@EnableWebSecurity |
||||||
|
@Configuration(proxyBeanMethods = false) |
||||||
|
internal class UseAuthorizationManagerFactoryConfiguration { |
||||||
|
// tag::httpSecurity[] |
||||||
|
@Bean |
||||||
|
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? { |
||||||
|
// @formatter:off |
||||||
|
http { |
||||||
|
authorizeHttpRequests { |
||||||
|
authorize("/admin/**", hasRole("ADMIN")) |
||||||
|
authorize(anyRequest, authenticated) |
||||||
|
} |
||||||
|
formLogin { } |
||||||
|
oneTimeTokenLogin { } |
||||||
|
} |
||||||
|
// @formatter:on |
||||||
|
return http.build() |
||||||
|
} |
||||||
|
// end::httpSecurity[] |
||||||
|
|
||||||
|
// tag::authorizationManagerFactoryBean[] |
||||||
|
@Bean |
||||||
|
fun authz(): AuthorizationManagerFactory<Object> { |
||||||
|
return DefaultAuthorizationManagerFactory.builder<Object>() |
||||||
|
.requireAdditionalAuthorities("FACTOR_PASSWORD", "FACTOR_OTT").build() |
||||||
|
} |
||||||
|
// end::authorizationManagerFactoryBean[] |
||||||
|
|
||||||
|
@Bean |
||||||
|
fun userDetailsService(): UserDetailsService { |
||||||
|
return InMemoryUserDetailsManager( |
||||||
|
User.withDefaultPasswordEncoder() |
||||||
|
.username("user") |
||||||
|
.password("password") |
||||||
|
.authorities("app") |
||||||
|
.build() |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
@Bean |
||||||
|
fun tokenGenerationSuccessHandler(): OneTimeTokenGenerationSuccessHandler { |
||||||
|
return RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent") |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,95 @@ |
|||||||
|
package org.springframework.security.kt.docs.servlet.authentication.customauthorizationmanagerfactory |
||||||
|
|
||||||
|
import org.jspecify.annotations.NullMarked |
||||||
|
import org.springframework.context.annotation.Bean |
||||||
|
import org.springframework.context.annotation.Configuration |
||||||
|
import org.springframework.security.access.expression.SecurityExpressionRoot |
||||||
|
import org.springframework.security.authorization.* |
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity |
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity |
||||||
|
import org.springframework.security.config.annotation.web.invoke |
||||||
|
import org.springframework.security.core.Authentication |
||||||
|
import org.springframework.security.core.GrantedAuthority |
||||||
|
import org.springframework.security.core.authority.AuthorityUtils |
||||||
|
import org.springframework.security.core.userdetails.UserDetails |
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService |
||||||
|
import org.springframework.security.web.SecurityFilterChain |
||||||
|
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler |
||||||
|
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler |
||||||
|
import org.springframework.stereotype.Component |
||||||
|
import java.util.function.Supplier |
||||||
|
|
||||||
|
@EnableWebSecurity |
||||||
|
@Configuration(proxyBeanMethods = false) |
||||||
|
internal class CustomAuthorizationManagerFactory { |
||||||
|
|
||||||
|
// tag::httpSecurity[] |
||||||
|
@Bean |
||||||
|
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? { |
||||||
|
// @formatter:off |
||||||
|
http { |
||||||
|
authorizeHttpRequests { |
||||||
|
authorize("/admin/**", hasRole("ADMIN")) |
||||||
|
authorize(anyRequest, authenticated) |
||||||
|
} |
||||||
|
formLogin { } |
||||||
|
oneTimeTokenLogin { } |
||||||
|
} |
||||||
|
// @formatter:on |
||||||
|
return http.build() |
||||||
|
} |
||||||
|
// end::httpSecurity[] |
||||||
|
|
||||||
|
// tag::authorizationManager[] |
||||||
|
@Component |
||||||
|
internal open class OptInToMfaAuthorizationManager : AuthorizationManager<Object> { |
||||||
|
override fun authorize( |
||||||
|
authentication: Supplier<out Authentication?>, context: Object): AuthorizationResult { |
||||||
|
val principal = authentication.get().getPrincipal() as MyPrincipal? |
||||||
|
if (principal!!.optedIn) { |
||||||
|
val root = object : SecurityExpressionRoot<Object>(authentication, context) { } |
||||||
|
return AuthorityAuthorizationDecision( |
||||||
|
root.hasAuthority("FACTOR_OTT"), |
||||||
|
AuthorityUtils.createAuthorityList("FACTOR_OTT") |
||||||
|
) |
||||||
|
} |
||||||
|
return AuthorizationDecision(true) |
||||||
|
} |
||||||
|
} |
||||||
|
// end::authorizationManager[] |
||||||
|
|
||||||
|
// tag::authorizationManagerFactory[] |
||||||
|
@Bean |
||||||
|
fun authorizationManagerFactory(optIn: OptInToMfaAuthorizationManager?): AuthorizationManagerFactory<Object> { |
||||||
|
val defaults = DefaultAuthorizationManagerFactory<Object>() |
||||||
|
defaults.setAdditionalAuthorization(optIn) |
||||||
|
return defaults |
||||||
|
} |
||||||
|
// end::authorizationManagerFactory[] |
||||||
|
|
||||||
|
@NullMarked |
||||||
|
class MyPrincipal(val user: String, val optedIn: Boolean) : UserDetails { |
||||||
|
override fun getAuthorities(): MutableCollection<out GrantedAuthority> { |
||||||
|
return AuthorityUtils.createAuthorityList("app") |
||||||
|
} |
||||||
|
|
||||||
|
override fun getPassword(): String? { |
||||||
|
return null |
||||||
|
} |
||||||
|
|
||||||
|
override fun getUsername(): String { |
||||||
|
return this.user |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Bean |
||||||
|
fun users(): UserDetailsService { |
||||||
|
return UserDetailsService { username: String? -> MyPrincipal(username!!, username == "optedin") } |
||||||
|
} |
||||||
|
|
||||||
|
@Bean |
||||||
|
fun tokenGenerationSuccessHandler(): OneTimeTokenGenerationSuccessHandler { |
||||||
|
return RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent") |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,93 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2004-present the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.security.kt.docs.servlet.authentication.customauthorizationmanagerfactory |
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test |
||||||
|
import org.junit.jupiter.api.extension.ExtendWith |
||||||
|
import org.springframework.beans.factory.annotation.Autowired |
||||||
|
import org.springframework.security.authentication.TestingAuthenticationToken |
||||||
|
import org.springframework.security.config.test.SpringTestContext |
||||||
|
import org.springframework.security.config.test.SpringTestContextExtension |
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService |
||||||
|
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors |
||||||
|
import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers |
||||||
|
import org.springframework.test.web.servlet.MockMvc |
||||||
|
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders |
||||||
|
import org.springframework.test.web.servlet.result.MockMvcResultMatchers |
||||||
|
import org.springframework.web.bind.annotation.GetMapping |
||||||
|
import org.springframework.web.bind.annotation.RestController |
||||||
|
|
||||||
|
/** |
||||||
|
* Tests [CustomX509Configuration]. |
||||||
|
* |
||||||
|
* @author Rob Winch |
||||||
|
*/ |
||||||
|
@ExtendWith(SpringTestContextExtension::class) |
||||||
|
class CustomAuthorizationManagerFactoryTests { |
||||||
|
@JvmField |
||||||
|
val spring: SpringTestContext = SpringTestContext(this) |
||||||
|
|
||||||
|
@Autowired |
||||||
|
var mockMvc: MockMvc? = null |
||||||
|
|
||||||
|
@Autowired |
||||||
|
var users: UserDetailsService? = null |
||||||
|
|
||||||
|
@Test |
||||||
|
@Throws(Exception::class) |
||||||
|
fun getWhenOptedInThenRedirectsToOtt() { |
||||||
|
this.spring.register(CustomAuthorizationManagerFactory::class.java, Http200Controller::class.java).autowire() |
||||||
|
val user = this.users!!.loadUserByUsername("optedin") |
||||||
|
// @formatter:off |
||||||
|
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/").with(SecurityMockMvcRequestPostProcessors.user(user))) |
||||||
|
.andExpect(MockMvcResultMatchers.status().is3xxRedirection()) |
||||||
|
.andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor=ott")) |
||||||
|
// @formatter:on |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
@Throws(Exception::class) |
||||||
|
fun getWhenNotOptedInThenAllows() { |
||||||
|
this.spring.register(CustomAuthorizationManagerFactory::class.java, Http200Controller::class.java).autowire() |
||||||
|
val user = this.users!!.loadUserByUsername("user") |
||||||
|
// @formatter:off |
||||||
|
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/").with(SecurityMockMvcRequestPostProcessors.user(user))) |
||||||
|
.andExpect(MockMvcResultMatchers.status().isOk()) |
||||||
|
.andExpect(SecurityMockMvcResultMatchers.authenticated().withUsername("user")) |
||||||
|
// @formatter:on |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
@Throws(Exception::class) |
||||||
|
fun getWhenOptedAndHasFactorThenAllows() { |
||||||
|
this.spring.register(CustomAuthorizationManagerFactory::class.java, Http200Controller::class.java).autowire() |
||||||
|
val user = this.users!!.loadUserByUsername("optedin") |
||||||
|
val token = TestingAuthenticationToken(user, "", "FACTOR_OTT") |
||||||
|
// @formatter:off |
||||||
|
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/").with(SecurityMockMvcRequestPostProcessors.authentication(token))) |
||||||
|
.andExpect(MockMvcResultMatchers.status().isOk()) |
||||||
|
.andExpect(SecurityMockMvcResultMatchers.authenticated().withUsername("optedin")) |
||||||
|
// @formatter:on |
||||||
|
} |
||||||
|
|
||||||
|
@RestController |
||||||
|
internal class Http200Controller { |
||||||
|
@GetMapping("/**") |
||||||
|
fun ok(): String { |
||||||
|
return "ok" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,52 @@ |
|||||||
|
package org.springframework.security.kt.docs.servlet.authentication.multifactorauthentication |
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean |
||||||
|
import org.springframework.context.annotation.Configuration |
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity |
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity |
||||||
|
import org.springframework.security.config.annotation.web.invoke |
||||||
|
import org.springframework.security.core.userdetails.User |
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService |
||||||
|
import org.springframework.security.provisioning.InMemoryUserDetailsManager |
||||||
|
import org.springframework.security.web.SecurityFilterChain |
||||||
|
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler |
||||||
|
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler |
||||||
|
|
||||||
|
@EnableWebSecurity |
||||||
|
@Configuration(proxyBeanMethods = false) |
||||||
|
internal class ListAuthoritiesConfiguration { |
||||||
|
|
||||||
|
// tag::httpSecurity[] |
||||||
|
@Bean |
||||||
|
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? { |
||||||
|
// @formatter:off |
||||||
|
http { |
||||||
|
authorizeHttpRequests { |
||||||
|
authorize(anyRequest, hasAllAuthorities("FACTOR_PASSWORD", "FACTOR_OTT")) |
||||||
|
} |
||||||
|
formLogin { } |
||||||
|
oneTimeTokenLogin { } |
||||||
|
} |
||||||
|
// @formatter:on |
||||||
|
return http.build() |
||||||
|
} |
||||||
|
// end::httpSecurity[] |
||||||
|
|
||||||
|
|
||||||
|
// end::httpSecurity[] |
||||||
|
@Bean |
||||||
|
fun userDetailsService(): UserDetailsService { |
||||||
|
return InMemoryUserDetailsManager( |
||||||
|
User.withDefaultPasswordEncoder() |
||||||
|
.username("user") |
||||||
|
.password("password") |
||||||
|
.authorities("app") |
||||||
|
.build() |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
@Bean |
||||||
|
fun tokenGenerationSuccessHandler(): OneTimeTokenGenerationSuccessHandler { |
||||||
|
return RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent") |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,114 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2004-present the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.security.kt.docs.servlet.authentication.multifactorauthentication |
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test |
||||||
|
import org.junit.jupiter.api.extension.ExtendWith |
||||||
|
import org.springframework.beans.factory.annotation.Autowired |
||||||
|
import org.springframework.security.config.test.SpringTestContext |
||||||
|
import org.springframework.security.config.test.SpringTestContextExtension |
||||||
|
import org.springframework.security.test.context.support.WithMockUser |
||||||
|
import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener |
||||||
|
import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers |
||||||
|
import org.springframework.test.context.TestExecutionListeners |
||||||
|
import org.springframework.test.context.junit.jupiter.SpringExtension |
||||||
|
import org.springframework.test.web.servlet.MockMvc |
||||||
|
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders |
||||||
|
import org.springframework.test.web.servlet.result.MockMvcResultMatchers |
||||||
|
import org.springframework.web.bind.annotation.GetMapping |
||||||
|
import org.springframework.web.bind.annotation.RestController |
||||||
|
|
||||||
|
/** |
||||||
|
* Tests [CustomX509Configuration]. |
||||||
|
* |
||||||
|
* @author Rob Winch |
||||||
|
*/ |
||||||
|
@ExtendWith(SpringExtension::class, SpringTestContextExtension::class) |
||||||
|
@TestExecutionListeners(WithSecurityContextTestExecutionListener::class) |
||||||
|
class MultiFactorAuthenticationTests { |
||||||
|
@JvmField |
||||||
|
val spring: SpringTestContext = SpringTestContext(this) |
||||||
|
|
||||||
|
@Autowired |
||||||
|
var mockMvc: MockMvc? = null |
||||||
|
|
||||||
|
@Test |
||||||
|
@WithMockUser(authorities = ["FACTOR_PASSWORD", "FACTOR_OTT"]) |
||||||
|
@Throws(Exception::class) |
||||||
|
fun getWhenAuthenticatedWithPasswordAndOttThenPermits() { |
||||||
|
this.spring.register(ListAuthoritiesConfiguration::class.java, Http200Controller::class.java).autowire() |
||||||
|
// @formatter:off |
||||||
|
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/")) |
||||||
|
.andExpect(MockMvcResultMatchers.status().isOk()) |
||||||
|
.andExpect(SecurityMockMvcResultMatchers.authenticated().withUsername("user")) |
||||||
|
// @formatter:on |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
@WithMockUser(authorities = ["FACTOR_PASSWORD"]) |
||||||
|
@Throws(Exception::class) |
||||||
|
fun getWhenAuthenticatedWithPasswordThenRedirectsToOtt() { |
||||||
|
this.spring.register(ListAuthoritiesConfiguration::class.java, Http200Controller::class.java).autowire() |
||||||
|
// @formatter:off |
||||||
|
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/")) |
||||||
|
.andExpect(MockMvcResultMatchers.status().is3xxRedirection()) |
||||||
|
.andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor=ott")) |
||||||
|
// @formatter:on |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
@WithMockUser(authorities = ["FACTOR_OTT"]) |
||||||
|
@Throws(Exception::class) |
||||||
|
fun getWhenAuthenticatedWithOttThenRedirectsToPassword() { |
||||||
|
this.spring.register(ListAuthoritiesConfiguration::class.java, Http200Controller::class.java).autowire() |
||||||
|
// @formatter:off |
||||||
|
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/")) |
||||||
|
.andExpect(MockMvcResultMatchers.status().is3xxRedirection()) |
||||||
|
.andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor=password")) |
||||||
|
// @formatter:on |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
@WithMockUser |
||||||
|
@Throws(Exception::class) |
||||||
|
fun getWhenAuthenticatedThenRedirectsToPassword() { |
||||||
|
this.spring.register(ListAuthoritiesConfiguration::class.java, Http200Controller::class.java).autowire() |
||||||
|
// @formatter:off |
||||||
|
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/")) |
||||||
|
.andExpect(MockMvcResultMatchers.status().is3xxRedirection()) |
||||||
|
.andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor=password")) |
||||||
|
// @formatter:on |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
@Throws(Exception::class) |
||||||
|
fun getWhenUnauthenticatedThenRedirectsToBoth() { |
||||||
|
this.spring.register(ListAuthoritiesConfiguration::class.java, Http200Controller::class.java).autowire() |
||||||
|
// @formatter:off |
||||||
|
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/")) |
||||||
|
.andExpect(MockMvcResultMatchers.status().is3xxRedirection()) |
||||||
|
.andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login")) |
||||||
|
// @formatter:on |
||||||
|
} |
||||||
|
|
||||||
|
@RestController |
||||||
|
internal class Http200Controller { |
||||||
|
@GetMapping("/**") |
||||||
|
fun ok(): String { |
||||||
|
return "ok" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,129 @@ |
|||||||
|
package org.springframework.security.kt.docs.servlet.authentication.obtainingmoreauthorization |
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest |
||||||
|
import jakarta.servlet.http.HttpServletResponse |
||||||
|
import org.springframework.context.annotation.Bean |
||||||
|
import org.springframework.context.annotation.Configuration |
||||||
|
import org.springframework.security.authorization.AllAuthoritiesAuthorizationManager.hasAllAuthorities |
||||||
|
import org.springframework.security.authorization.AuthorizationDecision |
||||||
|
import org.springframework.security.authorization.AuthorizationManager |
||||||
|
import org.springframework.security.authorization.AuthorizationManagerFactory |
||||||
|
import org.springframework.security.authorization.AuthorizationManagers.allOf |
||||||
|
import org.springframework.security.authorization.DefaultAuthorizationManagerFactory |
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity |
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity |
||||||
|
import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer |
||||||
|
import org.springframework.security.config.annotation.web.invoke |
||||||
|
import org.springframework.security.core.AuthenticationException |
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository |
||||||
|
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository |
||||||
|
import org.springframework.security.oauth2.client.registration.TestClientRegistrations |
||||||
|
import org.springframework.security.web.AuthenticationEntryPoint |
||||||
|
import org.springframework.security.web.DefaultSecurityFilterChain |
||||||
|
import org.springframework.security.web.access.intercept.RequestAuthorizationContext |
||||||
|
import org.springframework.stereotype.Component |
||||||
|
|
||||||
|
@EnableWebSecurity |
||||||
|
@Configuration(proxyBeanMethods = false) |
||||||
|
internal class MissingAuthorityConfiguration { |
||||||
|
|
||||||
|
// tag::httpSecurity[] |
||||||
|
@Bean |
||||||
|
fun securityFilterChain(http: HttpSecurity, oauth2: ScopeRetrievingAuthenticationEntryPoint): DefaultSecurityFilterChain? { |
||||||
|
http { |
||||||
|
authorizeHttpRequests { |
||||||
|
authorize("/profile/**", hasAuthority("SCOPE_profile:read")) |
||||||
|
authorize(anyRequest, authenticated) |
||||||
|
} |
||||||
|
x509 { } |
||||||
|
oauth2Login { } |
||||||
|
} |
||||||
|
|
||||||
|
http.exceptionHandling { e: ExceptionHandlingConfigurer<HttpSecurity> -> e |
||||||
|
.defaultDeniedHandlerForMissingAuthority(oauth2, "SCOPE_profile:read") |
||||||
|
} |
||||||
|
return http.build() |
||||||
|
} |
||||||
|
// end::httpSecurity[] |
||||||
|
|
||||||
|
// tag::authenticationEntryPoint[] |
||||||
|
@Component |
||||||
|
internal class ScopeRetrievingAuthenticationEntryPoint : AuthenticationEntryPoint { |
||||||
|
override fun commence(request: HttpServletRequest, response: HttpServletResponse, authException: AuthenticationException) { |
||||||
|
response.sendRedirect("https://authz.example.org/authorize?scope=profile:read") |
||||||
|
} |
||||||
|
} |
||||||
|
// end::authenticationEntryPoint[] |
||||||
|
|
||||||
|
// tag::authorizationManagerFactoryBean[] |
||||||
|
@Bean |
||||||
|
fun authz(): AuthorizationManagerFactory<RequestAuthorizationContext> { |
||||||
|
return FactorAuthorizationManagerFactory(hasAllAuthorities("FACTOR_X509", "FACTOR_AUTHORIZATION_CODE")) |
||||||
|
} |
||||||
|
// end::authorizationManagerFactoryBean[] |
||||||
|
|
||||||
|
// tag::authorizationManagerFactory[] |
||||||
|
internal inner class FactorAuthorizationManagerFactory(private val hasAuthorities: AuthorizationManager<RequestAuthorizationContext>) : |
||||||
|
AuthorizationManagerFactory<RequestAuthorizationContext> { |
||||||
|
private val delegate = DefaultAuthorizationManagerFactory<RequestAuthorizationContext>() |
||||||
|
|
||||||
|
override fun permitAll(): AuthorizationManager<RequestAuthorizationContext> { |
||||||
|
return this.delegate.permitAll() |
||||||
|
} |
||||||
|
|
||||||
|
override fun denyAll(): AuthorizationManager<RequestAuthorizationContext> { |
||||||
|
return this.delegate.denyAll() |
||||||
|
} |
||||||
|
|
||||||
|
override fun hasRole(role: String): AuthorizationManager<RequestAuthorizationContext> { |
||||||
|
return hasAnyRole(role) |
||||||
|
} |
||||||
|
|
||||||
|
override fun hasAnyRole(vararg roles: String): AuthorizationManager<RequestAuthorizationContext> { |
||||||
|
return addFactors(this.delegate.hasAnyRole(*roles)) |
||||||
|
} |
||||||
|
|
||||||
|
override fun hasAllRoles(vararg roles: String): AuthorizationManager<RequestAuthorizationContext> { |
||||||
|
return addFactors(this.delegate.hasAllRoles(*roles)) |
||||||
|
} |
||||||
|
|
||||||
|
override fun hasAuthority(authority: String): AuthorizationManager<RequestAuthorizationContext> { |
||||||
|
return hasAnyAuthority(authority) |
||||||
|
} |
||||||
|
|
||||||
|
override fun hasAnyAuthority(vararg authorities: String): AuthorizationManager<RequestAuthorizationContext> { |
||||||
|
return addFactors(this.delegate.hasAnyAuthority(*authorities)) |
||||||
|
} |
||||||
|
|
||||||
|
override fun hasAllAuthorities(vararg authorities: String): AuthorizationManager<RequestAuthorizationContext> { |
||||||
|
return addFactors(this.delegate.hasAllAuthorities(*authorities)) |
||||||
|
} |
||||||
|
|
||||||
|
override fun authenticated(): AuthorizationManager<RequestAuthorizationContext> { |
||||||
|
return addFactors(this.delegate.authenticated()) |
||||||
|
} |
||||||
|
|
||||||
|
override fun fullyAuthenticated(): AuthorizationManager<RequestAuthorizationContext> { |
||||||
|
return addFactors(this.delegate.fullyAuthenticated()) |
||||||
|
} |
||||||
|
|
||||||
|
override fun rememberMe(): AuthorizationManager<RequestAuthorizationContext> { |
||||||
|
return addFactors(this.delegate.rememberMe()) |
||||||
|
} |
||||||
|
|
||||||
|
override fun anonymous(): AuthorizationManager<RequestAuthorizationContext> { |
||||||
|
return this.delegate.anonymous() |
||||||
|
} |
||||||
|
|
||||||
|
private fun addFactors(delegate: AuthorizationManager<RequestAuthorizationContext>): AuthorizationManager<RequestAuthorizationContext> { |
||||||
|
return allOf(AuthorizationDecision(false), this.hasAuthorities, delegate) |
||||||
|
} |
||||||
|
} |
||||||
|
// end::authorizationManagerFactory[] |
||||||
|
|
||||||
|
// end::authenticationEntryPoint[] |
||||||
|
@Bean |
||||||
|
fun clients(): ClientRegistrationRepository { |
||||||
|
return InMemoryClientRegistrationRepository(TestClientRegistrations.clientRegistration().build()) |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,104 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2004-present the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.security.kt.docs.servlet.authentication.obtainingmoreauthorization |
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test |
||||||
|
import org.junit.jupiter.api.extension.ExtendWith |
||||||
|
import org.springframework.beans.factory.annotation.Autowired |
||||||
|
import org.springframework.security.config.test.SpringTestContext |
||||||
|
import org.springframework.security.config.test.SpringTestContextExtension |
||||||
|
import org.springframework.security.docs.servlet.authentication.obtainingmoreauthorization.ScopeConfiguration |
||||||
|
import org.springframework.security.test.context.support.WithMockUser |
||||||
|
import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener |
||||||
|
import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers |
||||||
|
import org.springframework.test.context.TestExecutionListeners |
||||||
|
import org.springframework.test.context.junit.jupiter.SpringExtension |
||||||
|
import org.springframework.test.web.servlet.MockMvc |
||||||
|
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders |
||||||
|
import org.springframework.test.web.servlet.result.MockMvcResultMatchers |
||||||
|
import org.springframework.web.bind.annotation.GetMapping |
||||||
|
import org.springframework.web.bind.annotation.RestController |
||||||
|
|
||||||
|
/** |
||||||
|
* Tests [CustomX509Configuration]. |
||||||
|
* |
||||||
|
* @author Rob Winch |
||||||
|
*/ |
||||||
|
@ExtendWith(SpringExtension::class, SpringTestContextExtension::class) |
||||||
|
@TestExecutionListeners(WithSecurityContextTestExecutionListener::class) |
||||||
|
class ObtainingMoreAuthorizationTests { |
||||||
|
@JvmField |
||||||
|
val spring: SpringTestContext = SpringTestContext(this) |
||||||
|
|
||||||
|
@Autowired |
||||||
|
var mockMvc: MockMvc? = null |
||||||
|
|
||||||
|
@Test |
||||||
|
@WithMockUser |
||||||
|
@Throws(Exception::class) |
||||||
|
fun profileWhenScopeConfigurationThenDenies() { |
||||||
|
this.spring.register(ScopeConfiguration::class.java, Http200Controller::class.java).autowire() |
||||||
|
// @formatter:off |
||||||
|
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/profile")) |
||||||
|
.andExpect(MockMvcResultMatchers.status().isForbidden()) |
||||||
|
// @formatter:on |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
@WithMockUser(authorities = ["FACTOR_X509", "FACTOR_AUTHORIZATION_CODE"]) |
||||||
|
@Throws(Exception::class) |
||||||
|
fun profileWhenMissingAuthorityConfigurationThenRedirectsToAuthorizationServer() { |
||||||
|
this.spring.register(MissingAuthorityConfiguration::class.java, Http200Controller::class.java).autowire() |
||||||
|
// @formatter:off |
||||||
|
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/profile")) |
||||||
|
.andExpect(MockMvcResultMatchers.status().is3xxRedirection()) |
||||||
|
.andExpect(MockMvcResultMatchers.redirectedUrl("https://authz.example.org/authorize?scope=profile:read")) |
||||||
|
// @formatter:on |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
@WithMockUser(authorities = ["SCOPE_profile:read"]) |
||||||
|
@Throws(Exception::class) |
||||||
|
fun profileWhenMissingX509WithOttThenForbidden() { |
||||||
|
this.spring.register(MissingAuthorityConfiguration::class.java, Http200Controller::class.java).autowire() |
||||||
|
// @formatter:off |
||||||
|
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/profile")) |
||||||
|
.andExpect(MockMvcResultMatchers.status().isForbidden()) |
||||||
|
// @formatter:on |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
@WithMockUser(authorities = ["FACTOR_X509", "FACTOR_AUTHORIZATION_CODE", "SCOPE_profile:read"]) |
||||||
|
@Throws( |
||||||
|
Exception::class |
||||||
|
) |
||||||
|
fun profileWhenAuthenticatedAndHasScopeThenPermits() { |
||||||
|
this.spring.register(MissingAuthorityConfiguration::class.java, Http200Controller::class.java).autowire() |
||||||
|
// @formatter:off |
||||||
|
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/profile")) |
||||||
|
.andExpect(MockMvcResultMatchers.status().isOk()) |
||||||
|
.andExpect(SecurityMockMvcResultMatchers.authenticated().withUsername("user")) |
||||||
|
// @formatter:on |
||||||
|
} |
||||||
|
|
||||||
|
@RestController |
||||||
|
internal class Http200Controller { |
||||||
|
@GetMapping("/**") |
||||||
|
fun ok(): String { |
||||||
|
return "ok" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,38 @@ |
|||||||
|
package org.springframework.security.kt.docs.servlet.authentication.obtainingmoreauthorization |
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean |
||||||
|
import org.springframework.context.annotation.Configuration |
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity |
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity |
||||||
|
import org.springframework.security.config.annotation.web.invoke |
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository |
||||||
|
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository |
||||||
|
import org.springframework.security.oauth2.client.registration.TestClientRegistrations |
||||||
|
import org.springframework.security.web.SecurityFilterChain |
||||||
|
|
||||||
|
@EnableWebSecurity |
||||||
|
@Configuration(proxyBeanMethods = false) |
||||||
|
class ScopeConfiguration { |
||||||
|
// tag::httpSecurity[] |
||||||
|
@Bean |
||||||
|
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? { |
||||||
|
// @formatter:off |
||||||
|
http { |
||||||
|
authorizeHttpRequests { |
||||||
|
authorize("/profile/**", hasAuthority("SCOPE_profile:read")) |
||||||
|
authorize(anyRequest, authenticated) |
||||||
|
} |
||||||
|
x509 { } |
||||||
|
oauth2Login { } |
||||||
|
} |
||||||
|
// @formatter:on |
||||||
|
return http.build() |
||||||
|
} |
||||||
|
// end::httpSecurity[] |
||||||
|
|
||||||
|
// end::httpSecurity[] |
||||||
|
@Bean |
||||||
|
fun clients(): ClientRegistrationRepository { |
||||||
|
return InMemoryClientRegistrationRepository(TestClientRegistrations.clientRegistration().build()) |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,93 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2004-present the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.security.kt.docs.servlet.authentication.reauthentication |
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test |
||||||
|
import org.junit.jupiter.api.extension.ExtendWith |
||||||
|
import org.springframework.beans.factory.annotation.Autowired |
||||||
|
import org.springframework.security.config.test.SpringTestContext |
||||||
|
import org.springframework.security.config.test.SpringTestContextExtension |
||||||
|
import org.springframework.security.docs.servlet.authentication.reauthentication.RequireOttConfiguration |
||||||
|
import org.springframework.security.docs.servlet.authentication.reauthentication.SimpleConfiguration |
||||||
|
import org.springframework.security.test.context.support.WithMockUser |
||||||
|
import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener |
||||||
|
import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers |
||||||
|
import org.springframework.test.context.TestExecutionListeners |
||||||
|
import org.springframework.test.context.junit.jupiter.SpringExtension |
||||||
|
import org.springframework.test.web.servlet.MockMvc |
||||||
|
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders |
||||||
|
import org.springframework.test.web.servlet.result.MockMvcResultMatchers |
||||||
|
import org.springframework.web.bind.annotation.GetMapping |
||||||
|
import org.springframework.web.bind.annotation.RestController |
||||||
|
|
||||||
|
/** |
||||||
|
* Tests [CustomX509Configuration]. |
||||||
|
* |
||||||
|
* @author Rob Winch |
||||||
|
*/ |
||||||
|
@ExtendWith(SpringExtension::class, SpringTestContextExtension::class) |
||||||
|
@TestExecutionListeners(WithSecurityContextTestExecutionListener::class) |
||||||
|
class ReauthenticationTests { |
||||||
|
@JvmField |
||||||
|
val spring: SpringTestContext = SpringTestContext(this) |
||||||
|
|
||||||
|
@Autowired |
||||||
|
var mockMvc: MockMvc? = null |
||||||
|
|
||||||
|
@Test |
||||||
|
@WithMockUser |
||||||
|
@Throws(Exception::class) |
||||||
|
fun formLoginWhenSimpleConfigurationThenPermits() { |
||||||
|
this.spring.register(SimpleConfiguration::class.java, Http200Controller::class.java).autowire() |
||||||
|
// @formatter:off |
||||||
|
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/")) |
||||||
|
.andExpect(MockMvcResultMatchers.status().isOk()) |
||||||
|
.andExpect(SecurityMockMvcResultMatchers.authenticated().withUsername("user")) |
||||||
|
// @formatter:on |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
@WithMockUser |
||||||
|
@Throws(Exception::class) |
||||||
|
fun formLoginWhenRequireOttConfigurationThenRedirectsToOtt() { |
||||||
|
this.spring.register(RequireOttConfiguration::class.java, Http200Controller::class.java).autowire() |
||||||
|
// @formatter:off |
||||||
|
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/profile")) |
||||||
|
.andExpect(MockMvcResultMatchers.status().is3xxRedirection()) |
||||||
|
.andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor=ott")) |
||||||
|
// @formatter:on |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
@WithMockUser(authorities = ["FACTOR_OTT"]) |
||||||
|
@Throws(Exception::class) |
||||||
|
fun ottWhenRequireOttConfigurationThenAllows() { |
||||||
|
this.spring.register(RequireOttConfiguration::class.java, Http200Controller::class.java).autowire() |
||||||
|
// @formatter:off |
||||||
|
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/profile")) |
||||||
|
.andExpect(MockMvcResultMatchers.status().isOk()) |
||||||
|
.andExpect(SecurityMockMvcResultMatchers.authenticated().withUsername("user")) |
||||||
|
// @formatter:on |
||||||
|
} |
||||||
|
|
||||||
|
@RestController |
||||||
|
internal class Http200Controller { |
||||||
|
@GetMapping("/**") |
||||||
|
fun ok(): String { |
||||||
|
return "ok" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,52 @@ |
|||||||
|
package org.springframework.security.kt.docs.servlet.authentication.reauthentication |
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean |
||||||
|
import org.springframework.context.annotation.Configuration |
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity |
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity |
||||||
|
import org.springframework.security.config.annotation.web.invoke |
||||||
|
import org.springframework.security.core.userdetails.User |
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService |
||||||
|
import org.springframework.security.provisioning.InMemoryUserDetailsManager |
||||||
|
import org.springframework.security.web.SecurityFilterChain |
||||||
|
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler |
||||||
|
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler |
||||||
|
|
||||||
|
@EnableWebSecurity |
||||||
|
@Configuration(proxyBeanMethods = false) |
||||||
|
class RequireOttConfiguration { |
||||||
|
|
||||||
|
// tag::httpSecurity[] |
||||||
|
@Bean |
||||||
|
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? { |
||||||
|
// @formatter:off |
||||||
|
http { |
||||||
|
authorizeHttpRequests { |
||||||
|
authorize("/profile/**", hasAuthority("FACTOR_OTT")) // <1> |
||||||
|
authorize(anyRequest, authenticated) |
||||||
|
} |
||||||
|
formLogin { } |
||||||
|
oneTimeTokenLogin { } |
||||||
|
} |
||||||
|
// @formatter:on |
||||||
|
return http.build() |
||||||
|
} |
||||||
|
// end::httpSecurity[] |
||||||
|
|
||||||
|
// end::httpSecurity[] |
||||||
|
@Bean |
||||||
|
fun userDetailsService(): UserDetailsService { |
||||||
|
return InMemoryUserDetailsManager( |
||||||
|
User.withDefaultPasswordEncoder() |
||||||
|
.username("user") |
||||||
|
.password("password") |
||||||
|
.authorities("app") |
||||||
|
.build() |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
@Bean |
||||||
|
fun tokenGenerationSuccessHandler(): OneTimeTokenGenerationSuccessHandler { |
||||||
|
return RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent") |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,50 @@ |
|||||||
|
package org.springframework.security.kt.docs.servlet.authentication.reauthentication |
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean |
||||||
|
import org.springframework.context.annotation.Configuration |
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity |
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity |
||||||
|
import org.springframework.security.config.annotation.web.invoke |
||||||
|
import org.springframework.security.core.userdetails.User |
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService |
||||||
|
import org.springframework.security.provisioning.InMemoryUserDetailsManager |
||||||
|
import org.springframework.security.web.SecurityFilterChain |
||||||
|
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler |
||||||
|
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler |
||||||
|
|
||||||
|
@EnableWebSecurity |
||||||
|
@Configuration(proxyBeanMethods = false) |
||||||
|
class SimpleConfiguration { |
||||||
|
// tag::httpSecurity[] |
||||||
|
@Bean |
||||||
|
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? { |
||||||
|
// @formatter:off |
||||||
|
http { |
||||||
|
authorizeHttpRequests { |
||||||
|
authorize(anyRequest, authenticated) |
||||||
|
} |
||||||
|
formLogin { } |
||||||
|
oneTimeTokenLogin { } |
||||||
|
} |
||||||
|
// @formatter:on |
||||||
|
return http.build() |
||||||
|
} |
||||||
|
// end::httpSecurity[] |
||||||
|
|
||||||
|
// end::httpSecurity[] |
||||||
|
@Bean |
||||||
|
fun userDetailsService(): UserDetailsService { |
||||||
|
return InMemoryUserDetailsManager( |
||||||
|
User.withDefaultPasswordEncoder() |
||||||
|
.username("user") |
||||||
|
.password("password") |
||||||
|
.authorities("app") |
||||||
|
.build() |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
@Bean |
||||||
|
fun tokenGenerationSuccessHandler(): OneTimeTokenGenerationSuccessHandler { |
||||||
|
return RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent") |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue