parent
							
								
									6bc6946ad9
								
							
						
					
					
						commit
						1864577e98
					
				| 
						 | 
				
			
			@ -59,6 +59,7 @@ 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;
 | 
			
		||||
| 
						 | 
				
			
			@ -123,7 +124,7 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
 | 
			
		|||
 | 
			
		||||
	private SessionRegistry sessionRegistry;
 | 
			
		||||
 | 
			
		||||
	private Integer maximumSessions;
 | 
			
		||||
	private SessionLimit sessionLimit;
 | 
			
		||||
 | 
			
		||||
	private String expiredUrl;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -329,7 +330,7 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
 | 
			
		|||
	 * @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();
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -570,7 +571,7 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
 | 
			
		|||
			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(
 | 
			
		||||
| 
						 | 
				
			
			@ -614,7 +615,7 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
 | 
			
		|||
	 * @return
 | 
			
		||||
	 */
 | 
			
		||||
	private boolean isConcurrentSessionControlEnabled() {
 | 
			
		||||
		return this.maximumSessions != null;
 | 
			
		||||
		return this.sessionLimit != null;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
| 
						 | 
				
			
			@ -706,7 +707,19 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
 | 
			
		|||
		 * @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;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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";
 | 
			
		||||
| 
						 | 
				
			
			@ -485,10 +489,16 @@ class HttpConfigurationBuilder {
 | 
			
		|||
			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);
 | 
			
		||||
| 
						 | 
				
			
			@ -591,6 +601,12 @@ class HttpConfigurationBuilder {
 | 
			
		|||
				.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);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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}?
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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.
 | 
			
		||||
| 
						 | 
				
			
			@ -64,6 +64,7 @@ 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;
 | 
			
		||||
| 
						 | 
				
			
			@ -249,6 +250,82 @@ public class SessionManagementConfigurerTests {
 | 
			
		|||
		// @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();
 | 
			
		||||
| 
						 | 
				
			
			@ -625,6 +702,42 @@ public class SessionManagementConfigurerTests {
 | 
			
		|||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@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 {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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.
 | 
			
		||||
| 
						 | 
				
			
			@ -24,6 +24,7 @@ import java.util.Map;
 | 
			
		|||
import java.util.Set;
 | 
			
		||||
 | 
			
		||||
import com.google.common.collect.ImmutableMap;
 | 
			
		||||
import jakarta.servlet.http.HttpSession;
 | 
			
		||||
import org.junit.jupiter.api.Test;
 | 
			
		||||
import org.junit.jupiter.api.extension.ExtendWith;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -33,14 +34,21 @@ import org.springframework.beans.factory.parsing.BeanDefinitionParsingException;
 | 
			
		|||
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.test.web.servlet.MockMvc;
 | 
			
		||||
import org.springframework.test.web.servlet.ResultMatcher;
 | 
			
		||||
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
 | 
			
		||||
import org.springframework.web.bind.annotation.GetMapping;
 | 
			
		||||
import org.springframework.web.bind.annotation.RestController;
 | 
			
		||||
 | 
			
		||||
import static org.assertj.core.api.Assertions.assertThat;
 | 
			
		||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
 | 
			
		||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
 | 
			
		||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
 | 
			
		||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
 | 
			
		||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
 | 
			
		||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
 | 
			
		||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
| 
						 | 
				
			
			@ -49,6 +57,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
 | 
			
		|||
 * @author Josh Cummings
 | 
			
		||||
 * @author Rafiullah Hamedy
 | 
			
		||||
 * @author Marcus Da Coregio
 | 
			
		||||
 * @author Claudenir Freitas
 | 
			
		||||
 */
 | 
			
		||||
@ExtendWith(SpringTestContextExtension.class)
 | 
			
		||||
public class HttpHeadersConfigTests {
 | 
			
		||||
| 
						 | 
				
			
			@ -782,6 +791,120 @@ public class HttpHeadersConfigTests {
 | 
			
		|||
		// @formatter:on
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void requestWhenSessionManagementConcurrencyControlMaxSessionIsOne() throws Exception {
 | 
			
		||||
		System.setProperty("security.session-management.concurrency-control.max-sessions", "1");
 | 
			
		||||
		this.spring.configLocations(this.xml("DefaultsSessionManagementConcurrencyControlMaxSessions")).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);
 | 
			
		||||
		// @formatter:on
 | 
			
		||||
		assertThat(firstSession).isNotNull();
 | 
			
		||||
		// @formatter:off
 | 
			
		||||
		this.mvc.perform(requestBuilder)
 | 
			
		||||
				.andExpect(status().isFound())
 | 
			
		||||
				.andExpect(redirectedUrl("/login?error"));
 | 
			
		||||
		// @formatter:on
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void requestWhenSessionManagementConcurrencyControlMaxSessionIsUnlimited() throws Exception {
 | 
			
		||||
		System.setProperty("security.session-management.concurrency-control.max-sessions", "-1");
 | 
			
		||||
		this.spring.configLocations(this.xml("DefaultsSessionManagementConcurrencyControlMaxSessions")).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();
 | 
			
		||||
		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 requestWhenSessionManagementConcurrencyControlMaxSessionRefIsOneForNonAdminUsers() throws Exception {
 | 
			
		||||
		this.spring.configLocations(this.xml("DefaultsSessionManagementConcurrencyControlMaxSessionsRef")).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);
 | 
			
		||||
		// @formatter:on
 | 
			
		||||
		assertThat(firstSession).isNotNull();
 | 
			
		||||
		// @formatter:off
 | 
			
		||||
		this.mvc.perform(requestBuilder)
 | 
			
		||||
				.andExpect(status().isFound())
 | 
			
		||||
				.andExpect(redirectedUrl("/login?error"));
 | 
			
		||||
		// @formatter:on
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void requestWhenSessionManagementConcurrencyControlMaxSessionRefIsTwoForAdminUsers() throws Exception {
 | 
			
		||||
		this.spring.configLocations(this.xml("DefaultsSessionManagementConcurrencyControlMaxSessionsRef")).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());
 | 
			
		||||
		// @formatter:off
 | 
			
		||||
		this.mvc.perform(requestBuilder)
 | 
			
		||||
				.andExpect(status().isFound())
 | 
			
		||||
				.andExpect(redirectedUrl("/login?error"));
 | 
			
		||||
		// @formatter:on
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void requestWhenSessionManagementConcurrencyControlWithInvalidMaxSessionConfig() {
 | 
			
		||||
		assertThatExceptionOfType(BeanDefinitionParsingException.class)
 | 
			
		||||
			.isThrownBy(() -> this.spring
 | 
			
		||||
				.configLocations(this.xml("DefaultsSessionManagementConcurrencyControlWithInvalidMaxSessionsConfig"))
 | 
			
		||||
				.autowire())
 | 
			
		||||
			.withMessageContaining("Cannot use 'max-sessions' attribute and 'max-sessions-ref' attribute together.");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private static ResultMatcher includesDefaults() {
 | 
			
		||||
		return includes(defaultHeaders);
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -832,4 +955,16 @@ public class HttpHeadersConfigTests {
 | 
			
		|||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public static class CustomSessionLimit implements SessionLimit {
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		public Integer apply(Authentication authentication) {
 | 
			
		||||
			if ("admin".equals(authentication.getName())) {
 | 
			
		||||
				return 2;
 | 
			
		||||
			}
 | 
			
		||||
			return 1;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,38 @@
 | 
			
		|||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<!--
 | 
			
		||||
  ~ 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.
 | 
			
		||||
  ~ 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.
 | 
			
		||||
  -->
 | 
			
		||||
 | 
			
		||||
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
 | 
			
		||||
		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 | 
			
		||||
		xmlns="http://www.springframework.org/schema/security"
 | 
			
		||||
		xsi:schemaLocation="
 | 
			
		||||
			http://www.springframework.org/schema/security
 | 
			
		||||
			https://www.springframework.org/schema/security/spring-security.xsd
 | 
			
		||||
			http://www.springframework.org/schema/beans
 | 
			
		||||
			https://www.springframework.org/schema/beans/spring-beans.xsd">
 | 
			
		||||
 | 
			
		||||
	<http auto-config="true">
 | 
			
		||||
		<session-management>
 | 
			
		||||
			<concurrency-control max-sessions="${security.session-management.concurrency-control.max-sessions}"
 | 
			
		||||
								 error-if-maximum-exceeded="true"/>
 | 
			
		||||
		</session-management>
 | 
			
		||||
		<intercept-url pattern="/**" access="permitAll"/>
 | 
			
		||||
	</http>
 | 
			
		||||
 | 
			
		||||
	<b:bean name="simple" class="org.springframework.security.config.http.HttpHeadersConfigTests.SimpleController"/>
 | 
			
		||||
 | 
			
		||||
	<b:import resource="userservice.xml"/>
 | 
			
		||||
</b:beans>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,41 @@
 | 
			
		|||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<!--
 | 
			
		||||
  ~ 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.
 | 
			
		||||
  ~ 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.
 | 
			
		||||
  -->
 | 
			
		||||
 | 
			
		||||
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
 | 
			
		||||
		 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 | 
			
		||||
		 xmlns="http://www.springframework.org/schema/security"
 | 
			
		||||
		 xsi:schemaLocation="
 | 
			
		||||
			http://www.springframework.org/schema/security
 | 
			
		||||
			https://www.springframework.org/schema/security/spring-security.xsd
 | 
			
		||||
			http://www.springframework.org/schema/beans
 | 
			
		||||
			https://www.springframework.org/schema/beans/spring-beans.xsd">
 | 
			
		||||
 | 
			
		||||
	<http auto-config="true">
 | 
			
		||||
		<session-management>
 | 
			
		||||
			<concurrency-control max-sessions-ref="customSessionLimit"
 | 
			
		||||
								 error-if-maximum-exceeded="true"/>
 | 
			
		||||
		</session-management>
 | 
			
		||||
		<intercept-url pattern="/**" access="permitAll"/>
 | 
			
		||||
	</http>
 | 
			
		||||
 | 
			
		||||
	<b:bean name="simple" class="org.springframework.security.config.http.HttpHeadersConfigTests.SimpleController"/>
 | 
			
		||||
 | 
			
		||||
	<b:bean name="customSessionLimit"
 | 
			
		||||
			class="org.springframework.security.config.http.HttpHeadersConfigTests.CustomSessionLimit"/>
 | 
			
		||||
 | 
			
		||||
	<b:import resource="userservice.xml"/>
 | 
			
		||||
</b:beans>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,42 @@
 | 
			
		|||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<!--
 | 
			
		||||
  ~ 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.
 | 
			
		||||
  ~ 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.
 | 
			
		||||
  -->
 | 
			
		||||
 | 
			
		||||
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
 | 
			
		||||
		 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 | 
			
		||||
		 xmlns="http://www.springframework.org/schema/security"
 | 
			
		||||
		 xsi:schemaLocation="
 | 
			
		||||
			http://www.springframework.org/schema/security
 | 
			
		||||
			https://www.springframework.org/schema/security/spring-security.xsd
 | 
			
		||||
			http://www.springframework.org/schema/beans
 | 
			
		||||
			https://www.springframework.org/schema/beans/spring-beans.xsd">
 | 
			
		||||
 | 
			
		||||
	<http auto-config="true">
 | 
			
		||||
		<session-management>
 | 
			
		||||
			<concurrency-control max-sessions="1"
 | 
			
		||||
								 max-sessions-ref="customSessionLimit"
 | 
			
		||||
								 error-if-maximum-exceeded="true"/>
 | 
			
		||||
		</session-management>
 | 
			
		||||
		<intercept-url pattern="/**" access="permitAll"/>
 | 
			
		||||
	</http>
 | 
			
		||||
 | 
			
		||||
	<b:bean name="simple" class="org.springframework.security.config.http.HttpHeadersConfigTests.SimpleController"/>
 | 
			
		||||
 | 
			
		||||
	<b:bean name="customSessionLimit"
 | 
			
		||||
			class="org.springframework.security.config.http.HttpHeadersConfigTests.CustomSessionLimit"/>
 | 
			
		||||
 | 
			
		||||
	<b:import resource="userservice.xml"/>
 | 
			
		||||
</b:beans>
 | 
			
		||||
| 
						 | 
				
			
			@ -2168,6 +2168,9 @@ Allows injection of the ExpiredSessionStrategy instance used by the ConcurrentSe
 | 
			
		|||
Maps to the `maximumSessions` property of `ConcurrentSessionControlAuthenticationStrategy`.
 | 
			
		||||
Specify `-1` as the value to support unlimited sessions.
 | 
			
		||||
 | 
			
		||||
[[nsa-concurrency-control-max-sessions-ref]]
 | 
			
		||||
* **max-sessions-ref**
 | 
			
		||||
Allows injection of the SessionLimit instance used by the ConcurrentSessionControlAuthenticationStrategy
 | 
			
		||||
 | 
			
		||||
[[nsa-concurrency-control-session-registry-alias]]
 | 
			
		||||
* **session-registry-alias**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -33,6 +33,7 @@ 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;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -76,7 +77,7 @@ public class ConcurrentSessionControlAuthenticationStrategy
 | 
			
		|||
 | 
			
		||||
	private boolean exceptionIfMaximumExceeded = false;
 | 
			
		||||
 | 
			
		||||
	private int maximumSessions = 1;
 | 
			
		||||
	private SessionLimit sessionLimit = SessionLimit.of(1);
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @param sessionRegistry the session registry which should be updated when the
 | 
			
		||||
| 
						 | 
				
			
			@ -130,7 +131,7 @@ public class ConcurrentSessionControlAuthenticationStrategy
 | 
			
		|||
	 * @return either -1 meaning unlimited, or a positive integer to limit (never zero)
 | 
			
		||||
	 */
 | 
			
		||||
	protected int getMaximumSessionsForThisUser(Authentication authentication) {
 | 
			
		||||
		return this.maximumSessions;
 | 
			
		||||
		return this.sessionLimit.apply(authentication);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
| 
						 | 
				
			
			@ -172,15 +173,24 @@ public class ConcurrentSessionControlAuthenticationStrategy
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Sets the <tt>maxSessions</tt> property. The default value is 1. Use -1 for
 | 
			
		||||
	 * Sets the <tt>sessionLimit</tt> property. The default value is 1. Use -1 for
 | 
			
		||||
	 * unlimited sessions.
 | 
			
		||||
	 * @param maximumSessions the maximum number of permitted sessions a user can have
 | 
			
		||||
	 * open simultaneously.
 | 
			
		||||
	 */
 | 
			
		||||
	public void setMaximumSessions(int maximumSessions) {
 | 
			
		||||
		Assert.isTrue(maximumSessions != 0,
 | 
			
		||||
				"MaximumLogins must be either -1 to allow unlimited logins, or a positive integer to specify a maximum");
 | 
			
		||||
		this.maximumSessions = maximumSessions;
 | 
			
		||||
		this.sessionLimit = SessionLimit.of(maximumSessions);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Sets the <tt>sessionLimit</tt> property. The default value is 1. Use -1 for
 | 
			
		||||
	 * unlimited sessions.
 | 
			
		||||
	 * @param sessionLimit the session limit strategy
 | 
			
		||||
	 * @since 6.5
 | 
			
		||||
	 */
 | 
			
		||||
	public void setMaximumSessions(SessionLimit sessionLimit) {
 | 
			
		||||
		Assert.notNull(sessionLimit, "sessionLimit cannot be null");
 | 
			
		||||
		this.sessionLimit = sessionLimit;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,49 @@
 | 
			
		|||
/*
 | 
			
		||||
 * Copyright 2015-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.
 | 
			
		||||
 * 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.web.session;
 | 
			
		||||
 | 
			
		||||
import java.util.function.Function;
 | 
			
		||||
 | 
			
		||||
import org.springframework.security.core.Authentication;
 | 
			
		||||
import org.springframework.util.Assert;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Represents the maximum number of sessions allowed. Use {@link #UNLIMITED} to indicate
 | 
			
		||||
 * that there is no limit.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Claudenir Freitas
 | 
			
		||||
 * @since 6.5
 | 
			
		||||
 */
 | 
			
		||||
public interface SessionLimit extends Function<Authentication, Integer> {
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Represents unlimited sessions.
 | 
			
		||||
	 */
 | 
			
		||||
	SessionLimit UNLIMITED = (authentication) -> -1;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Creates a {@link SessionLimit} that always returns the given value for any user
 | 
			
		||||
	 * @param maxSessions the maximum number of sessions allowed
 | 
			
		||||
	 * @return a {@link SessionLimit} instance that returns the given value.
 | 
			
		||||
	 */
 | 
			
		||||
	static SessionLimit of(int maxSessions) {
 | 
			
		||||
		Assert.isTrue(maxSessions != 0,
 | 
			
		||||
				"MaximumLogins must be either -1 to allow unlimited logins, or a positive integer to specify a maximum");
 | 
			
		||||
		return (authentication) -> maxSessions;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
/*
 | 
			
		||||
 * Copyright 2002-2019 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.
 | 
			
		||||
| 
						 | 
				
			
			@ -34,6 +34,7 @@ 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;
 | 
			
		||||
| 
						 | 
				
			
			@ -41,9 +42,11 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
 | 
			
		|||
import static org.mockito.ArgumentMatchers.any;
 | 
			
		||||
import static org.mockito.ArgumentMatchers.anyBoolean;
 | 
			
		||||
import static org.mockito.BDDMockito.given;
 | 
			
		||||
import static org.mockito.Mockito.verifyNoInteractions;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @author Rob Winch
 | 
			
		||||
 * @author Claudenir Freitas
 | 
			
		||||
 *
 | 
			
		||||
 */
 | 
			
		||||
@ExtendWith(MockitoExtension.class)
 | 
			
		||||
| 
						 | 
				
			
			@ -144,6 +147,86 @@ public class ConcurrentSessionControlAuthenticationStrategyTests {
 | 
			
		|||
		assertThat(this.sessionInformation.isExpired()).isFalse();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void setMaximumSessionsWithNullValue() {
 | 
			
		||||
		assertThatExceptionOfType(IllegalArgumentException.class)
 | 
			
		||||
			.isThrownBy(() -> this.strategy.setMaximumSessions(null))
 | 
			
		||||
			.withMessage("sessionLimit cannot be null");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void noRegisteredSessionUsingSessionLimit() {
 | 
			
		||||
		given(this.sessionRegistry.getAllSessions(any(), anyBoolean())).willReturn(Collections.emptyList());
 | 
			
		||||
		this.strategy.setMaximumSessions(SessionLimit.of(1));
 | 
			
		||||
		this.strategy.setExceptionIfMaximumExceeded(true);
 | 
			
		||||
		this.strategy.onAuthentication(this.authentication, this.request, this.response);
 | 
			
		||||
		// no exception
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void maxSessionsSameSessionIdUsingSessionLimit() {
 | 
			
		||||
		MockHttpSession session = new MockHttpSession(new MockServletContext(), this.sessionInformation.getSessionId());
 | 
			
		||||
		this.request.setSession(session);
 | 
			
		||||
		given(this.sessionRegistry.getAllSessions(any(), anyBoolean()))
 | 
			
		||||
			.willReturn(Collections.singletonList(this.sessionInformation));
 | 
			
		||||
		this.strategy.setMaximumSessions(SessionLimit.of(1));
 | 
			
		||||
		this.strategy.setExceptionIfMaximumExceeded(true);
 | 
			
		||||
		this.strategy.onAuthentication(this.authentication, this.request, this.response);
 | 
			
		||||
		// no exception
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void maxSessionsWithExceptionUsingSessionLimit() {
 | 
			
		||||
		given(this.sessionRegistry.getAllSessions(any(), anyBoolean()))
 | 
			
		||||
			.willReturn(Collections.singletonList(this.sessionInformation));
 | 
			
		||||
		this.strategy.setMaximumSessions(SessionLimit.of(1));
 | 
			
		||||
		this.strategy.setExceptionIfMaximumExceeded(true);
 | 
			
		||||
		assertThatExceptionOfType(SessionAuthenticationException.class)
 | 
			
		||||
			.isThrownBy(() -> this.strategy.onAuthentication(this.authentication, this.request, this.response));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void maxSessionsExpireExistingUserUsingSessionLimit() {
 | 
			
		||||
		given(this.sessionRegistry.getAllSessions(any(), anyBoolean()))
 | 
			
		||||
			.willReturn(Collections.singletonList(this.sessionInformation));
 | 
			
		||||
		this.strategy.setMaximumSessions(SessionLimit.of(1));
 | 
			
		||||
		this.strategy.onAuthentication(this.authentication, this.request, this.response);
 | 
			
		||||
		assertThat(this.sessionInformation.isExpired()).isTrue();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void maxSessionsExpireLeastRecentExistingUserUsingSessionLimit() {
 | 
			
		||||
		SessionInformation moreRecentSessionInfo = new SessionInformation(this.authentication.getPrincipal(), "unique",
 | 
			
		||||
				new Date(1374766999999L));
 | 
			
		||||
		given(this.sessionRegistry.getAllSessions(any(), anyBoolean()))
 | 
			
		||||
			.willReturn(Arrays.asList(moreRecentSessionInfo, this.sessionInformation));
 | 
			
		||||
		this.strategy.setMaximumSessions(SessionLimit.of(2));
 | 
			
		||||
		this.strategy.onAuthentication(this.authentication, this.request, this.response);
 | 
			
		||||
		assertThat(this.sessionInformation.isExpired()).isTrue();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void onAuthenticationWhenMaxSessionsExceededByTwoThenTwoSessionsExpiredUsingSessionLimit() {
 | 
			
		||||
		SessionInformation oldestSessionInfo = new SessionInformation(this.authentication.getPrincipal(), "unique1",
 | 
			
		||||
				new Date(1374766134214L));
 | 
			
		||||
		SessionInformation secondOldestSessionInfo = new SessionInformation(this.authentication.getPrincipal(),
 | 
			
		||||
				"unique2", new Date(1374766134215L));
 | 
			
		||||
		given(this.sessionRegistry.getAllSessions(any(), anyBoolean()))
 | 
			
		||||
			.willReturn(Arrays.asList(oldestSessionInfo, secondOldestSessionInfo, this.sessionInformation));
 | 
			
		||||
		this.strategy.setMaximumSessions(SessionLimit.of(2));
 | 
			
		||||
		this.strategy.onAuthentication(this.authentication, this.request, this.response);
 | 
			
		||||
		assertThat(oldestSessionInfo.isExpired()).isTrue();
 | 
			
		||||
		assertThat(secondOldestSessionInfo.isExpired()).isTrue();
 | 
			
		||||
		assertThat(this.sessionInformation.isExpired()).isFalse();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void onAuthenticationWhenSessionLimitIsUnlimited() {
 | 
			
		||||
		this.strategy.setMaximumSessions(SessionLimit.UNLIMITED);
 | 
			
		||||
		this.strategy.onAuthentication(this.authentication, this.request, this.response);
 | 
			
		||||
		verifyNoInteractions(this.sessionRegistry);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void setMessageSourceNull() {
 | 
			
		||||
		assertThatIllegalArgumentException().isThrownBy(() -> this.strategy.setMessageSource(null));
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,59 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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.
 | 
			
		||||
 * 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.web.session;
 | 
			
		||||
 | 
			
		||||
import org.junit.jupiter.api.Test;
 | 
			
		||||
import org.junit.jupiter.params.ParameterizedTest;
 | 
			
		||||
import org.junit.jupiter.params.provider.ValueSource;
 | 
			
		||||
import org.mockito.Mockito;
 | 
			
		||||
 | 
			
		||||
import org.springframework.security.core.Authentication;
 | 
			
		||||
 | 
			
		||||
import static org.assertj.core.api.Assertions.assertThat;
 | 
			
		||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @author Claudenir Freitas
 | 
			
		||||
 * @since 6.5
 | 
			
		||||
 */
 | 
			
		||||
class SessionLimitTests {
 | 
			
		||||
 | 
			
		||||
	private final Authentication authentication = Mockito.mock(Authentication.class);
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	void testUnlimitedInstance() {
 | 
			
		||||
		SessionLimit sessionLimit = SessionLimit.UNLIMITED;
 | 
			
		||||
		int result = sessionLimit.apply(this.authentication);
 | 
			
		||||
		assertThat(result).isEqualTo(-1);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@ParameterizedTest
 | 
			
		||||
	@ValueSource(ints = { -1, 1, 2, 3 })
 | 
			
		||||
	void testInstanceWithValidMaxSessions(int maxSessions) {
 | 
			
		||||
		SessionLimit sessionLimit = SessionLimit.of(maxSessions);
 | 
			
		||||
		int result = sessionLimit.apply(this.authentication);
 | 
			
		||||
		assertThat(result).isEqualTo(maxSessions);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	void testInstanceWithInvalidMaxSessions() {
 | 
			
		||||
		assertThatIllegalArgumentException().isThrownBy(() -> SessionLimit.of(0))
 | 
			
		||||
			.withMessage(
 | 
			
		||||
					"MaximumLogins must be either -1 to allow unlimited logins, or a positive integer to specify a maximum");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue