Add RememberMeDsl

Issue: gh-9319
This commit is contained in:
Ivan Pavlov 2021-02-06 02:40:02 +03:00 committed by Eleftheria Stein-Kousathana
parent 987b19f23c
commit 857830f695
3 changed files with 670 additions and 0 deletions

View File

@ -644,6 +644,33 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu
this.http.oauth2ResourceServer(oauth2ResourceServerCustomizer)
}
/**
* Configures Remember Me authentication.
*
* Example:
*
* ```
* @EnableWebSecurity
* class SecurityConfig : WebSecurityConfigurerAdapter() {
*
* override fun configure(http: HttpSecurity) {
* http {
* rememberMe {
* tokenValiditySeconds = 604800
* }
* }
* }
* }
* ```
*
* @param rememberMeConfiguration custom configuration to configure remember me
* @see [RememberMeDsl]
*/
fun rememberMe(rememberMeConfiguration: RememberMeDsl.() -> Unit) {
val rememberMeCustomizer = RememberMeDsl().apply(rememberMeConfiguration).get()
this.http.rememberMe(rememberMeCustomizer)
}
/**
* Adds the [Filter] at the location of the specified [Filter] class.
*

View File

@ -0,0 +1,79 @@
/*
* Copyright 2002-2021 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.config.web.servlet
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configurers.RememberMeConfigurer
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.web.authentication.AuthenticationSuccessHandler
import org.springframework.security.web.authentication.RememberMeServices
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository
/**
* A Kotlin DSL to configure [HttpSecurity] Remember me using idiomatic Kotlin code.
*
* @author Ivan Pavlov
* @property authenticationSuccessHandler the [AuthenticationSuccessHandler] used after
* authentication success
* @property key the key to identify tokens
* @property rememberMeServices the [RememberMeServices] to use
* @property rememberMeParameter the HTTP parameter used to indicate to remember
* the user at time of login. Defaults to 'remember-me'
* @property rememberMeCookieName the name of cookie which store the token for
* remember me authentication. Defaults to 'remember-me'
* @property rememberMeCookieDomain the domain name within which the remember me cookie
* is visible
* @property tokenRepository the [PersistentTokenRepository] to use. Defaults to
* [org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices] instead
* @property userDetailsService the [UserDetailsService] used to look up the UserDetails
* when a remember me token is valid
* @property tokenValiditySeconds how long (in seconds) a token is valid for.
* Defaults to 2 weeks
* @property useSecureCookie whether the cookie should be flagged as secure or not
* @property alwaysRemember whether the cookie should always be created even if
* the remember-me parameter is not set. Defaults to `false`
*/
@SecurityMarker
class RememberMeDsl {
var authenticationSuccessHandler: AuthenticationSuccessHandler? = null
var key: String? = null
var rememberMeServices: RememberMeServices? = null
var rememberMeParameter: String? = null
var rememberMeCookieName: String? = null
var rememberMeCookieDomain: String? = null
var tokenRepository: PersistentTokenRepository? = null
var userDetailsService: UserDetailsService? = null
var tokenValiditySeconds: Int? = null
var useSecureCookie: Boolean? = null
var alwaysRemember: Boolean? = null
internal fun get(): (RememberMeConfigurer<HttpSecurity>) -> Unit {
return { rememberMe ->
authenticationSuccessHandler?.also { rememberMe.authenticationSuccessHandler(authenticationSuccessHandler) }
key?.also { rememberMe.key(key) }
rememberMeServices?.also { rememberMe.rememberMeServices(rememberMeServices) }
rememberMeParameter?.also { rememberMe.rememberMeParameter(rememberMeParameter) }
rememberMeCookieName?.also { rememberMe.rememberMeCookieName(rememberMeCookieName) }
rememberMeCookieDomain?.also { rememberMe.rememberMeCookieDomain(rememberMeCookieDomain) }
tokenRepository?.also { rememberMe.tokenRepository(tokenRepository) }
userDetailsService?.also { rememberMe.userDetailsService(userDetailsService) }
tokenValiditySeconds?.also { rememberMe.tokenValiditySeconds(tokenValiditySeconds!!) }
useSecureCookie?.also { rememberMe.useSecureCookie(useSecureCookie!!) }
alwaysRemember?.also { rememberMe.alwaysRemember(alwaysRemember!!) }
}
}
}

View File

@ -0,0 +1,564 @@
/*
* Copyright 2002-2021 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.config.web.servlet
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.jupiter.api.fail
import org.mockito.BDDMockito.given
import org.mockito.Mockito.*
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.core.annotation.Order
import org.springframework.mock.web.MockHttpSession
import org.springframework.security.authentication.RememberMeAuthenticationToken
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
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.configuration.WebSecurityConfigurerAdapter
import org.springframework.security.config.test.SpringTestRule
import org.springframework.security.core.Authentication
import org.springframework.security.core.authority.AuthorityUtils
import org.springframework.security.core.userdetails.PasswordEncodedUser
import org.springframework.security.core.userdetails.User
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf
import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers
import org.springframework.security.web.authentication.AuthenticationSuccessHandler
import org.springframework.security.web.authentication.RememberMeServices
import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices
import org.springframework.security.web.authentication.rememberme.PersistentRememberMeToken
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository
import org.springframework.security.web.util.matcher.AntPathRequestMatcher
import org.springframework.test.web.servlet.MockHttpServletRequestDsl
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.get
import org.springframework.test.web.servlet.post
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
/**
* Tests for [RememberMeDsl]
*
* @author Ivan Pavlov
*/
internal class RememberMeDslTests {
@Rule
@JvmField
val spring = SpringTestRule()
@Autowired
lateinit var mockMvc: MockMvc
@Test
fun `Remember Me login when remember me true then responds with remember me cookie`() {
this.spring.register(RememberMeConfig::class.java).autowire()
mockMvc.post("/login")
{
loginRememberMeRequest()
}.andExpect {
cookie {
exists("remember-me")
}
}
}
@Test
fun `Remember Me get when remember me cookie then authentication is remember me authentication token`() {
this.spring.register(RememberMeConfig::class.java).autowire()
val mvcResult = mockMvc.post("/login")
{
loginRememberMeRequest()
}.andReturn()
val rememberMeCookie = mvcResult.response.getCookie("remember-me")
?: fail { "Missing remember-me cookie in login response" }
mockMvc.get("/abc")
{
cookie(rememberMeCookie)
}.andExpect {
val rememberMeAuthentication = SecurityMockMvcResultMatchers.authenticated()
.withAuthentication { assertThat(it).isInstanceOf(RememberMeAuthenticationToken::class.java) }
match(rememberMeAuthentication)
}
}
@Test
fun `Remember Me logout when remember me cookie then authentication is remember me cookie expired`() {
this.spring.register(RememberMeConfig::class.java).autowire()
val mvcResult = mockMvc.post("/login")
{
loginRememberMeRequest()
}.andReturn()
val rememberMeCookie = mvcResult.response.getCookie("remember-me")
?: fail { "Missing remember-me cookie in login response" }
val mockSession = mvcResult.request.session as MockHttpSession
mockMvc.post("/logout")
{
with(csrf())
cookie(rememberMeCookie)
session = mockSession
}.andExpect {
status { isFound() }
redirectedUrl("/login?logout")
cookie {
maxAge("remember-me", 0)
}
}
}
@Test
fun `Remember Me get when remember me cookie and logged out then redirects to login`() {
this.spring.register(RememberMeConfig::class.java).autowire()
mockMvc.perform(formLogin())
val mvcResult = mockMvc.post("/login")
{
loginRememberMeRequest()
}.andReturn()
val rememberMeCookie = mvcResult.response.getCookie("remember-me")
?: fail { "Missing remember-me cookie in login request" }
val mockSession = mvcResult.request.session as MockHttpSession
val logoutMvcResult = mockMvc.post("/logout")
{
with(csrf())
cookie(rememberMeCookie)
session = mockSession
}.andReturn()
val expiredRememberMeCookie = logoutMvcResult.response.getCookie("remember-me")
?: fail { "Missing remember-me cookie in logout response" }
mockMvc.get("/abc")
{
with(csrf())
cookie(expiredRememberMeCookie)
}.andExpect {
status { isFound() }
redirectedUrl("http://localhost/login")
}
}
@Test
fun `Remember Me login when remember me domain then remember me cookie has domain`() {
this.spring.register(RememberMeDomainConfig::class.java).autowire()
mockMvc.post("/login")
{
loginRememberMeRequest()
}.andExpect {
cookie {
domain("remember-me", "spring.io")
}
}
}
@Test
fun `Remember Me when remember me services then uses`() {
RememberMeServicesRefConfig.REMEMBER_ME_SERVICES = mock(RememberMeServices::class.java)
this.spring.register(RememberMeServicesRefConfig::class.java).autowire()
mockMvc.get("/")
verify(RememberMeServicesRefConfig.REMEMBER_ME_SERVICES).autoLogin(any(HttpServletRequest::class.java),
any(HttpServletResponse::class.java))
mockMvc.post("/login") {
with(csrf())
}
verify(RememberMeServicesRefConfig.REMEMBER_ME_SERVICES).loginFail(any(HttpServletRequest::class.java),
any(HttpServletResponse::class.java))
mockMvc.post("/login") {
loginRememberMeRequest()
}
verify(RememberMeServicesRefConfig.REMEMBER_ME_SERVICES).loginSuccess(any(HttpServletRequest::class.java),
any(HttpServletResponse::class.java), any(Authentication::class.java))
}
@Test
fun `Remember Me when authentication success handler then uses`() {
RememberMeSuccessHandlerConfig.SUCCESS_HANDLER = mock(AuthenticationSuccessHandler::class.java)
this.spring.register(RememberMeSuccessHandlerConfig::class.java).autowire()
val mvcResult = mockMvc.post("/login") {
loginRememberMeRequest()
}.andReturn()
verifyNoInteractions(RememberMeSuccessHandlerConfig.SUCCESS_HANDLER)
val rememberMeCookie = mvcResult.response.getCookie("remember-me")
?: fail { "Missing remember-me cookie in login response" }
mockMvc.get("/abc") {
cookie(rememberMeCookie)
}
verify(RememberMeSuccessHandlerConfig.SUCCESS_HANDLER).onAuthenticationSuccess(
any(HttpServletRequest::class.java), any(HttpServletResponse::class.java),
any(Authentication::class.java))
}
@Test
fun `Remember Me when key then remember me works only for matching routes`() {
this.spring.register(WithoutKeyConfig::class.java, KeyConfig::class.java).autowire()
val withoutKeyMvcResult = mockMvc.post("/without-key/login") {
loginRememberMeRequest()
}.andReturn()
val withoutKeyRememberMeCookie = withoutKeyMvcResult.response.getCookie("remember-me")
?: fail { "Missing remember-me cookie in without key login response" }
mockMvc.get("/abc") {
cookie(withoutKeyRememberMeCookie)
}.andExpect {
status { isFound() }
redirectedUrl("http://localhost/login")
}
val keyMvcResult = mockMvc.post("/login") {
loginRememberMeRequest()
}.andReturn()
val keyRememberMeCookie = keyMvcResult.response.getCookie("remember-me")
?: fail { "Missing remember-me cookie in key login response" }
mockMvc.get("/abc") {
cookie(keyRememberMeCookie)
}.andExpect {
status { isNotFound() }
}
}
@Test
fun `Remember Me when token repository then uses`() {
RememberMeTokenRepositoryConfig.TOKEN_REPOSITORY = mock(PersistentTokenRepository::class.java)
this.spring.register(RememberMeTokenRepositoryConfig::class.java).autowire()
mockMvc.post("/login") {
loginRememberMeRequest()
}
verify(RememberMeTokenRepositoryConfig.TOKEN_REPOSITORY).createNewToken(
any(PersistentRememberMeToken::class.java))
}
@Test
fun `Remember Me when token validity seconds then cookie max age`() {
this.spring.register(RememberMeTokenValidityConfig::class.java).autowire()
mockMvc.post("/login") {
loginRememberMeRequest()
}.andExpect {
cookie {
maxAge("remember-me", 42)
}
}
}
@Test
fun `Remember Me when using defaults then cookie max age`() {
this.spring.register(RememberMeConfig::class.java).autowire()
mockMvc.post("/login") {
loginRememberMeRequest()
}.andExpect {
cookie {
maxAge("remember-me", AbstractRememberMeServices.TWO_WEEKS_S)
}
}
}
@Test
fun `Remember Me when use secure cookie then cookie secure`() {
this.spring.register(RememberMeUseSecureCookieConfig::class.java).autowire()
mockMvc.post("/login") {
loginRememberMeRequest()
}.andExpect {
cookie {
secure("remember-me", true)
}
}
}
@Test
fun `Remember Me when using defaults then cookie secure`() {
this.spring.register(RememberMeConfig::class.java).autowire()
mockMvc.post("/login") {
loginRememberMeRequest()
secure = true
}.andExpect {
cookie {
secure("remember-me", true)
}
}
}
@Test
fun `Remember Me when parameter then responds with remember me cookie`() {
this.spring.register(RememberMeParameterConfig::class.java).autowire()
mockMvc.post("/login") {
loginRememberMeRequest("rememberMe")
}.andExpect {
cookie {
exists("remember-me")
}
}
}
@Test
fun `Remember Me when cookie name then responds with remember me cookie with such name`() {
this.spring.register(RememberMeCookieNameConfig::class.java).autowire()
mockMvc.post("/login") {
loginRememberMeRequest()
}.andExpect {
cookie {
exists("rememberMe")
}
}
}
@Test
fun `Remember Me when global user details service then uses`() {
RememberMeDefaultUserDetailsServiceConfig.USER_DETAIL_SERVICE = mock(UserDetailsService::class.java)
this.spring.register(RememberMeDefaultUserDetailsServiceConfig::class.java).autowire()
mockMvc.post("/login") {
loginRememberMeRequest()
}
verify(RememberMeDefaultUserDetailsServiceConfig.USER_DETAIL_SERVICE).loadUserByUsername("user")
}
@Test
fun `Remember Me when user details service then uses`() {
RememberMeUserDetailsServiceConfig.USER_DETAIL_SERVICE = mock(UserDetailsService::class.java)
this.spring.register(RememberMeUserDetailsServiceConfig::class.java).autowire()
val user = User("user", "password", AuthorityUtils.createAuthorityList("ROLE_USER"))
given(RememberMeUserDetailsServiceConfig.USER_DETAIL_SERVICE.loadUserByUsername("user")).willReturn(user)
mockMvc.post("/login") {
loginRememberMeRequest()
}
verify(RememberMeUserDetailsServiceConfig.USER_DETAIL_SERVICE).loadUserByUsername("user")
}
@Test
fun `Remember Me when always remember then remembers without HTTP parameter`() {
this.spring.register(RememberMeAlwaysRememberConfig::class.java).autowire()
mockMvc.post("/login") {
loginRememberMeRequest(rememberMeValue = null)
}.andExpect {
cookie {
exists("remember-me")
}
}
}
private fun MockHttpServletRequestDsl.loginRememberMeRequest(rememberMeParameter: String = "remember-me",
rememberMeValue: Boolean? = true) {
with(csrf())
param("username", "user")
param("password", "password")
rememberMeValue?.also {
param(rememberMeParameter, rememberMeValue.toString())
}
}
abstract class DefaultUserConfig : WebSecurityConfigurerAdapter() {
@Autowired
open fun configureGlobal(auth: AuthenticationManagerBuilder) {
auth.inMemoryAuthentication()
.withUser(PasswordEncodedUser.user())
}
}
@EnableWebSecurity
open class RememberMeConfig : DefaultUserConfig() {
override fun configure(http: HttpSecurity) {
http {
authorizeRequests {
authorize(anyRequest, hasRole("USER"))
}
formLogin {}
rememberMe {}
}
}
}
@EnableWebSecurity
open class RememberMeDomainConfig : DefaultUserConfig() {
override fun configure(http: HttpSecurity) {
http {
authorizeRequests {
authorize(anyRequest, hasRole("USER"))
}
formLogin {}
rememberMe {
rememberMeCookieDomain = "spring.io"
}
}
}
}
@EnableWebSecurity
open class RememberMeServicesRefConfig : DefaultUserConfig() {
override fun configure(http: HttpSecurity) {
http {
formLogin {}
rememberMe {
rememberMeServices = REMEMBER_ME_SERVICES
}
}
}
companion object {
lateinit var REMEMBER_ME_SERVICES: RememberMeServices
}
}
@EnableWebSecurity
open class RememberMeSuccessHandlerConfig : DefaultUserConfig() {
override fun configure(http: HttpSecurity) {
http {
formLogin {}
rememberMe {
authenticationSuccessHandler = SUCCESS_HANDLER
}
}
}
companion object {
lateinit var SUCCESS_HANDLER: AuthenticationSuccessHandler
}
}
@EnableWebSecurity
@Order(0)
open class WithoutKeyConfig : DefaultUserConfig() {
override fun configure(http: HttpSecurity) {
http {
securityMatcher(AntPathRequestMatcher("/without-key/**"))
formLogin {
loginProcessingUrl = "/without-key/login"
}
rememberMe {}
}
}
}
@EnableWebSecurity
open class KeyConfig : DefaultUserConfig() {
override fun configure(http: HttpSecurity) {
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
formLogin {}
rememberMe {
key = "RememberMeKey"
}
}
}
}
@EnableWebSecurity
open class RememberMeTokenRepositoryConfig : DefaultUserConfig() {
override fun configure(http: HttpSecurity) {
http {
formLogin {}
rememberMe {
tokenRepository = TOKEN_REPOSITORY
}
}
}
companion object {
lateinit var TOKEN_REPOSITORY: PersistentTokenRepository
}
}
@EnableWebSecurity
open class RememberMeTokenValidityConfig : DefaultUserConfig() {
override fun configure(http: HttpSecurity) {
http {
formLogin {}
rememberMe {
tokenValiditySeconds = 42
}
}
}
}
@EnableWebSecurity
open class RememberMeUseSecureCookieConfig : DefaultUserConfig() {
override fun configure(http: HttpSecurity) {
http {
formLogin {}
rememberMe {
useSecureCookie = true
}
}
}
}
@EnableWebSecurity
open class RememberMeParameterConfig : DefaultUserConfig() {
override fun configure(http: HttpSecurity) {
http {
formLogin {}
rememberMe {
rememberMeParameter = "rememberMe"
}
}
}
}
@EnableWebSecurity
open class RememberMeCookieNameConfig : DefaultUserConfig() {
override fun configure(http: HttpSecurity) {
http {
formLogin {}
rememberMe {
rememberMeCookieName = "rememberMe"
}
}
}
}
@EnableWebSecurity
open class RememberMeDefaultUserDetailsServiceConfig : DefaultUserConfig() {
override fun configure(http: HttpSecurity) {
http {
formLogin {}
rememberMe {}
}
}
override fun configure(auth: AuthenticationManagerBuilder) {
auth.userDetailsService(USER_DETAIL_SERVICE)
}
companion object {
lateinit var USER_DETAIL_SERVICE: UserDetailsService
}
}
@EnableWebSecurity
open class RememberMeUserDetailsServiceConfig : DefaultUserConfig() {
override fun configure(http: HttpSecurity) {
http {
formLogin {}
rememberMe {
userDetailsService = USER_DETAIL_SERVICE
}
}
}
companion object {
lateinit var USER_DETAIL_SERVICE: UserDetailsService
}
}
@EnableWebSecurity
open class RememberMeAlwaysRememberConfig : DefaultUserConfig() {
override fun configure(http: HttpSecurity) {
http {
formLogin {}
rememberMe {
alwaysRemember = true
}
}
}
}
}