Document RequiredFactor Valid Duration
Issue gh-17997
This commit is contained in:
@@ -55,7 +55,7 @@ We have demonstrated how to configure an entire application to require MFA (Glob
|
|||||||
However, there are times that an application only wants parts of the application to require MFA.
|
However, there are times that an application only wants parts of the application to require MFA.
|
||||||
Consider the following requirements:
|
Consider the following requirements:
|
||||||
|
|
||||||
- URLs that begin with `/admin/**` should require the authorities `FACTOR_OTT`, `FACTOR_PASSWORD`, `ROLE_ADMIN`.
|
- URLs that begin with `/admin/` should require the authorities `FACTOR_OTT`, `FACTOR_PASSWORD`, `ROLE_ADMIN`.
|
||||||
- URLs that begin with `/user/settings` should require the authorities `FACTOR_OTT`, `FACTOR_PASSWORD`
|
- URLs that begin with `/user/settings` should require the authorities `FACTOR_OTT`, `FACTOR_PASSWORD`
|
||||||
- Every other URL requires an authenticated user
|
- Every other URL requires an authenticated user
|
||||||
|
|
||||||
@@ -72,6 +72,30 @@ By not publishing it as a Bean, we are able to selectively use the `Authorizatio
|
|||||||
There is no MFA requirement, because the `AuthorizationManagerFactory` is not used.
|
There is no MFA requirement, because the `AuthorizationManagerFactory` is not used.
|
||||||
<5> Set up the authentication mechanisms that can provide the required factors.
|
<5> Set up the authentication mechanisms that can provide the required factors.
|
||||||
|
|
||||||
|
[[valid-duration]]
|
||||||
|
== Specifying a Valid Duration
|
||||||
|
|
||||||
|
At times, we may want to define authorization rules based upon how recently we authenticated.
|
||||||
|
For example, an application may want to require that the user has authenticated within the last hour in order to allow access to the `/user/settings` endpoint.
|
||||||
|
|
||||||
|
Remember at the time of authentication, a `FactorGrantedAuthority` is added to the `Authentication`.
|
||||||
|
The `FactorGrantedAuthority` specifies when it was `issuedAt`, but does not describe how long it is valid for.
|
||||||
|
This is intentional, because it allows a single `FactorGrantedAuthority` to be used with different ``validDuration``s.
|
||||||
|
|
||||||
|
Let's take a look at an example that illustrates how to meet the following requirements:
|
||||||
|
|
||||||
|
- URLs that begin with `/admin/` should require that a password has been provided within the last 30 minutes
|
||||||
|
- URLs that being with `/user/settings` should require that a password has been provided within the last hour
|
||||||
|
- Otherwise, authentication is required, but it does not care if it is a password or how long ago authentication occurred
|
||||||
|
|
||||||
|
include-code::./ValidDurationConfiguration[tag=httpSecurity,indent=0]
|
||||||
|
<1> First we define `passwordIn30m` as a requirement for a password within 30 minutes
|
||||||
|
<2> Next, we define `passwordInHour` as a requirement for a password within an hour
|
||||||
|
<3> We use `passwordIn30m` to require that URLs that begin with `/admin/` should require that a password has been provided in the last 30 minutes and that the user has the `ROLE_ADMIN` authority
|
||||||
|
<4> We use `passwordInHour` to require that URLs that begin with `/user/settings` should require that a password has been provided in the last hour
|
||||||
|
<5> Otherwise, authentication is required, but it does not care if it is a password or how long ago authentication occurred
|
||||||
|
<6> Set up the authentication mechanisms that can provide the required factors.
|
||||||
|
|
||||||
[[programmatic-mfa]]
|
[[programmatic-mfa]]
|
||||||
== Programmatic MFA
|
== Programmatic MFA
|
||||||
|
|
||||||
|
|||||||
+6
-7
@@ -24,13 +24,12 @@ class SelectiveMfaConfiguration {
|
|||||||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
// <1>
|
// <1>
|
||||||
AuthorizationManagerFactory<Object> mfa =
|
var mfa = AuthorizationManagerFactories.multiFactor()
|
||||||
AuthorizationManagerFactories.<Object>multiFactor()
|
.requireFactors(
|
||||||
.requireFactors(
|
FactorGrantedAuthority.PASSWORD_AUTHORITY,
|
||||||
FactorGrantedAuthority.PASSWORD_AUTHORITY,
|
FactorGrantedAuthority.OTT_AUTHORITY
|
||||||
FactorGrantedAuthority.OTT_AUTHORITY
|
)
|
||||||
)
|
.build();
|
||||||
.build();
|
|
||||||
http
|
http
|
||||||
.authorizeHttpRequests((authorize) -> authorize
|
.authorizeHttpRequests((authorize) -> authorize
|
||||||
// <2>
|
// <2>
|
||||||
|
|||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
package org.springframework.security.docs.servlet.authentication.validduration;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.authorization.AuthorizationManagerFactories;
|
||||||
|
import org.springframework.security.authorization.AuthorizationManagerFactory;
|
||||||
|
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.authority.FactorGrantedAuthority;
|
||||||
|
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 ValidDurationConfiguration {
|
||||||
|
|
||||||
|
// tag::httpSecurity[]
|
||||||
|
@Bean
|
||||||
|
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||||
|
// @formatter:off
|
||||||
|
// <1>
|
||||||
|
var passwordIn30m = AuthorizationManagerFactories.multiFactor()
|
||||||
|
.requireFactor( (factor) -> factor
|
||||||
|
.passwordAuthority()
|
||||||
|
.validDuration(Duration.ofMinutes(30))
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
// <2>
|
||||||
|
var passwordInHour = AuthorizationManagerFactories.multiFactor()
|
||||||
|
.requireFactor( (factor) -> factor
|
||||||
|
.passwordAuthority()
|
||||||
|
.validDuration(Duration.ofHours(1))
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
http
|
||||||
|
.authorizeHttpRequests((authorize) -> authorize
|
||||||
|
// <3>
|
||||||
|
.requestMatchers("/admin/**").access(passwordIn30m.hasRole("ADMIN"))
|
||||||
|
// <4>
|
||||||
|
.requestMatchers("/user/settings/**").access(passwordInHour.authenticated())
|
||||||
|
// <5>
|
||||||
|
.anyRequest().authenticated()
|
||||||
|
)
|
||||||
|
// <6>
|
||||||
|
.formLogin(Customizer.withDefaults());
|
||||||
|
// @formatter:on
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
// end::httpSecurity[]
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
UserDetailsService userDetailsService() {
|
||||||
|
return new InMemoryUserDetailsManager(
|
||||||
|
User.withDefaultPasswordEncoder()
|
||||||
|
.username("user")
|
||||||
|
.password("password")
|
||||||
|
.authorities("app")
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+128
@@ -0,0 +1,128 @@
|
|||||||
|
/*
|
||||||
|
* 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.validduration;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
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.authority.FactorGrantedAuthority;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.security.docs.servlet.authentication.servletx509config.CustomX509Configuration;
|
||||||
|
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.test.web.servlet.request.RequestPostProcessor;
|
||||||
|
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.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrlPattern;
|
||||||
|
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 ValidDurationConfigurationTests {
|
||||||
|
|
||||||
|
public final SpringTestContext spring = new SpringTestContext(this);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
MockMvc mockMvc;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void adminWhenExpiredThenRequired() throws Exception {
|
||||||
|
this.spring.register(
|
||||||
|
ValidDurationConfiguration.class, Http200Controller.class).autowire();
|
||||||
|
// @formatter:off
|
||||||
|
this.mockMvc.perform(get("/admin/").with(admin(Duration.ofMinutes(31))))
|
||||||
|
.andExpect(status().is3xxRedirection())
|
||||||
|
.andExpect(redirectedUrlPattern("http://localhost/login?*"));
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void adminWhenNotExpiredThenOk() throws Exception {
|
||||||
|
this.spring.register(
|
||||||
|
ValidDurationConfiguration.class, Http200Controller.class).autowire();
|
||||||
|
// @formatter:off
|
||||||
|
this.mockMvc.perform(get("/admin/").with(admin(Duration.ofMinutes(29))))
|
||||||
|
.andExpect(status().isOk());
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void settingsWhenExpiredThenRequired() throws Exception {
|
||||||
|
this.spring.register(
|
||||||
|
ValidDurationConfiguration.class, Http200Controller.class).autowire();
|
||||||
|
// @formatter:off
|
||||||
|
this.mockMvc.perform(get("/user/settings").with(user(Duration.ofMinutes(61))))
|
||||||
|
.andExpect(status().is3xxRedirection())
|
||||||
|
.andExpect(redirectedUrlPattern("http://localhost/login?*"));
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void settingsWhenNotExpiredThenOk() throws Exception {
|
||||||
|
this.spring.register(
|
||||||
|
ValidDurationConfiguration.class, Http200Controller.class).autowire();
|
||||||
|
// @formatter:off
|
||||||
|
this.mockMvc.perform(get("/user/settings").with(user(Duration.ofMinutes(59))))
|
||||||
|
.andExpect(status().isOk());
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RequestPostProcessor admin(Duration sinceAuthn) {
|
||||||
|
return authn("admin", sinceAuthn);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RequestPostProcessor user(Duration sinceAuthn) {
|
||||||
|
return authn("user", sinceAuthn);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RequestPostProcessor authn(String username, Duration sinceAuthn) {
|
||||||
|
Instant issuedAt = Instant.now().minus(sinceAuthn);
|
||||||
|
FactorGrantedAuthority factor = FactorGrantedAuthority
|
||||||
|
.withAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY)
|
||||||
|
.issuedAt(issuedAt)
|
||||||
|
.build();
|
||||||
|
String role = username.toUpperCase();
|
||||||
|
TestingAuthenticationToken authn = new TestingAuthenticationToken(username, "",
|
||||||
|
factor, new SimpleGrantedAuthority("ROLE_" + role));
|
||||||
|
return authentication(authn);
|
||||||
|
}
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
static class Http200Controller {
|
||||||
|
@GetMapping("/**")
|
||||||
|
String ok() {
|
||||||
|
return "ok";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-7
@@ -24,13 +24,12 @@ internal class SelectiveMfaConfiguration {
|
|||||||
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? {
|
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? {
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
// <1>
|
// <1>
|
||||||
val mfa: AuthorizationManagerFactory<Any> =
|
val mfa = AuthorizationManagerFactories.multiFactor<Any>()
|
||||||
AuthorizationManagerFactories.multiFactor<Any>()
|
.requireFactors(
|
||||||
.requireFactors(
|
FactorGrantedAuthority.PASSWORD_AUTHORITY,
|
||||||
FactorGrantedAuthority.PASSWORD_AUTHORITY,
|
FactorGrantedAuthority.OTT_AUTHORITY
|
||||||
FactorGrantedAuthority.OTT_AUTHORITY
|
)
|
||||||
)
|
.build()
|
||||||
.build()
|
|
||||||
http {
|
http {
|
||||||
authorizeHttpRequests {
|
authorizeHttpRequests {
|
||||||
// <2>
|
// <2>
|
||||||
|
|||||||
+73
@@ -0,0 +1,73 @@
|
|||||||
|
package org.springframework.security.kt.docs.servlet.authentication.validduration
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.security.authorization.AuthorizationManagerFactories
|
||||||
|
import org.springframework.security.authorization.AuthorizationManagerFactory
|
||||||
|
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.authority.FactorGrantedAuthority
|
||||||
|
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 java.time.Duration
|
||||||
|
|
||||||
|
@EnableWebSecurity
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
internal class ValidDurationConfiguration {
|
||||||
|
// tag::httpSecurity[]
|
||||||
|
@Bean
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? {
|
||||||
|
// @formatter:off
|
||||||
|
// <1>
|
||||||
|
val passwordIn30m = AuthorizationManagerFactories.multiFactor<Any>()
|
||||||
|
.requireFactor( { factor -> factor
|
||||||
|
.passwordAuthority()
|
||||||
|
.validDuration(Duration.ofMinutes(30))
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
// <2>
|
||||||
|
val passwordInHour = AuthorizationManagerFactories.multiFactor<Any>()
|
||||||
|
.requireFactor( { factor -> factor
|
||||||
|
.passwordAuthority()
|
||||||
|
.validDuration(Duration.ofHours(1))
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
http {
|
||||||
|
authorizeHttpRequests {
|
||||||
|
// <3>
|
||||||
|
authorize("/admin/**", passwordIn30m.hasRole("ADMIN"))
|
||||||
|
// <4>
|
||||||
|
authorize("/user/settings/**", passwordInHour.authenticated())
|
||||||
|
// <5>
|
||||||
|
authorize(anyRequest, authenticated)
|
||||||
|
}
|
||||||
|
// <6>
|
||||||
|
formLogin { }
|
||||||
|
}
|
||||||
|
// @formatter:on
|
||||||
|
return http.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
+133
@@ -0,0 +1,133 @@
|
|||||||
|
/*
|
||||||
|
* 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.validduration
|
||||||
|
|
||||||
|
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.authority.FactorGrantedAuthority
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority
|
||||||
|
import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener
|
||||||
|
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors
|
||||||
|
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.request.RequestPostProcessor
|
||||||
|
import org.springframework.test.web.servlet.result.MockMvcResultMatchers
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
import java.time.Duration
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests [CustomX509Configuration].
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
*/
|
||||||
|
@ExtendWith(SpringExtension::class, SpringTestContextExtension::class)
|
||||||
|
@TestExecutionListeners(WithSecurityContextTestExecutionListener::class)
|
||||||
|
class ValidDurationConfigurationTests {
|
||||||
|
@JvmField
|
||||||
|
val spring: SpringTestContext = SpringTestContext(this)
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
var mockMvc: MockMvc? = null
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun adminWhenExpiredThenRequired() {
|
||||||
|
this.spring.register(
|
||||||
|
ValidDurationConfiguration::class.java, Http200Controller::class.java
|
||||||
|
).autowire()
|
||||||
|
// @formatter:off
|
||||||
|
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/admin/").with(admin(Duration.ofMinutes(31))))
|
||||||
|
.andExpect(MockMvcResultMatchers.status().is3xxRedirection())
|
||||||
|
.andExpect(MockMvcResultMatchers.redirectedUrlPattern("http://localhost/login?*"))
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun adminWhenNotExpiredThenOk() {
|
||||||
|
this.spring.register(
|
||||||
|
ValidDurationConfiguration::class.java, Http200Controller::class.java
|
||||||
|
).autowire()
|
||||||
|
// @formatter:off
|
||||||
|
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/admin/").with(admin(Duration.ofMinutes(29))))
|
||||||
|
.andExpect(MockMvcResultMatchers.status().isOk())
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun settingsWhenExpiredThenRequired() {
|
||||||
|
this.spring.register(
|
||||||
|
ValidDurationConfiguration::class.java, Http200Controller::class.java
|
||||||
|
).autowire()
|
||||||
|
// @formatter:off
|
||||||
|
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/user/settings").with(user(Duration.ofMinutes(61))))
|
||||||
|
.andExpect(MockMvcResultMatchers.status().is3xxRedirection())
|
||||||
|
.andExpect(MockMvcResultMatchers.redirectedUrlPattern("http://localhost/login?*"))
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun settingsWhenNotExpiredThenOk() {
|
||||||
|
this.spring.register(
|
||||||
|
ValidDurationConfiguration::class.java, ValidDurationConfigurationTests.Http200Controller::class.java
|
||||||
|
).autowire()
|
||||||
|
// @formatter:off
|
||||||
|
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/user/settings").with(user(Duration.ofMinutes(59))))
|
||||||
|
.andExpect(MockMvcResultMatchers.status().isOk())
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun admin(sinceAuthn: Duration): RequestPostProcessor {
|
||||||
|
return authn("admin", sinceAuthn)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun user(sinceAuthn: Duration): RequestPostProcessor {
|
||||||
|
return authn("user", sinceAuthn)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun authn(username: String, sinceAuthn: Duration): RequestPostProcessor {
|
||||||
|
val issuedAt = Instant.now().minus(sinceAuthn)
|
||||||
|
val factor = FactorGrantedAuthority
|
||||||
|
.withAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY)
|
||||||
|
.issuedAt(issuedAt)
|
||||||
|
.build()
|
||||||
|
val role = username.uppercase(Locale.getDefault())
|
||||||
|
val authn = TestingAuthenticationToken(
|
||||||
|
username, "",
|
||||||
|
factor, SimpleGrantedAuthority("ROLE_" + role)
|
||||||
|
)
|
||||||
|
return SecurityMockMvcRequestPostProcessors.authentication(authn)
|
||||||
|
}
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
internal class Http200Controller {
|
||||||
|
@GetMapping("/**")
|
||||||
|
fun ok(): String {
|
||||||
|
return "ok"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user