Browse Source

Polish SessionLimit

- Move to the web.authentication.session package since it is only needed
by web.authentication.session elements and does not access any other web
element itself.
- Add Kotlin support
- Add documentation

Issue gh-16206
pull/16201/head
Josh Cummings 1 year ago
parent
commit
1104b45832
No known key found for this signature in database
GPG Key ID: A306A51F43B8E5A5
  1. 2
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java
  2. 11
      config/src/main/kotlin/org/springframework/security/config/annotation/web/session/SessionConcurrencyDsl.kt
  3. 2
      config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java
  4. 2
      config/src/test/java/org/springframework/security/config/http/HttpHeadersConfigTests.java
  5. 69
      config/src/test/kotlin/org/springframework/security/config/annotation/web/session/SessionConcurrencyDslTests.kt
  6. 57
      docs/modules/ROOT/pages/servlet/authentication/session-management.adoc
  7. 1
      web/src/main/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategy.java
  8. 2
      web/src/main/java/org/springframework/security/web/authentication/session/SessionLimit.java
  9. 1
      web/src/test/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategyTests.java
  10. 2
      web/src/test/java/org/springframework/security/web/authentication/session/SessionLimitTests.java

2
config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java

@ -47,6 +47,7 @@ import org.springframework.security.web.authentication.session.NullAuthenticated @@ -47,6 +47,7 @@ import org.springframework.security.web.authentication.session.NullAuthenticated
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy;
import org.springframework.security.web.authentication.session.SessionLimit;
import org.springframework.security.web.context.DelegatingSecurityContextRepository;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.NullSecurityContextRepository;
@ -59,7 +60,6 @@ import org.springframework.security.web.session.DisableEncodeUrlFilter; @@ -59,7 +60,6 @@ import org.springframework.security.web.session.DisableEncodeUrlFilter;
import org.springframework.security.web.session.ForceEagerSessionCreationFilter;
import org.springframework.security.web.session.InvalidSessionStrategy;
import org.springframework.security.web.session.SessionInformationExpiredStrategy;
import org.springframework.security.web.session.SessionLimit;
import org.springframework.security.web.session.SessionManagementFilter;
import org.springframework.security.web.session.SimpleRedirectInvalidSessionStrategy;
import org.springframework.security.web.session.SimpleRedirectSessionInformationExpiredStrategy;

11
config/src/main/kotlin/org/springframework/security/config/annotation/web/session/SessionConcurrencyDsl.kt

@ -19,7 +19,9 @@ package org.springframework.security.config.annotation.web.session @@ -19,7 +19,9 @@ package org.springframework.security.config.annotation.web.session
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer
import org.springframework.security.core.session.SessionRegistry
import org.springframework.security.web.authentication.session.SessionLimit
import org.springframework.security.web.session.SessionInformationExpiredStrategy
import org.springframework.util.Assert
/**
* A Kotlin DSL to configure the behaviour of multiple sessions using idiomatic
@ -44,12 +46,21 @@ class SessionConcurrencyDsl { @@ -44,12 +46,21 @@ class SessionConcurrencyDsl {
var expiredSessionStrategy: SessionInformationExpiredStrategy? = null
var maxSessionsPreventsLogin: Boolean? = null
var sessionRegistry: SessionRegistry? = null
private var sessionLimit: SessionLimit? = null
fun maximumSessions(max: SessionLimit) {
this.sessionLimit = max
}
internal fun get(): (SessionManagementConfigurer<HttpSecurity>.ConcurrencyControlConfigurer) -> Unit {
Assert.isTrue(maximumSessions == null || sessionLimit == null, "You cannot specify maximumSessions as both an Int and a SessionLimit. Please use only one.")
return { sessionConcurrencyControl ->
maximumSessions?.also {
sessionConcurrencyControl.maximumSessions(maximumSessions!!)
}
sessionLimit?.also {
sessionConcurrencyControl.maximumSessions(sessionLimit!!)
}
expiredUrl?.also {
sessionConcurrencyControl.expiredUrl(expiredUrl)
}

2
config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java

@ -59,12 +59,12 @@ import org.springframework.security.web.authentication.session.ChangeSessionIdAu @@ -59,12 +59,12 @@ import org.springframework.security.web.authentication.session.ChangeSessionIdAu
import org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionLimit;
import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.session.ConcurrentSessionFilter;
import org.springframework.security.web.session.HttpSessionDestroyedEvent;
import org.springframework.security.web.session.SessionLimit;
import org.springframework.security.web.session.SessionManagementFilter;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;

2
config/src/test/java/org/springframework/security/config/http/HttpHeadersConfigTests.java

@ -35,7 +35,7 @@ import org.springframework.beans.factory.xml.XmlBeanDefinitionStoreException; @@ -35,7 +35,7 @@ import org.springframework.beans.factory.xml.XmlBeanDefinitionStoreException;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.session.SessionLimit;
import org.springframework.security.web.authentication.session.SessionLimit;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultMatcher;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;

69
config/src/test/kotlin/org/springframework/security/config/annotation/web/session/SessionConcurrencyDslTests.kt

@ -18,18 +18,19 @@ package org.springframework.security.config.annotation.web.session @@ -18,18 +18,19 @@ package org.springframework.security.config.annotation.web.session
import io.mockk.every
import io.mockk.mockkObject
import java.util.Date
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.mock.web.MockHttpSession
import org.springframework.security.authorization.AuthorityAuthorizationManager
import org.springframework.security.authorization.AuthorizationManager
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.config.test.SpringTestContext
import org.springframework.security.config.test.SpringTestContextExtension
import org.springframework.security.config.annotation.web.invoke
import org.springframework.security.core.session.SessionInformation
import org.springframework.security.core.session.SessionRegistry
import org.springframework.security.core.session.SessionRegistryImpl
@ -44,6 +45,7 @@ import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get @@ -44,6 +45,7 @@ import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import java.util.*
/**
* Tests for [SessionConcurrencyDsl]
@ -173,16 +175,75 @@ class SessionConcurrencyDslTests { @@ -173,16 +175,75 @@ class SessionConcurrencyDslTests {
open fun sessionRegistry(): SessionRegistry = SESSION_REGISTRY
}
@Test
fun `session concurrency when session limit then no more sessions allowed`() {
this.spring.register(MaximumSessionsFunctionConfig::class.java, UserDetailsConfig::class.java).autowire()
this.mockMvc.perform(post("/login")
.with(csrf())
.param("username", "user")
.param("password", "password"))
this.mockMvc.perform(post("/login")
.with(csrf())
.param("username", "user")
.param("password", "password"))
.andExpect(status().isFound)
.andExpect(redirectedUrl("/login?error"))
this.mockMvc.perform(post("/login")
.with(csrf())
.param("username", "admin")
.param("password", "password"))
.andExpect(status().isFound)
.andExpect(redirectedUrl("/"))
this.mockMvc.perform(post("/login")
.with(csrf())
.param("username", "admin")
.param("password", "password"))
.andExpect(status().isFound)
.andExpect(redirectedUrl("/"))
}
@Configuration
@EnableWebSecurity
open class MaximumSessionsFunctionConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
val isAdmin: AuthorizationManager<Any> = AuthorityAuthorizationManager.hasRole("ADMIN")
http {
sessionManagement {
sessionConcurrency {
maximumSessions {
authentication -> if (isAdmin.authorize({ authentication }, null)!!.isGranted) -1 else 1
}
maxSessionsPreventsLogin = true
}
}
formLogin { }
}
return http.build()
}
}
@Configuration
open class UserDetailsConfig {
@Bean
open fun userDetailsService(): UserDetailsService {
val userDetails = User.withDefaultPasswordEncoder()
val user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build()
return InMemoryUserDetailsManager(userDetails)
val admin = User.withDefaultPasswordEncoder()
.username("admin")
.password("password")
.roles("ADMIN")
.build()
return InMemoryUserDetailsManager(user, admin)
}
}
}

57
docs/modules/ROOT/pages/servlet/authentication/session-management.adoc

@ -399,7 +399,62 @@ XML:: @@ -399,7 +399,62 @@ XML::
This will prevent a user from logging in multiple times - a second login will cause the first to be invalidated.
Using Spring Boot, you can test the above configuration scenario the following way:
You can also adjust this based on who the user is.
For example, administrators may be able to have more than one session:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
AuthorizationManager<?> isAdmin = AuthorityAuthorizationManager.hasRole("ADMIN");
http
.sessionManagement(session -> session
.maximumSessions((authentication) -> isAdmin.authorize(() -> authentication, null).isGranted() ? -1 : 1)
);
return http.build();
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
val isAdmin: AuthorizationManager<*> = AuthorityAuthorizationManager.hasRole("ADMIN")
http {
sessionManagement {
sessionConcurrency {
maximumSessions {
authentication -> if (isAdmin.authorize({ authentication }, null)!!.isGranted) -1 else 1
}
}
}
}
return http.build()
}
----
XML::
+
[source,xml,role="secondary"]
----
<http>
...
<session-management>
<concurrency-control max-sessions-ref="sessionLimit" />
</session-management>
</http>
<b:bean id="sessionLimit" class="my.SessionLimitImplementation"/>
----
======
Using Spring Boot, you can test the above configurations in the following way:
[tabs]
======

1
web/src/main/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategy.java

@ -33,7 +33,6 @@ import org.springframework.security.core.session.SessionRegistry; @@ -33,7 +33,6 @@ import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.session.ConcurrentSessionFilter;
import org.springframework.security.web.session.SessionLimit;
import org.springframework.security.web.session.SessionManagementFilter;
import org.springframework.util.Assert;

2
web/src/main/java/org/springframework/security/web/session/SessionLimit.java → web/src/main/java/org/springframework/security/web/authentication/session/SessionLimit.java

@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.security.web.session;
package org.springframework.security.web.authentication.session;
import java.util.function.Function;

1
web/src/test/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategyTests.java

@ -34,7 +34,6 @@ import org.springframework.security.authentication.TestingAuthenticationToken; @@ -34,7 +34,6 @@ import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.web.session.SessionLimit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;

2
web/src/test/java/org/springframework/security/web/session/SessionLimitTests.java → web/src/test/java/org/springframework/security/web/authentication/session/SessionLimitTests.java

@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.security.web.session;
package org.springframework.security.web.authentication.session;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
Loading…
Cancel
Save