Merge branch 'spring-projects:main' into gh-16251
This commit is contained in:
commit
7146c46df5
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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>
|
||||
----
|
||||
======
|
|
@ -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**
|
||||
|
|
|
@ -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]
|
||||
======
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue