7 changed files with 439 additions and 15 deletions
@ -0,0 +1,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() |
||||
); |
||||
} |
||||
} |
||||
@ -0,0 +1,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"; |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,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") |
||||
} |
||||
} |
||||
@ -0,0 +1,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" |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue