Skip to content

Support Determining Max Sessions by Authentication #16218

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
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;
Expand Down Expand Up @@ -123,7 +124,7 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>

private SessionRegistry sessionRegistry;

private Integer maximumSessions;
private SessionLimit sessionLimit;

private String expiredUrl;

Expand Down Expand Up @@ -329,7 +330,7 @@ public SessionManagementConfigurer<H> sessionFixation(
* @return the {@link SessionManagementConfigurer} for further customizations
*/
public ConcurrencyControlConfigurer maximumSessions(int maximumSessions) {
this.maximumSessions = maximumSessions;
this.sessionLimit = SessionLimit.of(maximumSessions);
this.propertiesThatRequireImplicitAuthentication.add("maximumSessions = " + maximumSessions);
return new ConcurrencyControlConfigurer();
}
Expand Down Expand Up @@ -570,7 +571,7 @@ private SessionAuthenticationStrategy getSessionAuthenticationStrategy(H http) {
SessionRegistry sessionRegistry = getSessionRegistry(http);
ConcurrentSessionControlAuthenticationStrategy concurrentSessionControlStrategy = new ConcurrentSessionControlAuthenticationStrategy(
sessionRegistry);
concurrentSessionControlStrategy.setMaximumSessions(this.maximumSessions);
concurrentSessionControlStrategy.setMaximumSessions(this.sessionLimit);
concurrentSessionControlStrategy.setExceptionIfMaximumExceeded(this.maxSessionsPreventsLogin);
concurrentSessionControlStrategy = postProcess(concurrentSessionControlStrategy);
RegisterSessionAuthenticationStrategy registerSessionStrategy = new RegisterSessionAuthenticationStrategy(
Expand Down Expand Up @@ -614,7 +615,7 @@ private void registerDelegateApplicationListener(H http, ApplicationListener<?>
* @return
*/
private boolean isConcurrentSessionControlEnabled() {
return this.maximumSessions != null;
return this.sessionLimit != null;
}

/**
Expand Down Expand Up @@ -706,7 +707,19 @@ private ConcurrencyControlConfigurer() {
* @return the {@link ConcurrencyControlConfigurer} for further customizations
*/
public ConcurrencyControlConfigurer maximumSessions(int maximumSessions) {
SessionManagementConfigurer.this.maximumSessions = maximumSessions;
SessionManagementConfigurer.this.sessionLimit = SessionLimit.of(maximumSessions);
return this;
}

/**
* Determines the behaviour when a session limit is detected.
* @param sessionLimit the {@link SessionLimit} to check the maximum number of
* sessions for a user
* @return the {@link ConcurrencyControlConfigurer} for further customizations
* @since 6.5
*/
public ConcurrencyControlConfigurer maximumSessions(SessionLimit sessionLimit) {
SessionManagementConfigurer.this.sessionLimit = sessionLimit;
return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ class HttpConfigurationBuilder {

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

private static final String ATT_MAX_SESSIONS_REF = "max-sessions-ref";

private static final String ATT_MAX_SESSIONS = "max-sessions";

private static final String ATT_SESSION_AUTH_ERROR_URL = "session-authentication-error-url";

private static final String ATT_SECURITY_CONTEXT_HOLDER_STRATEGY = "security-context-holder-strategy-ref";
Expand Down Expand Up @@ -485,10 +489,16 @@ else if (StringUtils.hasText(sessionAuthStratRef)) {
concurrentSessionStrategy.addConstructorArgValue(this.sessionRegistryRef);
String maxSessions = this.pc.getReaderContext()
.getEnvironment()
.resolvePlaceholders(sessionCtrlElt.getAttribute("max-sessions"));
.resolvePlaceholders(sessionCtrlElt.getAttribute(ATT_MAX_SESSIONS));
if (StringUtils.hasText(maxSessions)) {
concurrentSessionStrategy.addPropertyValue("maximumSessions", maxSessions);
}
String maxSessionsRef = this.pc.getReaderContext()
.getEnvironment()
.resolvePlaceholders(sessionCtrlElt.getAttribute(ATT_MAX_SESSIONS_REF));
if (StringUtils.hasText(maxSessionsRef)) {
concurrentSessionStrategy.addPropertyReference("maximumSessions", maxSessionsRef);
}
String exceptionIfMaximumExceeded = sessionCtrlElt.getAttribute("error-if-maximum-exceeded");
if (StringUtils.hasText(exceptionIfMaximumExceeded)) {
concurrentSessionStrategy.addPropertyValue("exceptionIfMaximumExceeded", exceptionIfMaximumExceeded);
Expand Down Expand Up @@ -591,6 +601,12 @@ private void createConcurrencyControlFilterAndSessionRegistry(Element element) {
.error("Cannot use 'expired-url' attribute and 'expired-session-strategy-ref'" + " attribute together.",
source);
}
String maxSessions = element.getAttribute(ATT_MAX_SESSIONS);
String maxSessionsRef = element.getAttribute(ATT_MAX_SESSIONS_REF);
if (StringUtils.hasText(maxSessions) && StringUtils.hasText(maxSessionsRef)) {
this.pc.getReaderContext()
.error("Cannot use 'max-sessions' attribute and 'max-sessions-ref' attribute together.", source);
}
if (StringUtils.hasText(expiryUrl)) {
BeanDefinitionBuilder expiredSessionBldr = BeanDefinitionBuilder
.rootBeanDefinition(SimpleRedirectSessionInformationExpiredStrategy.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,9 @@ concurrency-control =
concurrency-control.attlist &=
## 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.
attribute max-sessions {xsd:token}?
concurrency-control.attlist &=
## Allows injection of the SessionLimit instance used by the ConcurrentSessionControlAuthenticationStrategy
attribute max-sessions-ref {xsd:token}?
concurrency-control.attlist &=
## 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.
attribute expired-url {xsd:token}?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2688,6 +2688,13 @@
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="max-sessions-ref" type="xs:token">
<xs:annotation>
<xs:documentation>Allows injection of the SessionLimit instance used by the
ConcurrentSessionControlAuthenticationStrategy
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="expired-url" type="xs:token">
<xs:annotation>
<xs:documentation>The URL a user will be redirected to if they attempt to use a session which has been
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 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.
Expand Down Expand Up @@ -64,6 +64,7 @@
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;
Expand Down Expand Up @@ -249,6 +250,82 @@ public void loginWhenUserLoggedInAndMaxSessionsOneInLambdaThenLoginPrevented() t
// @formatter:on
}

@Test
public void loginWhenAdminUserLoggedInAndSessionLimitIsConfiguredThenLoginSuccessfully() throws Exception {
this.spring.register(ConcurrencyControlWithSessionLimitConfig.class).autowire();
// @formatter:off
MockHttpServletRequestBuilder requestBuilder = post("/login")
.with(csrf())
.param("username", "admin")
.param("password", "password");
HttpSession firstSession = this.mvc.perform(requestBuilder)
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/"))
.andReturn()
.getRequest()
.getSession(false);
assertThat(firstSession).isNotNull();
HttpSession secondSession = this.mvc.perform(requestBuilder)
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/"))
.andReturn()
.getRequest()
.getSession(false);
assertThat(secondSession).isNotNull();
// @formatter:on
assertThat(firstSession.getId()).isNotEqualTo(secondSession.getId());
}

@Test
public void loginWhenAdminUserLoggedInAndSessionLimitIsConfiguredThenLoginPrevented() throws Exception {
this.spring.register(ConcurrencyControlWithSessionLimitConfig.class).autowire();
// @formatter:off
MockHttpServletRequestBuilder requestBuilder = post("/login")
.with(csrf())
.param("username", "admin")
.param("password", "password");
HttpSession firstSession = this.mvc.perform(requestBuilder)
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/"))
.andReturn()
.getRequest()
.getSession(false);
assertThat(firstSession).isNotNull();
HttpSession secondSession = this.mvc.perform(requestBuilder)
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/"))
.andReturn()
.getRequest()
.getSession(false);
assertThat(secondSession).isNotNull();
assertThat(firstSession.getId()).isNotEqualTo(secondSession.getId());
this.mvc.perform(requestBuilder)
.andExpect(status().isFound())
.andExpect(redirectedUrl("/login?error"));
// @formatter:on
}

@Test
public void loginWhenUserLoggedInAndSessionLimitIsConfiguredThenLoginPrevented() throws Exception {
this.spring.register(ConcurrencyControlWithSessionLimitConfig.class).autowire();
// @formatter:off
MockHttpServletRequestBuilder requestBuilder = post("/login")
.with(csrf())
.param("username", "user")
.param("password", "password");
HttpSession firstSession = this.mvc.perform(requestBuilder)
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/"))
.andReturn()
.getRequest()
.getSession(false);
assertThat(firstSession).isNotNull();
this.mvc.perform(requestBuilder)
.andExpect(status().isFound())
.andExpect(redirectedUrl("/login?error"));
// @formatter:on
}

@Test
public void requestWhenSessionCreationPolicyStateLessInLambdaThenNoSessionCreated() throws Exception {
this.spring.register(SessionCreationPolicyStateLessInLambdaConfig.class).autowire();
Expand Down Expand Up @@ -625,6 +702,42 @@ UserDetailsService userDetailsService() {

}

@Configuration
@EnableWebSecurity
static class ConcurrencyControlWithSessionLimitConfig {

@Bean
SecurityFilterChain filterChain(HttpSecurity http, SessionLimit sessionLimit) throws Exception {
// @formatter:off
http
.formLogin(withDefaults())
.sessionManagement((sessionManagement) -> sessionManagement
.sessionConcurrency((sessionConcurrency) -> sessionConcurrency
.maximumSessions(sessionLimit)
.maxSessionsPreventsLogin(true)
)
);
// @formatter:on
return http.build();
}

@Bean
UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager(PasswordEncodedUser.admin(), PasswordEncodedUser.user());
}

@Bean
SessionLimit SessionLimit() {
return (authentication) -> {
if ("admin".equals(authentication.getName())) {
return 2;
}
return 1;
};
}

}

@Configuration
@EnableWebSecurity
static class SessionCreationPolicyStateLessInLambdaConfig {
Expand Down
Loading
Loading