From 857830f6958c229650e6a00d376af38c8bcddbad Mon Sep 17 00:00:00 2001
From: Ivan Pavlov <ivan.pavlov.95@gmail.com>
Date: Sat, 6 Feb 2021 02:40:02 +0300
Subject: [PATCH] Add RememberMeDsl

Issue: gh-9319
---
 .../config/web/servlet/HttpSecurityDsl.kt     |  27 +
 .../config/web/servlet/RememberMeDsl.kt       |  79 +++
 .../config/web/servlet/RememberMeDslTests.kt  | 564 ++++++++++++++++++
 3 files changed, 670 insertions(+)
 create mode 100644 config/src/main/kotlin/org/springframework/security/config/web/servlet/RememberMeDsl.kt
 create mode 100644 config/src/test/kotlin/org/springframework/security/config/web/servlet/RememberMeDslTests.kt

diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/HttpSecurityDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/HttpSecurityDsl.kt
index 137d459ce1..f368522abf 100644
--- a/config/src/main/kotlin/org/springframework/security/config/web/servlet/HttpSecurityDsl.kt
+++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/HttpSecurityDsl.kt
@@ -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.
      *
diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/RememberMeDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/RememberMeDsl.kt
new file mode 100644
index 0000000000..db69d5d4f6
--- /dev/null
+++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/RememberMeDsl.kt
@@ -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!!) }
+        }
+    }
+}
diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/RememberMeDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/RememberMeDslTests.kt
new file mode 100644
index 0000000000..38fa02c26a
--- /dev/null
+++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/RememberMeDslTests.kt
@@ -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
+                }
+            }
+        }
+    }
+
+}