Merge branch 'spring-projects:main' into gh-16251

This commit is contained in:
Daeho Kwon 2024-12-21 00:48:51 +09:00 committed by GitHub
commit 7146c46df5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
55 changed files with 1581 additions and 72 deletions

View File

@ -18,28 +18,45 @@ package org.springframework.security.config.annotation.web.builders;
import java.util.List;
import jakarta.servlet.Filter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.UnreachableFilterChainException;
import org.springframework.security.web.access.intercept.AuthorizationFilter;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
/**
* A filter chain validator for filter chains built by {@link WebSecurity}
*
* @author Josh Cummings
* @author Max Batischev
* @since 6.5
*/
final class WebSecurityFilterChainValidator implements FilterChainProxy.FilterChainValidator {
private final Log logger = LogFactory.getLog(getClass());
@Override
public void validate(FilterChainProxy filterChainProxy) {
List<SecurityFilterChain> chains = filterChainProxy.getFilterChains();
checkForAnyRequestRequestMatcher(chains);
checkForDuplicateMatchers(chains);
checkAuthorizationFilters(chains);
}
private void checkForAnyRequestRequestMatcher(List<SecurityFilterChain> chains) {
DefaultSecurityFilterChain anyRequestFilterChain = null;
for (SecurityFilterChain chain : chains) {
if (anyRequestFilterChain != null) {
String message = "A filter chain that matches any request [" + anyRequestFilterChain
+ "] has already been configured, which means that this filter chain [" + chain
+ "] will never get invoked. Please use `HttpSecurity#securityMatcher` to ensure that there is only one filter chain configured for 'any request' and that the 'any request' filter chain is published last.";
throw new IllegalArgumentException(message);
throw new UnreachableFilterChainException(message, anyRequestFilterChain, chain);
}
if (chain instanceof DefaultSecurityFilterChain defaultChain) {
if (defaultChain.getRequestMatcher() instanceof AnyRequestMatcher) {
@ -49,4 +66,48 @@ final class WebSecurityFilterChainValidator implements FilterChainProxy.FilterCh
}
}
private void checkForDuplicateMatchers(List<SecurityFilterChain> chains) {
DefaultSecurityFilterChain filterChain = null;
for (SecurityFilterChain chain : chains) {
if (filterChain != null) {
if (chain instanceof DefaultSecurityFilterChain defaultChain) {
if (defaultChain.getRequestMatcher().equals(filterChain.getRequestMatcher())) {
throw new UnreachableFilterChainException(
"The FilterChainProxy contains two filter chains using the" + " matcher "
+ defaultChain.getRequestMatcher(),
filterChain, defaultChain);
}
}
}
if (chain instanceof DefaultSecurityFilterChain defaultChain) {
filterChain = defaultChain;
}
}
}
private void checkAuthorizationFilters(List<SecurityFilterChain> chains) {
Filter authorizationFilter = null;
Filter filterSecurityInterceptor = null;
for (SecurityFilterChain chain : chains) {
for (Filter filter : chain.getFilters()) {
if (filter instanceof AuthorizationFilter) {
authorizationFilter = filter;
}
if (filter instanceof FilterSecurityInterceptor) {
filterSecurityInterceptor = filter;
}
}
if (authorizationFilter != null && filterSecurityInterceptor != null) {
this.logger.warn(
"It is not recommended to use authorizeRequests in the configuration. Please only use authorizeHttpRequests");
}
if (filterSecurityInterceptor != null) {
this.logger.warn(
"Usage of authorizeRequests is deprecated. Please use authorizeHttpRequests in the configuration");
}
authorizationFilter = null;
filterSecurityInterceptor = null;
}
}
}

View File

@ -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;
@ -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;
}

View File

@ -39,6 +39,7 @@ import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.UnreachableFilterChainException;
import org.springframework.security.web.access.ExceptionTranslationFilter;
import org.springframework.security.web.access.intercept.AuthorizationFilter;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
@ -53,7 +54,6 @@ import org.springframework.security.web.jaasapi.JaasApiIntegrationFilter;
import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter;
import org.springframework.security.web.session.SessionManagementFilter;
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
public class DefaultFilterChainValidator implements FilterChainProxy.FilterChainValidator {
@ -69,31 +69,67 @@ public class DefaultFilterChainValidator implements FilterChainProxy.FilterChain
}
checkPathOrder(new ArrayList<>(fcp.getFilterChains()));
checkForDuplicateMatchers(new ArrayList<>(fcp.getFilterChains()));
checkAuthorizationFilters(new ArrayList<>(fcp.getFilterChains()));
}
private void checkPathOrder(List<SecurityFilterChain> filterChains) {
// Check that the universal pattern is listed at the end, if at all
Iterator<SecurityFilterChain> chains = filterChains.iterator();
while (chains.hasNext()) {
RequestMatcher matcher = ((DefaultSecurityFilterChain) chains.next()).getRequestMatcher();
if (AnyRequestMatcher.INSTANCE.equals(matcher) && chains.hasNext()) {
throw new IllegalArgumentException("A universal match pattern ('/**') is defined "
+ " before other patterns in the filter chain, causing them to be ignored. Please check the "
+ "ordering in your <security:http> namespace or FilterChainProxy bean configuration");
if (chains.next() instanceof DefaultSecurityFilterChain securityFilterChain) {
if (AnyRequestMatcher.INSTANCE.equals(securityFilterChain.getRequestMatcher()) && chains.hasNext()) {
throw new UnreachableFilterChainException("A universal match pattern ('/**') is defined "
+ " before other patterns in the filter chain, causing them to be ignored. Please check the "
+ "ordering in your <security:http> namespace or FilterChainProxy bean configuration",
securityFilterChain, chains.next());
}
}
}
}
private void checkForDuplicateMatchers(List<SecurityFilterChain> chains) {
while (chains.size() > 1) {
DefaultSecurityFilterChain chain = (DefaultSecurityFilterChain) chains.remove(0);
for (SecurityFilterChain test : chains) {
if (chain.getRequestMatcher().equals(((DefaultSecurityFilterChain) test).getRequestMatcher())) {
throw new IllegalArgumentException("The FilterChainProxy contains two filter chains using the"
+ " matcher " + chain.getRequestMatcher() + ". If you are using multiple <http> namespace "
+ "elements, you must use a 'pattern' attribute to define the request patterns to which they apply.");
DefaultSecurityFilterChain filterChain = null;
for (SecurityFilterChain chain : chains) {
if (filterChain != null) {
if (chain instanceof DefaultSecurityFilterChain defaultChain) {
if (defaultChain.getRequestMatcher().equals(filterChain.getRequestMatcher())) {
throw new UnreachableFilterChainException(
"The FilterChainProxy contains two filter chains using the" + " matcher "
+ defaultChain.getRequestMatcher()
+ ". If you are using multiple <http> namespace "
+ "elements, you must use a 'pattern' attribute to define the request patterns to which they apply.",
defaultChain, chain);
}
}
}
if (chain instanceof DefaultSecurityFilterChain defaultChain) {
filterChain = defaultChain;
}
}
}
private void checkAuthorizationFilters(List<SecurityFilterChain> chains) {
Filter authorizationFilter = null;
Filter filterSecurityInterceptor = null;
for (SecurityFilterChain chain : chains) {
for (Filter filter : chain.getFilters()) {
if (filter instanceof AuthorizationFilter) {
authorizationFilter = filter;
}
if (filter instanceof FilterSecurityInterceptor) {
filterSecurityInterceptor = filter;
}
}
if (authorizationFilter != null && filterSecurityInterceptor != null) {
this.logger.warn(
"It is not recommended to use authorizeRequests in the configuration. Please only use authorizeHttpRequests");
}
if (filterSecurityInterceptor != null) {
this.logger.warn(
"Usage of authorizeRequests is deprecated. Please use authorizeHttpRequests in the configuration");
}
authorizationFilter = null;
filterSecurityInterceptor = null;
}
}

View File

@ -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);

View File

@ -146,6 +146,7 @@ final class Saml2LogoutBeanDefinitionParser implements BeanDefinitionParser {
BeanMetadataElement saml2LogoutRequestSuccessHandler = BeanDefinitionBuilder
.rootBeanDefinition(Saml2RelyingPartyInitiatedLogoutSuccessHandler.class)
.addConstructorArgValue(logoutRequestResolver)
.addPropertyValue("logoutRequestRepository", logoutRequestRepository)
.getBeanDefinition();
this.logoutFilter = BeanDefinitionBuilder.rootBeanDefinition(LogoutFilter.class)
.addConstructorArgValue(saml2LogoutRequestSuccessHandler)

View File

@ -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 {
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)
}

View File

@ -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}?

View File

@ -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

View File

@ -116,8 +116,11 @@ import org.springframework.security.oauth2.server.resource.authentication.Bearer
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionAuthenticatedPrincipal;
import org.springframework.security.saml2.Saml2Exception;
import org.springframework.security.saml2.core.Saml2Error;
import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal;
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest;
import org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest;
import org.springframework.security.saml2.provider.service.authentication.TestSaml2Authentications;
@ -125,6 +128,12 @@ import org.springframework.security.saml2.provider.service.authentication.TestSa
import org.springframework.security.saml2.provider.service.authentication.TestSaml2RedirectAuthenticationRequests;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedCredentialsNotFoundException;
import org.springframework.security.web.authentication.rememberme.CookieTheftException;
import org.springframework.security.web.authentication.rememberme.InvalidCookieException;
import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationException;
import org.springframework.security.web.authentication.session.SessionAuthenticationException;
import org.springframework.security.web.authentication.www.NonceExpiredException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
@ -301,6 +310,10 @@ class SpringSecurityCoreVersionSerializableTests {
(r) -> new LdapAuthority("USER", "username", Map.of("attribute", List.of("value1", "value2"))));
// saml2-service-provider
generatorByClassName.put(Saml2AuthenticationException.class,
(r) -> new Saml2AuthenticationException(new Saml2Error("code", "descirption"), "message",
new IOException("fail")));
generatorByClassName.put(Saml2Exception.class, (r) -> new Saml2Exception("message", new IOException("fail")));
generatorByClassName.put(DefaultSaml2AuthenticatedPrincipal.class,
(r) -> TestSaml2Authentications.authentication().getPrincipal());
generatorByClassName.put(Saml2Authentication.class,
@ -321,6 +334,16 @@ class SpringSecurityCoreVersionSerializableTests {
token.setDetails(details);
return token;
});
generatorByClassName.put(PreAuthenticatedCredentialsNotFoundException.class,
(r) -> new PreAuthenticatedCredentialsNotFoundException("message", new IOException("fail")));
generatorByClassName.put(CookieTheftException.class, (r) -> new CookieTheftException("message"));
generatorByClassName.put(InvalidCookieException.class, (r) -> new InvalidCookieException("message"));
generatorByClassName.put(RememberMeAuthenticationException.class,
(r) -> new RememberMeAuthenticationException("message", new IOException("fail")));
generatorByClassName.put(SessionAuthenticationException.class,
(r) -> new SessionAuthenticationException("message"));
generatorByClassName.put(NonceExpiredException.class,
(r) -> new NonceExpiredException("message", new IOException("fail")));
}
@ParameterizedTest

View File

@ -0,0 +1,119 @@
/*
* 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.config.annotation.web.builders;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.UnreachableFilterChainException;
import org.springframework.security.web.access.ExceptionTranslationFilter;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatchers;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatNoException;
/**
* Tests for {@link WebSecurityFilterChainValidator}
*
* @author Max Batischev
*/
@ExtendWith(MockitoExtension.class)
public class WebSecurityFilterChainValidatorTests {
private final WebSecurityFilterChainValidator validator = new WebSecurityFilterChainValidator();
@Mock
private AnonymousAuthenticationFilter authenticationFilter;
@Mock
private ExceptionTranslationFilter exceptionTranslationFilter;
@Mock
private FilterSecurityInterceptor authorizationInterceptor;
@Test
void validateWhenFilterSecurityInterceptorConfiguredThenValidates() {
SecurityFilterChain chain = new DefaultSecurityFilterChain(AntPathRequestMatcher.antMatcher("/api"),
this.authenticationFilter, this.exceptionTranslationFilter, this.authorizationInterceptor);
FilterChainProxy proxy = new FilterChainProxy(List.of(chain));
assertThatNoException().isThrownBy(() -> this.validator.validate(proxy));
}
@Test
void validateWhenAnyRequestMatcherIsPresentThenUnreachableFilterChainException() {
SecurityFilterChain chain1 = new DefaultSecurityFilterChain(AntPathRequestMatcher.antMatcher("/api"),
this.authenticationFilter, this.exceptionTranslationFilter, this.authorizationInterceptor);
SecurityFilterChain chain2 = new DefaultSecurityFilterChain(AnyRequestMatcher.INSTANCE,
this.authenticationFilter, this.exceptionTranslationFilter, this.authorizationInterceptor);
List<SecurityFilterChain> chains = new ArrayList<>();
chains.add(chain2);
chains.add(chain1);
FilterChainProxy proxy = new FilterChainProxy(chains);
assertThatExceptionOfType(UnreachableFilterChainException.class)
.isThrownBy(() -> this.validator.validate(proxy));
}
@Test
void validateWhenSameRequestMatchersArePresentThenUnreachableFilterChainException() {
SecurityFilterChain chain1 = new DefaultSecurityFilterChain(AntPathRequestMatcher.antMatcher("/api"),
this.authenticationFilter, this.exceptionTranslationFilter, this.authorizationInterceptor);
SecurityFilterChain chain2 = new DefaultSecurityFilterChain(AntPathRequestMatcher.antMatcher("/api"),
this.authenticationFilter, this.exceptionTranslationFilter, this.authorizationInterceptor);
List<SecurityFilterChain> chains = new ArrayList<>();
chains.add(chain2);
chains.add(chain1);
FilterChainProxy proxy = new FilterChainProxy(chains);
assertThatExceptionOfType(UnreachableFilterChainException.class)
.isThrownBy(() -> this.validator.validate(proxy));
}
@Test
void validateWhenSameComposedRequestMatchersArePresentThenUnreachableFilterChainException() {
RequestMatcher matcher1 = RequestMatchers.anyOf(RequestMatchers.allOf(AntPathRequestMatcher.antMatcher("/api"),
AntPathRequestMatcher.antMatcher("*.do")), AntPathRequestMatcher.antMatcher("/admin"));
RequestMatcher matcher2 = RequestMatchers.anyOf(RequestMatchers.allOf(AntPathRequestMatcher.antMatcher("/api"),
AntPathRequestMatcher.antMatcher("*.do")), AntPathRequestMatcher.antMatcher("/admin"));
SecurityFilterChain chain1 = new DefaultSecurityFilterChain(matcher1, this.authenticationFilter,
this.exceptionTranslationFilter, this.authorizationInterceptor);
SecurityFilterChain chain2 = new DefaultSecurityFilterChain(matcher2, this.authenticationFilter,
this.exceptionTranslationFilter, this.authorizationInterceptor);
List<SecurityFilterChain> chains = new ArrayList<>();
chains.add(chain2);
chains.add(chain1);
FilterChainProxy proxy = new FilterChainProxy(chains);
assertThatExceptionOfType(UnreachableFilterChainException.class)
.isThrownBy(() -> this.validator.validate(proxy));
}
}

View File

@ -323,7 +323,7 @@ public class WebSecurityConfigurationTests {
assertThatExceptionOfType(BeanCreationException.class)
.isThrownBy(() -> this.spring.register(MultipleAnyRequestSecurityFilterChainConfig.class).autowire())
.havingRootCause()
.isExactlyInstanceOf(IllegalArgumentException.class);
.isInstanceOf(IllegalArgumentException.class);
}
private void assertAnotherUserPermission(WebInvocationPrivilegeEvaluator privilegeEvaluator) {

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2023 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.

View File

@ -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.
@ -59,6 +59,7 @@ 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;
@ -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 {

View File

@ -484,6 +484,7 @@ public class Saml2LogoutConfigurerTests {
verify(getBean(Saml2LogoutResponseValidator.class)).validate(any());
}
// gh-11363
@Test
public void saml2LogoutWhenCustomLogoutRequestRepositoryThenUses() throws Exception {
this.spring.register(Saml2LogoutComponentsConfig.class).autowire();

View File

@ -16,7 +16,9 @@
package org.springframework.security.config.http;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
@ -33,6 +35,8 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.UnreachableFilterChainException;
import org.springframework.security.web.access.ExceptionTranslationFilter;
import org.springframework.security.web.access.intercept.AuthorizationFilter;
import org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource;
@ -40,9 +44,12 @@ import org.springframework.security.web.access.intercept.FilterInvocationSecurit
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willThrow;
@ -97,6 +104,11 @@ public class DefaultFilterChainValidatorTests {
ReflectionTestUtils.setField(this.validator, "logger", this.logger);
}
@Test
void validateWhenFilterSecurityInterceptorConfiguredThenValidates() {
assertThatNoException().isThrownBy(() -> this.validator.validate(this.chain));
}
// SEC-1878
@SuppressWarnings("unchecked")
@Test
@ -130,4 +142,21 @@ public class DefaultFilterChainValidatorTests {
verify(customMetaDataSource, atLeastOnce()).getAttributes(any());
}
@Test
void validateWhenSameRequestMatchersArePresentThenUnreachableFilterChainException() {
AnonymousAuthenticationFilter authenticationFilter = mock(AnonymousAuthenticationFilter.class);
ExceptionTranslationFilter exceptionTranslationFilter = mock(ExceptionTranslationFilter.class);
SecurityFilterChain chain1 = new DefaultSecurityFilterChain(AntPathRequestMatcher.antMatcher("/api"),
authenticationFilter, exceptionTranslationFilter, this.authorizationInterceptor);
SecurityFilterChain chain2 = new DefaultSecurityFilterChain(AntPathRequestMatcher.antMatcher("/api"),
authenticationFilter, exceptionTranslationFilter, this.authorizationInterceptor);
List<SecurityFilterChain> chains = new ArrayList<>();
chains.add(chain2);
chains.add(chain1);
FilterChainProxy proxy = new FilterChainProxy(chains);
assertThatExceptionOfType(UnreachableFilterChainException.class)
.isThrownBy(() -> this.validator.validate(proxy));
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2022 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.

View File

@ -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.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;
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;
}
}
}

View File

@ -63,6 +63,7 @@ import org.springframework.web.util.UriUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.verify;
@ -380,6 +381,22 @@ public class Saml2LogoutBeanDefinitionParserTests {
verify(getBean(Saml2LogoutResponseValidator.class)).validate(any());
}
// gh-11363
@Test
public void saml2LogoutWhenCustomLogoutRequestRepositoryThenUses() throws Exception {
this.spring.configLocations(this.xml("CustomComponents")).autowire();
RelyingPartyRegistration registration = this.repository.findByRegistrationId("get");
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
.samlRequest(this.rpLogoutRequest)
.id(this.rpLogoutRequestId)
.relayState(this.rpLogoutRequestRelayState)
.parameters((params) -> params.put("Signature", this.rpLogoutRequestSignature))
.build();
given(getBean(Saml2LogoutRequestResolver.class).resolve(any(), any())).willReturn(logoutRequest);
this.mvc.perform(post("/logout").with(authentication(this.saml2User)).with(csrf()));
verify(getBean(Saml2LogoutRequestRepository.class)).saveLogoutRequest(eq(logoutRequest), any(), any());
}
private <T> T getBean(Class<T> clazz) {
return this.spring.getContext().getBean(clazz);
}

View File

@ -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
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 {
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)
}
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -18,6 +18,7 @@ package org.springframework.security.core.annotation;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Executable;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
@ -83,6 +84,7 @@ import org.springframework.util.ClassUtils;
*
* @param <A> the annotation to search for and synthesize
* @author Josh Cummings
* @author DingHao
* @since 6.4
*/
final class UniqueSecurityAnnotationScanner<A extends Annotation> extends AbstractSecurityAnnotationScanner<A> {
@ -107,7 +109,7 @@ final class UniqueSecurityAnnotationScanner<A extends Annotation> extends Abstra
MergedAnnotation<A> merge(AnnotatedElement element, Class<?> targetClass) {
if (element instanceof Parameter parameter) {
return this.uniqueParameterAnnotationCache.computeIfAbsent(parameter, (p) -> {
List<MergedAnnotation<A>> annotations = findDirectAnnotations(p);
List<MergedAnnotation<A>> annotations = findParameterAnnotations(p);
return requireUnique(p, annotations);
});
}
@ -137,6 +139,56 @@ final class UniqueSecurityAnnotationScanner<A extends Annotation> extends Abstra
};
}
private List<MergedAnnotation<A>> findParameterAnnotations(Parameter current) {
List<MergedAnnotation<A>> directAnnotations = findDirectAnnotations(current);
if (!directAnnotations.isEmpty()) {
return directAnnotations;
}
Executable executable = current.getDeclaringExecutable();
if (executable instanceof Method method) {
Class<?> clazz = method.getDeclaringClass();
Set<Class<?>> visited = new HashSet<>();
while (clazz != null && clazz != Object.class) {
directAnnotations = findClosestParameterAnnotations(method, clazz, current, visited);
if (!directAnnotations.isEmpty()) {
return directAnnotations;
}
clazz = clazz.getSuperclass();
}
}
return Collections.emptyList();
}
private List<MergedAnnotation<A>> findClosestParameterAnnotations(Method method, Class<?> clazz, Parameter current,
Set<Class<?>> visited) {
if (!visited.add(clazz)) {
return Collections.emptyList();
}
List<MergedAnnotation<A>> annotations = new ArrayList<>(findDirectParameterAnnotations(method, clazz, current));
for (Class<?> ifc : clazz.getInterfaces()) {
annotations.addAll(findClosestParameterAnnotations(method, ifc, current, visited));
}
return annotations;
}
private List<MergedAnnotation<A>> findDirectParameterAnnotations(Method method, Class<?> clazz, Parameter current) {
try {
Method methodToUse = clazz.getDeclaredMethod(method.getName(), method.getParameterTypes());
for (Parameter parameter : methodToUse.getParameters()) {
if (parameter.getName().equals(current.getName())) {
List<MergedAnnotation<A>> directAnnotations = findDirectAnnotations(parameter);
if (!directAnnotations.isEmpty()) {
return directAnnotations;
}
}
}
}
catch (NoSuchMethodException ex) {
// move on
}
return Collections.emptyList();
}
private List<MergedAnnotation<A>> findMethodAnnotations(Method method, Class<?> targetClass) {
// The method may be on an interface, but we need attributes from the target
// class.

View File

@ -16,7 +16,13 @@
package org.springframework.security.core.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.List;
import org.junit.jupiter.api.Test;
@ -34,6 +40,9 @@ public class UniqueSecurityAnnotationScannerTests {
private UniqueSecurityAnnotationScanner<PreAuthorize> scanner = new UniqueSecurityAnnotationScanner<>(
PreAuthorize.class);
private UniqueSecurityAnnotationScanner<CustomParameterAnnotation> parameterScanner = new UniqueSecurityAnnotationScanner<>(
CustomParameterAnnotation.class);
@Test
void scanWhenAnnotationOnInterfaceThenResolves() throws Exception {
Method method = AnnotationOnInterface.class.getDeclaredMethod("method");
@ -251,6 +260,101 @@ public class UniqueSecurityAnnotationScannerTests {
assertThat(preAuthorize).isNull();
}
@Test
void scanParameterAnnotationWhenAnnotationOnInterface() throws Exception {
Parameter parameter = UserService.class.getDeclaredMethod("add", String.class).getParameters()[0];
CustomParameterAnnotation customParameterAnnotation = this.parameterScanner.scan(parameter);
assertThat(customParameterAnnotation.value()).isEqualTo("one");
}
@Test
void scanParameterAnnotationWhenClassInheritingInterfaceAnnotation() throws Exception {
Parameter parameter = UserServiceImpl.class.getDeclaredMethod("add", String.class).getParameters()[0];
CustomParameterAnnotation customParameterAnnotation = this.parameterScanner.scan(parameter);
assertThat(customParameterAnnotation.value()).isEqualTo("one");
}
@Test
void scanParameterAnnotationWhenClassOverridingMethodOverridingInterface() throws Exception {
Parameter parameter = UserServiceImpl.class.getDeclaredMethod("get", String.class).getParameters()[0];
CustomParameterAnnotation customParameterAnnotation = this.parameterScanner.scan(parameter);
assertThat(customParameterAnnotation.value()).isEqualTo("five");
}
@Test
void scanParameterAnnotationWhenMultipleMethodInheritanceThenException() throws Exception {
Parameter parameter = UserServiceImpl.class.getDeclaredMethod("list", String.class).getParameters()[0];
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> this.parameterScanner.scan(parameter));
}
@Test
void scanParameterAnnotationWhenInterfaceNoAnnotationsThenException() throws Exception {
Parameter parameter = UserServiceImpl.class.getDeclaredMethod("delete", String.class).getParameters()[0];
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> this.parameterScanner.scan(parameter));
}
interface UserService {
void add(@CustomParameterAnnotation("one") String user);
List<String> list(@CustomParameterAnnotation("two") String user);
String get(@CustomParameterAnnotation("three") String user);
void delete(@CustomParameterAnnotation("five") String user);
}
interface OtherUserService {
List<String> list(@CustomParameterAnnotation("four") String user);
}
interface ThirdPartyUserService {
void delete(@CustomParameterAnnotation("five") String user);
}
interface RemoteUserService extends ThirdPartyUserService {
}
static class UserServiceImpl implements UserService, OtherUserService, RemoteUserService {
@Override
public void add(String user) {
}
@Override
public List<String> list(String user) {
return List.of(user);
}
@Override
public String get(@CustomParameterAnnotation("five") String user) {
return user;
}
@Override
public void delete(String user) {
}
}
@Target({ ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@interface CustomParameterAnnotation {
String value();
}
@PreAuthorize("one")
private interface AnnotationOnInterface {

View File

@ -0,0 +1,104 @@
= Web Migrations
== Favor Relative URIs
When redirecting to a login endpoint, Spring Security has favored absolute URIs in the past.
For example, if you set your login page like so:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
http
// ...
.formLogin((form) -> form.loginPage("/my-login"))
// ...
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
http {
formLogin {
loginPage = "/my-login"
}
}
----
Xml::
+
[source,kotlin,role="secondary"]
----
<http ...>
<form-login login-page="/my-login"/>
</http>
----
======
then when redirecting to `/my-login` Spring Security would use a `Location:` like the following:
[source]
----
302 Found
// ...
Location: https://myapp.example.org/my-login
----
However, this is no longer necessary given that the RFC is was based on is now obsolete.
In Spring Security 7, this is changed to use a relative URI like so:
[source]
----
302 Found
// ...
Location: /my-login
----
Most applications will not notice a difference.
However, in the event that this change causes problems, you can switch back to the Spring Security 6 behavior by setting the `favorRelativeUrls` value:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
LoginUrlAuthenticationEntryPoint entryPoint = new LoginUrlAuthenticationEntryPoint("/my-login");
entryPoint.setFavorRelativeUris(false);
http
// ...
.exceptionHandling((exceptions) -> exceptions.authenticaitonEntryPoint(entryPoint))
// ...
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
LoginUrlAuthenticationEntryPoint entryPoint = LoginUrlAuthenticationEntryPoint("/my-login")
entryPoint.setFavorRelativeUris(false)
http {
exceptionHandling {
authenticationEntryPoint = entryPoint
}
}
----
Xml::
+
[source,kotlin,role="secondary"]
----
<http entry-point-ref="myEntryPoint">
<!-- ... -->
</http>
<b:bean id="myEntryPoint" class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
<b:property name="favorRelativeUris" value="true"/>
</b:bean>
----
======

View File

@ -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**

View File

@ -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]
======

View File

@ -10,14 +10,14 @@ org-aspectj = "1.9.22.1"
org-bouncycastle = "1.79"
org-eclipse-jetty = "11.0.24"
org-jetbrains-kotlin = "1.9.25"
org-jetbrains-kotlinx = "1.9.0"
org-jetbrains-kotlinx = "1.10.0"
org-mockito = "5.14.2"
org-opensaml = "4.3.2"
org-opensaml5 = "5.1.2"
org-springframework = "6.2.1"
[libraries]
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.12"
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.14"
com-fasterxml-jackson-jackson-bom = "com.fasterxml.jackson:jackson-bom:2.18.2"
com-google-inject-guice = "com.google.inject:guice:3.0"
com-netflix-nebula-nebula-project-plugin = "com.netflix.nebula:nebula-project-plugin:8.2.0"
@ -64,13 +64,13 @@ org-apereo-cas-client-cas-client-core = "org.apereo.cas.client:cas-client-core:4
io-freefair-gradle-aspectj-plugin = "io.freefair.gradle:aspectj-plugin:8.11"
org-aspectj-aspectjrt = { module = "org.aspectj:aspectjrt", version.ref = "org-aspectj" }
org-aspectj-aspectjweaver = { module = "org.aspectj:aspectjweaver", version.ref = "org-aspectj" }
org-assertj-assertj-core = "org.assertj:assertj-core:3.26.3"
org-assertj-assertj-core = "org.assertj:assertj-core:3.27.0"
org-bouncycastle-bcpkix-jdk15on = { module = "org.bouncycastle:bcpkix-jdk18on", version.ref = "org-bouncycastle" }
org-bouncycastle-bcprov-jdk15on = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "org-bouncycastle" }
org-eclipse-jetty-jetty-server = { module = "org.eclipse.jetty:jetty-server", version.ref = "org-eclipse-jetty" }
org-eclipse-jetty-jetty-servlet = { module = "org.eclipse.jetty:jetty-servlet", version.ref = "org-eclipse-jetty" }
org-hamcrest = "org.hamcrest:hamcrest:2.2"
org-hibernate-orm-hibernate-core = "org.hibernate.orm:hibernate-core:6.6.3.Final"
org-hibernate-orm-hibernate-core = "org.hibernate.orm:hibernate-core:6.6.4.Final"
org-hsqldb = "org.hsqldb:hsqldb:2.7.4"
org-jetbrains-kotlin-kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "org-jetbrains-kotlin" }
org-jetbrains-kotlin-kotlin-gradle-plugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25"

View File

@ -107,9 +107,19 @@ public class OAuth2AccessToken extends AbstractOAuth2Token {
public static final TokenType BEARER = new TokenType("Bearer");
/**
* @since 6.5
*/
public static final TokenType DPOP = new TokenType("DPoP");
private final String value;
private TokenType(String value) {
/**
* Constructs a {@code TokenType} using the provided value.
* @param value the value of the token type
* @since 6.5
*/
public TokenType(String value) {
Assert.hasText(value, "value cannot be empty");
this.value = value;
}

View File

@ -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.
@ -16,11 +16,16 @@
package org.springframework.security.saml2;
import java.io.Serial;
/**
* @since 5.2
*/
public class Saml2Exception extends RuntimeException {
@Serial
private static final long serialVersionUID = 6076252564189633016L;
public Saml2Exception(String message) {
super(message);
}

View File

@ -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.
@ -16,6 +16,8 @@
package org.springframework.security.saml2.provider.service.authentication;
import java.io.Serial;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.saml2.core.Saml2Error;
@ -40,6 +42,9 @@ import org.springframework.util.Assert;
*/
public class Saml2AuthenticationException extends AuthenticationException {
@Serial
private static final long serialVersionUID = -2996886630890949105L;
private final Saml2Error error;
/**

View File

@ -0,0 +1,55 @@
/*
* 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;
import org.springframework.security.core.SpringSecurityCoreVersion;
/**
* Thrown if {@link SecurityFilterChain securityFilterChain} is not valid.
*
* @author Max Batischev
* @since 6.5
*/
public class UnreachableFilterChainException extends IllegalArgumentException {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final SecurityFilterChain filterChain;
private final SecurityFilterChain unreachableFilterChain;
/**
* Constructs an <code>UnreachableFilterChainException</code> with the specified
* message.
* @param message the detail message
*/
public UnreachableFilterChainException(String message, SecurityFilterChain filterChain,
SecurityFilterChain unreachableFilterChain) {
super(message);
this.filterChain = filterChain;
this.unreachableFilterChain = unreachableFilterChain;
}
public SecurityFilterChain getFilterChain() {
return this.filterChain;
}
public SecurityFilterChain getUnreachableFilterChain() {
return this.unreachableFilterChain;
}
}

View File

@ -61,6 +61,7 @@ import org.springframework.util.StringUtils;
* @author colin sampaleanu
* @author Omri Spector
* @author Luke Taylor
* @author Michal Okosy
* @since 3.0
*/
public class LoginUrlAuthenticationEntryPoint implements AuthenticationEntryPoint, InitializingBean {
@ -77,6 +78,8 @@ public class LoginUrlAuthenticationEntryPoint implements AuthenticationEntryPoin
private boolean useForward = false;
private boolean favorRelativeUris = false;
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
/**
@ -146,27 +149,38 @@ public class LoginUrlAuthenticationEntryPoint implements AuthenticationEntryPoin
if (UrlUtils.isAbsoluteUrl(loginForm)) {
return loginForm;
}
int serverPort = this.portResolver.getServerPort(request);
String scheme = request.getScheme();
RedirectUrlBuilder urlBuilder = new RedirectUrlBuilder();
urlBuilder.setScheme(scheme);
urlBuilder.setServerName(request.getServerName());
urlBuilder.setPort(serverPort);
urlBuilder.setContextPath(request.getContextPath());
urlBuilder.setPathInfo(loginForm);
if (this.forceHttps && "http".equals(scheme)) {
Integer httpsPort = this.portMapper.lookupHttpsPort(serverPort);
if (httpsPort != null) {
// Overwrite scheme and port in the redirect URL
urlBuilder.setScheme("https");
urlBuilder.setPort(httpsPort);
}
else {
logger.warn(LogMessage.format("Unable to redirect to HTTPS as no port mapping found for HTTP port %s",
serverPort));
}
if (requiresRewrite(request)) {
return httpsUri(request, loginForm);
}
return urlBuilder.getUrl();
return this.favorRelativeUris ? loginForm : absoluteUri(request, loginForm).getUrl();
}
private boolean requiresRewrite(HttpServletRequest request) {
return this.forceHttps && "http".equals(request.getScheme());
}
private String httpsUri(HttpServletRequest request, String path) {
int serverPort = this.portResolver.getServerPort(request);
Integer httpsPort = this.portMapper.lookupHttpsPort(serverPort);
if (httpsPort == null) {
logger.warn(LogMessage.format("Unable to redirect to HTTPS as no port mapping found for HTTP port %s",
serverPort));
return this.favorRelativeUris ? path : absoluteUri(request, path).getUrl();
}
RedirectUrlBuilder builder = absoluteUri(request, path);
builder.setScheme("https");
builder.setPort(httpsPort);
return builder.getUrl();
}
private RedirectUrlBuilder absoluteUri(HttpServletRequest request, String path) {
RedirectUrlBuilder urlBuilder = new RedirectUrlBuilder();
urlBuilder.setScheme(request.getScheme());
urlBuilder.setServerName(request.getServerName());
urlBuilder.setPort(this.portResolver.getServerPort(request));
urlBuilder.setContextPath(request.getContextPath());
urlBuilder.setPathInfo(path);
return urlBuilder;
}
/**
@ -244,4 +258,18 @@ public class LoginUrlAuthenticationEntryPoint implements AuthenticationEntryPoin
return this.useForward;
}
/**
* Favor using relative URIs when formulating a redirect.
*
* <p>
* Note that a relative redirect is not always possible. For example, when redirecting
* from {@code http} to {@code https}, the URL needs to be absolute.
* </p>
* @param favorRelativeUris whether to favor relative URIs or not
* @since 6.5
*/
public void setFavorRelativeUris(boolean favorRelativeUris) {
this.favorRelativeUris = favorRelativeUris;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 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.
@ -16,10 +16,15 @@
package org.springframework.security.web.authentication.preauth;
import java.io.Serial;
import org.springframework.security.core.AuthenticationException;
public class PreAuthenticatedCredentialsNotFoundException extends AuthenticationException {
@Serial
private static final long serialVersionUID = 2026209817833032728L;
public PreAuthenticatedCredentialsNotFoundException(String msg) {
super(msg);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 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.
@ -16,11 +16,16 @@
package org.springframework.security.web.authentication.rememberme;
import java.io.Serial;
/**
* @author Luke Taylor
*/
public class CookieTheftException extends RememberMeAuthenticationException {
@Serial
private static final long serialVersionUID = -7215039140728554850L;
public CookieTheftException(String message) {
super(message);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 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.
@ -16,6 +16,8 @@
package org.springframework.security.web.authentication.rememberme;
import java.io.Serial;
/**
* Exception thrown by a RememberMeServices implementation to indicate that a submitted
* cookie is of an invalid format or has expired.
@ -24,6 +26,9 @@ package org.springframework.security.web.authentication.rememberme;
*/
public class InvalidCookieException extends RememberMeAuthenticationException {
@Serial
private static final long serialVersionUID = -7952247791921087125L;
public InvalidCookieException(String message) {
super(message);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 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.
@ -16,6 +16,8 @@
package org.springframework.security.web.authentication.rememberme;
import java.io.Serial;
import org.springframework.security.core.AuthenticationException;
/**
@ -27,6 +29,9 @@ import org.springframework.security.core.AuthenticationException;
*/
public class RememberMeAuthenticationException extends AuthenticationException {
@Serial
private static final long serialVersionUID = 7028526952590057426L;
/**
* Constructs a {@code RememberMeAuthenticationException} with the specified message
* and root cause.

View File

@ -76,7 +76,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 +130,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 +172,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;
}
/**

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 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.
@ -16,6 +16,8 @@
package org.springframework.security.web.authentication.session;
import java.io.Serial;
import org.springframework.security.core.AuthenticationException;
/**
@ -31,6 +33,9 @@ import org.springframework.security.core.AuthenticationException;
*/
public class SessionAuthenticationException extends AuthenticationException {
@Serial
private static final long serialVersionUID = -2359914603911936474L;
public SessionAuthenticationException(String msg) {
super(msg);
}

View File

@ -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.authentication.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;
}
}

View File

@ -16,6 +16,8 @@
package org.springframework.security.web.authentication.www;
import java.io.Serial;
import org.springframework.security.core.AuthenticationException;
/**
@ -25,6 +27,9 @@ import org.springframework.security.core.AuthenticationException;
*/
public class NonceExpiredException extends AuthenticationException {
@Serial
private static final long serialVersionUID = -3487244679050681257L;
/**
* Constructs a <code>NonceExpiredException</code> with the specified message.
* @param msg the detail message

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 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.
@ -20,6 +20,7 @@ import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
@ -90,6 +91,23 @@ public final class AndRequestMatcher implements RequestMatcher {
return MatchResult.match(variables);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
AndRequestMatcher that = (AndRequestMatcher) o;
return Objects.equals(this.requestMatchers, that.requestMatchers);
}
@Override
public int hashCode() {
return Objects.hash(this.requestMatchers);
}
@Override
public String toString() {
return "And " + this.requestMatchers;

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 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.
@ -18,6 +18,7 @@ package org.springframework.security.web.util.matcher;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import jakarta.servlet.http.HttpServletRequest;
@ -81,6 +82,23 @@ public final class OrRequestMatcher implements RequestMatcher {
return MatchResult.notMatch();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
OrRequestMatcher that = (OrRequestMatcher) o;
return Objects.equals(this.requestMatchers, that.requestMatchers);
}
@Override
public int hashCode() {
return Objects.hash(this.requestMatchers);
}
@Override
public String toString() {
return "Or " + this.requestMatchers;

View File

@ -135,6 +135,12 @@ public class LoginUrlAuthenticationEntryPointTests {
ep.setPortResolver(new MockPortResolver(8080, 8443));
ep.commence(request, response, null);
assertThat(response.getRedirectedUrl()).isEqualTo("https://www.example.com:8443/bigWebApp/hello");
// access to https via http port
request.setServerPort(8080);
response = new MockHttpServletResponse();
ep.setPortResolver(new MockPortResolver(8080, 8443));
ep.commence(request, response, null);
assertThat(response.getRedirectedUrl()).isEqualTo("https://www.example.com:8443/bigWebApp/hello");
}
@Test
@ -231,4 +237,54 @@ public class LoginUrlAuthenticationEntryPointTests {
assertThatIllegalArgumentException().isThrownBy(ep::afterPropertiesSet);
}
@Test
public void commenceWhenFavorRelativeUrisThenHttpsSchemeNotIncluded() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setRequestURI("/some_path");
request.setScheme("https");
request.setServerName("www.example.com");
request.setContextPath("/bigWebApp");
request.setServerPort(443);
MockHttpServletResponse response = new MockHttpServletResponse();
LoginUrlAuthenticationEntryPoint ep = new LoginUrlAuthenticationEntryPoint("/hello");
ep.setFavorRelativeUris(true);
ep.setPortMapper(new PortMapperImpl());
ep.setForceHttps(true);
ep.setPortMapper(new PortMapperImpl());
ep.setPortResolver(new MockPortResolver(80, 443));
ep.afterPropertiesSet();
ep.commence(request, response, null);
assertThat(response.getRedirectedUrl()).isEqualTo("/bigWebApp/hello");
request.setServerPort(8443);
response = new MockHttpServletResponse();
ep.setPortResolver(new MockPortResolver(8080, 8443));
ep.commence(request, response, null);
assertThat(response.getRedirectedUrl()).isEqualTo("/bigWebApp/hello");
// access to https via http port
request.setServerPort(8080);
response = new MockHttpServletResponse();
ep.setPortResolver(new MockPortResolver(8080, 8443));
ep.commence(request, response, null);
assertThat(response.getRedirectedUrl()).isEqualTo("/bigWebApp/hello");
}
@Test
public void commenceWhenFavorRelativeUrisThenHttpSchemeNotIncluded() throws Exception {
LoginUrlAuthenticationEntryPoint ep = new LoginUrlAuthenticationEntryPoint("/hello");
ep.setFavorRelativeUris(true);
ep.setPortMapper(new PortMapperImpl());
ep.setPortResolver(new MockPortResolver(80, 443));
ep.afterPropertiesSet();
MockHttpServletRequest request = new MockHttpServletRequest();
request.setRequestURI("/some_path");
request.setContextPath("/bigWebApp");
request.setScheme("http");
request.setServerName("localhost");
request.setContextPath("/bigWebApp");
request.setServerPort(80);
MockHttpServletResponse response = new MockHttpServletResponse();
ep.commence(request, response, null);
assertThat(response.getRedirectedUrl()).isEqualTo("/bigWebApp/hello");
}
}

View File

@ -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.
@ -41,9 +41,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 +146,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));

View File

@ -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.authentication.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");
}
}