Skip to content

Commit 1864577

Browse files
Claudenir Machadojzheaux
authored andcommitted
Address SessionLimitStrategy
Closes gh-16206
1 parent 6bc6946 commit 1864577

File tree

14 files changed

+627
-15
lines changed

14 files changed

+627
-15
lines changed

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

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
import org.springframework.security.web.session.ForceEagerSessionCreationFilter;
6060
import org.springframework.security.web.session.InvalidSessionStrategy;
6161
import org.springframework.security.web.session.SessionInformationExpiredStrategy;
62+
import org.springframework.security.web.session.SessionLimit;
6263
import org.springframework.security.web.session.SessionManagementFilter;
6364
import org.springframework.security.web.session.SimpleRedirectInvalidSessionStrategy;
6465
import org.springframework.security.web.session.SimpleRedirectSessionInformationExpiredStrategy;
@@ -123,7 +124,7 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
123124

124125
private SessionRegistry sessionRegistry;
125126

126-
private Integer maximumSessions;
127+
private SessionLimit sessionLimit;
127128

128129
private String expiredUrl;
129130

@@ -329,7 +330,7 @@ public SessionManagementConfigurer<H> sessionFixation(
329330
* @return the {@link SessionManagementConfigurer} for further customizations
330331
*/
331332
public ConcurrencyControlConfigurer maximumSessions(int maximumSessions) {
332-
this.maximumSessions = maximumSessions;
333+
this.sessionLimit = SessionLimit.of(maximumSessions);
333334
this.propertiesThatRequireImplicitAuthentication.add("maximumSessions = " + maximumSessions);
334335
return new ConcurrencyControlConfigurer();
335336
}
@@ -570,7 +571,7 @@ private SessionAuthenticationStrategy getSessionAuthenticationStrategy(H http) {
570571
SessionRegistry sessionRegistry = getSessionRegistry(http);
571572
ConcurrentSessionControlAuthenticationStrategy concurrentSessionControlStrategy = new ConcurrentSessionControlAuthenticationStrategy(
572573
sessionRegistry);
573-
concurrentSessionControlStrategy.setMaximumSessions(this.maximumSessions);
574+
concurrentSessionControlStrategy.setMaximumSessions(this.sessionLimit);
574575
concurrentSessionControlStrategy.setExceptionIfMaximumExceeded(this.maxSessionsPreventsLogin);
575576
concurrentSessionControlStrategy = postProcess(concurrentSessionControlStrategy);
576577
RegisterSessionAuthenticationStrategy registerSessionStrategy = new RegisterSessionAuthenticationStrategy(
@@ -614,7 +615,7 @@ private void registerDelegateApplicationListener(H http, ApplicationListener<?>
614615
* @return
615616
*/
616617
private boolean isConcurrentSessionControlEnabled() {
617-
return this.maximumSessions != null;
618+
return this.sessionLimit != null;
618619
}
619620

620621
/**
@@ -706,7 +707,19 @@ private ConcurrencyControlConfigurer() {
706707
* @return the {@link ConcurrencyControlConfigurer} for further customizations
707708
*/
708709
public ConcurrencyControlConfigurer maximumSessions(int maximumSessions) {
709-
SessionManagementConfigurer.this.maximumSessions = maximumSessions;
710+
SessionManagementConfigurer.this.sessionLimit = SessionLimit.of(maximumSessions);
711+
return this;
712+
}
713+
714+
/**
715+
* Determines the behaviour when a session limit is detected.
716+
* @param sessionLimit the {@link SessionLimit} to check the maximum number of
717+
* sessions for a user
718+
* @return the {@link ConcurrencyControlConfigurer} for further customizations
719+
* @since 6.5
720+
*/
721+
public ConcurrencyControlConfigurer maximumSessions(SessionLimit sessionLimit) {
722+
SessionManagementConfigurer.this.sessionLimit = sessionLimit;
710723
return this;
711724
}
712725

config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,10 @@ class HttpConfigurationBuilder {
122122

123123
private static final String ATT_SESSION_AUTH_STRATEGY_REF = "session-authentication-strategy-ref";
124124

125+
private static final String ATT_MAX_SESSIONS_REF = "max-sessions-ref";
126+
127+
private static final String ATT_MAX_SESSIONS = "max-sessions";
128+
125129
private static final String ATT_SESSION_AUTH_ERROR_URL = "session-authentication-error-url";
126130

127131
private static final String ATT_SECURITY_CONTEXT_HOLDER_STRATEGY = "security-context-holder-strategy-ref";
@@ -485,10 +489,16 @@ else if (StringUtils.hasText(sessionAuthStratRef)) {
485489
concurrentSessionStrategy.addConstructorArgValue(this.sessionRegistryRef);
486490
String maxSessions = this.pc.getReaderContext()
487491
.getEnvironment()
488-
.resolvePlaceholders(sessionCtrlElt.getAttribute("max-sessions"));
492+
.resolvePlaceholders(sessionCtrlElt.getAttribute(ATT_MAX_SESSIONS));
489493
if (StringUtils.hasText(maxSessions)) {
490494
concurrentSessionStrategy.addPropertyValue("maximumSessions", maxSessions);
491495
}
496+
String maxSessionsRef = this.pc.getReaderContext()
497+
.getEnvironment()
498+
.resolvePlaceholders(sessionCtrlElt.getAttribute(ATT_MAX_SESSIONS_REF));
499+
if (StringUtils.hasText(maxSessionsRef)) {
500+
concurrentSessionStrategy.addPropertyReference("maximumSessions", maxSessionsRef);
501+
}
492502
String exceptionIfMaximumExceeded = sessionCtrlElt.getAttribute("error-if-maximum-exceeded");
493503
if (StringUtils.hasText(exceptionIfMaximumExceeded)) {
494504
concurrentSessionStrategy.addPropertyValue("exceptionIfMaximumExceeded", exceptionIfMaximumExceeded);
@@ -591,6 +601,12 @@ private void createConcurrencyControlFilterAndSessionRegistry(Element element) {
591601
.error("Cannot use 'expired-url' attribute and 'expired-session-strategy-ref'" + " attribute together.",
592602
source);
593603
}
604+
String maxSessions = element.getAttribute(ATT_MAX_SESSIONS);
605+
String maxSessionsRef = element.getAttribute(ATT_MAX_SESSIONS_REF);
606+
if (StringUtils.hasText(maxSessions) && StringUtils.hasText(maxSessionsRef)) {
607+
this.pc.getReaderContext()
608+
.error("Cannot use 'max-sessions' attribute and 'max-sessions-ref' attribute together.", source);
609+
}
594610
if (StringUtils.hasText(expiryUrl)) {
595611
BeanDefinitionBuilder expiredSessionBldr = BeanDefinitionBuilder
596612
.rootBeanDefinition(SimpleRedirectSessionInformationExpiredStrategy.class);

config/src/main/resources/org/springframework/security/config/spring-security-6.5.rnc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -934,6 +934,9 @@ concurrency-control =
934934
concurrency-control.attlist &=
935935
## The maximum number of sessions a single authenticated user can have open at the same time. Defaults to "1". A negative value denotes unlimited sessions.
936936
attribute max-sessions {xsd:token}?
937+
concurrency-control.attlist &=
938+
## Allows injection of the SessionLimit instance used by the ConcurrentSessionControlAuthenticationStrategy
939+
attribute max-sessions-ref {xsd:token}?
937940
concurrency-control.attlist &=
938941
## The URL a user will be redirected to if they attempt to use a session which has been "expired" because they have logged in again.
939942
attribute expired-url {xsd:token}?

config/src/main/resources/org/springframework/security/config/spring-security-6.5.xsd

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2688,6 +2688,13 @@
26882688
</xs:documentation>
26892689
</xs:annotation>
26902690
</xs:attribute>
2691+
<xs:attribute name="max-sessions-ref" type="xs:token">
2692+
<xs:annotation>
2693+
<xs:documentation>Allows injection of the SessionLimit instance used by the
2694+
ConcurrentSessionControlAuthenticationStrategy
2695+
</xs:documentation>
2696+
</xs:annotation>
2697+
</xs:attribute>
26912698
<xs:attribute name="expired-url" type="xs:token">
26922699
<xs:annotation>
26932700
<xs:documentation>The URL a user will be redirected to if they attempt to use a session which has been

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

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -64,6 +64,7 @@
6464
import org.springframework.security.web.savedrequest.RequestCache;
6565
import org.springframework.security.web.session.ConcurrentSessionFilter;
6666
import org.springframework.security.web.session.HttpSessionDestroyedEvent;
67+
import org.springframework.security.web.session.SessionLimit;
6768
import org.springframework.security.web.session.SessionManagementFilter;
6869
import org.springframework.test.web.servlet.MockMvc;
6970
import org.springframework.test.web.servlet.MvcResult;
@@ -249,6 +250,82 @@ public void loginWhenUserLoggedInAndMaxSessionsOneInLambdaThenLoginPrevented() t
249250
// @formatter:on
250251
}
251252

253+
@Test
254+
public void loginWhenAdminUserLoggedInAndSessionLimitIsConfiguredThenLoginSuccessfully() throws Exception {
255+
this.spring.register(ConcurrencyControlWithSessionLimitConfig.class).autowire();
256+
// @formatter:off
257+
MockHttpServletRequestBuilder requestBuilder = post("/login")
258+
.with(csrf())
259+
.param("username", "admin")
260+
.param("password", "password");
261+
HttpSession firstSession = this.mvc.perform(requestBuilder)
262+
.andExpect(status().is3xxRedirection())
263+
.andExpect(redirectedUrl("/"))
264+
.andReturn()
265+
.getRequest()
266+
.getSession(false);
267+
assertThat(firstSession).isNotNull();
268+
HttpSession secondSession = this.mvc.perform(requestBuilder)
269+
.andExpect(status().is3xxRedirection())
270+
.andExpect(redirectedUrl("/"))
271+
.andReturn()
272+
.getRequest()
273+
.getSession(false);
274+
assertThat(secondSession).isNotNull();
275+
// @formatter:on
276+
assertThat(firstSession.getId()).isNotEqualTo(secondSession.getId());
277+
}
278+
279+
@Test
280+
public void loginWhenAdminUserLoggedInAndSessionLimitIsConfiguredThenLoginPrevented() throws Exception {
281+
this.spring.register(ConcurrencyControlWithSessionLimitConfig.class).autowire();
282+
// @formatter:off
283+
MockHttpServletRequestBuilder requestBuilder = post("/login")
284+
.with(csrf())
285+
.param("username", "admin")
286+
.param("password", "password");
287+
HttpSession firstSession = this.mvc.perform(requestBuilder)
288+
.andExpect(status().is3xxRedirection())
289+
.andExpect(redirectedUrl("/"))
290+
.andReturn()
291+
.getRequest()
292+
.getSession(false);
293+
assertThat(firstSession).isNotNull();
294+
HttpSession secondSession = this.mvc.perform(requestBuilder)
295+
.andExpect(status().is3xxRedirection())
296+
.andExpect(redirectedUrl("/"))
297+
.andReturn()
298+
.getRequest()
299+
.getSession(false);
300+
assertThat(secondSession).isNotNull();
301+
assertThat(firstSession.getId()).isNotEqualTo(secondSession.getId());
302+
this.mvc.perform(requestBuilder)
303+
.andExpect(status().isFound())
304+
.andExpect(redirectedUrl("/login?error"));
305+
// @formatter:on
306+
}
307+
308+
@Test
309+
public void loginWhenUserLoggedInAndSessionLimitIsConfiguredThenLoginPrevented() throws Exception {
310+
this.spring.register(ConcurrencyControlWithSessionLimitConfig.class).autowire();
311+
// @formatter:off
312+
MockHttpServletRequestBuilder requestBuilder = post("/login")
313+
.with(csrf())
314+
.param("username", "user")
315+
.param("password", "password");
316+
HttpSession firstSession = this.mvc.perform(requestBuilder)
317+
.andExpect(status().is3xxRedirection())
318+
.andExpect(redirectedUrl("/"))
319+
.andReturn()
320+
.getRequest()
321+
.getSession(false);
322+
assertThat(firstSession).isNotNull();
323+
this.mvc.perform(requestBuilder)
324+
.andExpect(status().isFound())
325+
.andExpect(redirectedUrl("/login?error"));
326+
// @formatter:on
327+
}
328+
252329
@Test
253330
public void requestWhenSessionCreationPolicyStateLessInLambdaThenNoSessionCreated() throws Exception {
254331
this.spring.register(SessionCreationPolicyStateLessInLambdaConfig.class).autowire();
@@ -625,6 +702,42 @@ UserDetailsService userDetailsService() {
625702

626703
}
627704

705+
@Configuration
706+
@EnableWebSecurity
707+
static class ConcurrencyControlWithSessionLimitConfig {
708+
709+
@Bean
710+
SecurityFilterChain filterChain(HttpSecurity http, SessionLimit sessionLimit) throws Exception {
711+
// @formatter:off
712+
http
713+
.formLogin(withDefaults())
714+
.sessionManagement((sessionManagement) -> sessionManagement
715+
.sessionConcurrency((sessionConcurrency) -> sessionConcurrency
716+
.maximumSessions(sessionLimit)
717+
.maxSessionsPreventsLogin(true)
718+
)
719+
);
720+
// @formatter:on
721+
return http.build();
722+
}
723+
724+
@Bean
725+
UserDetailsService userDetailsService() {
726+
return new InMemoryUserDetailsManager(PasswordEncodedUser.admin(), PasswordEncodedUser.user());
727+
}
728+
729+
@Bean
730+
SessionLimit SessionLimit() {
731+
return (authentication) -> {
732+
if ("admin".equals(authentication.getName())) {
733+
return 2;
734+
}
735+
return 1;
736+
};
737+
}
738+
739+
}
740+
628741
@Configuration
629742
@EnableWebSecurity
630743
static class SessionCreationPolicyStateLessInLambdaConfig {

0 commit comments

Comments
 (0)