7 changed files with 439 additions and 15 deletions
@ -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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
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 @@ |
|||||||
|
/* |
||||||
|
* 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