Revert unnecessary commits from main

Issue gh-15016
This commit is contained in:
Marcus Hert Da Coregio
2024-05-08 13:44:07 -03:00
parent e4745c99e9
commit 08f11f06ab
317 changed files with 1281 additions and 30496 deletions

View File

@@ -411,7 +411,7 @@ public class FilterChainProxy extends GenericFilterBean {
/**
* Provide a new {@link FilterChain} that accounts for the provided filters as
* well as the original filter chain.
* well as teh original filter chain.
* @param original the original {@link FilterChain}
* @param filters the security filters
* @return a security-enabled {@link FilterChain} that includes the provided

View File

@@ -1,61 +0,0 @@
/*
* 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.
* 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.access;
import java.util.function.Supplier;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
import org.springframework.security.web.util.matcher.IpAddressMatcher;
import org.springframework.util.Assert;
/**
* A {@link AuthorizationManager}, that determines if the current request contains the
* specified address or range of addresses
*
* @author brunodmartins
* @since 6.3
*/
public final class IpAddressAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {
private final IpAddressMatcher ipAddressMatcher;
IpAddressAuthorizationManager(String ipAddress) {
this.ipAddressMatcher = new IpAddressMatcher(ipAddress);
}
/**
* Creates an instance of {@link IpAddressAuthorizationManager} with the provided IP
* address.
* @param ipAddress the address or range of addresses from which the request must
* @return the new instance
*/
public static IpAddressAuthorizationManager hasIpAddress(String ipAddress) {
Assert.notNull(ipAddress, "ipAddress cannot be null");
return new IpAddressAuthorizationManager(ipAddress);
}
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication,
RequestAuthorizationContext requestAuthorizationContext) {
return new AuthorizationDecision(
this.ipAddressMatcher.matcher(requestAuthorizationContext.getRequest()).isMatch());
}
}

View File

@@ -1,60 +0,0 @@
/*
* 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;
import java.util.ArrayList;
import java.util.List;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.core.Authentication;
import org.springframework.util.Assert;
/**
* A {@link AuthenticationConverter}, that iterates over multiple
* {@link AuthenticationConverter}. The first non-null {@link Authentication} will be used
* as a result.
*
* @author Max Batischev
* @since 6.3
*/
public final class DelegatingAuthenticationConverter implements AuthenticationConverter {
private final List<AuthenticationConverter> delegates;
public DelegatingAuthenticationConverter(List<AuthenticationConverter> delegates) {
Assert.notEmpty(delegates, "delegates cannot be null");
this.delegates = new ArrayList<>(delegates);
}
public DelegatingAuthenticationConverter(AuthenticationConverter... delegates) {
Assert.notEmpty(delegates, "delegates cannot be null");
this.delegates = List.of(delegates);
}
@Override
public Authentication convert(HttpServletRequest request) {
for (AuthenticationConverter delegate : this.delegates) {
Authentication authentication = delegate.convert(request);
if (authentication != null) {
return authentication;
}
}
return null;
}
}

View File

@@ -1,117 +0,0 @@
/*
* 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.password;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.password.CompromisedPasswordCheckResult;
import org.springframework.security.authentication.password.CompromisedPasswordChecker;
import org.springframework.security.crypto.codec.Hex;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestClientException;
/**
* Checks if the provided password was leaked by relying on
* <a href="https://www.haveibeenpwned.com/API/v3#PwnedPasswords">Have I Been Pwned REST
* API</a>. This implementation uses the Search by Range in order to protect the value of
* the source password being searched for.
*
* @author Marcus da Coregio
* @since 6.3
*/
public final class HaveIBeenPwnedRestApiPasswordChecker implements CompromisedPasswordChecker {
private static final String API_URL = "https://api.pwnedpasswords.com/range/";
private static final int PREFIX_LENGTH = 5;
private final Log logger = LogFactory.getLog(getClass());
private final MessageDigest sha1Digest;
private RestClient restClient = RestClient.builder().baseUrl(API_URL).build();
public HaveIBeenPwnedRestApiPasswordChecker() {
this.sha1Digest = getSha1Digest();
}
@Override
@NonNull
public CompromisedPasswordCheckResult check(String password) {
byte[] hash = this.sha1Digest.digest(password.getBytes(StandardCharsets.UTF_8));
String encoded = new String(Hex.encode(hash)).toUpperCase();
String prefix = encoded.substring(0, PREFIX_LENGTH);
String suffix = encoded.substring(PREFIX_LENGTH);
List<String> passwords = getLeakedPasswordsForPrefix(prefix);
boolean isLeaked = findLeakedPassword(passwords, suffix);
return new CompromisedPasswordCheckResult(isLeaked);
}
/**
* Sets the {@link RestClient} to use when making requests to Have I Been Pwned REST
* API. By default, a {@link RestClient} with a base URL of {@link #API_URL} is used.
* @param restClient the {@link RestClient} to use
*/
public void setRestClient(RestClient restClient) {
Assert.notNull(restClient, "restClient cannot be null");
this.restClient = restClient;
}
private boolean findLeakedPassword(List<String> passwords, String suffix) {
for (String pw : passwords) {
if (pw.startsWith(suffix)) {
return true;
}
}
return false;
}
private List<String> getLeakedPasswordsForPrefix(String prefix) {
try {
String response = this.restClient.get().uri(prefix).retrieve().body(String.class);
if (!StringUtils.hasText(response)) {
return Collections.emptyList();
}
return response.lines().toList();
}
catch (RestClientException ex) {
this.logger.error("Request for leaked passwords failed", ex);
return Collections.emptyList();
}
}
private static MessageDigest getSha1Digest() {
try {
return MessageDigest.getInstance("SHA-1");
}
catch (NoSuchAlgorithmException ex) {
throw new RuntimeException(ex.getMessage());
}
}
}

View File

@@ -1,111 +0,0 @@
/*
* 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.password;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import org.springframework.security.authentication.password.CompromisedPasswordCheckResult;
import org.springframework.security.authentication.password.ReactiveCompromisedPasswordChecker;
import org.springframework.security.crypto.codec.Hex;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;
/**
* Checks if the provided password was leaked by relying on
* <a href="https://www.haveibeenpwned.com/API/v3#PwnedPasswords">Have I Been Pwned REST
* API</a>. This implementation uses the Search by Range in order to protect the value of
* the source password being searched for.
*
* @author Marcus da Coregio
* @since 6.3
*/
public class HaveIBeenPwnedRestApiReactivePasswordChecker implements ReactiveCompromisedPasswordChecker {
private static final String API_URL = "https://api.pwnedpasswords.com/range/";
private static final int PREFIX_LENGTH = 5;
private final Log logger = LogFactory.getLog(getClass());
private WebClient webClient = WebClient.builder().baseUrl(API_URL).build();
private final MessageDigest sha1Digest;
public HaveIBeenPwnedRestApiReactivePasswordChecker() {
this.sha1Digest = getSha1Digest();
}
@Override
public Mono<CompromisedPasswordCheckResult> check(String password) {
return getHash(password).map((hash) -> new String(Hex.encode(hash)))
.flatMap(this::findLeakedPassword)
.map(CompromisedPasswordCheckResult::new);
}
private Mono<Boolean> findLeakedPassword(String encodedPassword) {
String prefix = encodedPassword.substring(0, PREFIX_LENGTH).toUpperCase();
String suffix = encodedPassword.substring(PREFIX_LENGTH).toUpperCase();
return getLeakedPasswordsForPrefix(prefix).any((leakedPw) -> leakedPw.startsWith(suffix));
}
private Flux<String> getLeakedPasswordsForPrefix(String prefix) {
return this.webClient.get().uri(prefix).retrieve().bodyToMono(String.class).flatMapMany((body) -> {
if (StringUtils.hasText(body)) {
return Flux.fromStream(body.lines());
}
return Flux.empty();
})
.doOnError((ex) -> this.logger.error("Request for leaked passwords failed", ex))
.onErrorResume(WebClientResponseException.class, (ex) -> Flux.empty());
}
/**
* Sets the {@link WebClient} to use when making requests to Have I Been Pwned REST
* API. By default, a {@link WebClient} with a base URL of {@link #API_URL} is used.
* @param webClient the {@link WebClient} to use
*/
public void setWebClient(WebClient webClient) {
Assert.notNull(webClient, "webClient cannot be null");
this.webClient = webClient;
}
private Mono<byte[]> getHash(String password) {
return Mono.fromSupplier(() -> this.sha1Digest.digest(password.getBytes(StandardCharsets.UTF_8)))
.subscribeOn(Schedulers.boundedElastic())
.publishOn(Schedulers.parallel());
}
private static MessageDigest getSha1Digest() {
try {
return MessageDigest.getInstance("SHA-1");
}
catch (NoSuchAlgorithmException ex) {
throw new RuntimeException(ex.getMessage());
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2016 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.
@@ -19,15 +19,12 @@ package org.springframework.security.web.authentication.session;
import org.springframework.security.core.AuthenticationException;
/**
* Thrown by an {@link SessionAuthenticationStrategy} or
* {@link ServerSessionAuthenticationStrategy} to indicate that an authentication object
* is not valid for the current session, typically because the same user has exceeded the
* number of sessions they are allowed to have concurrently.
* Thrown by an <tt>SessionAuthenticationStrategy</tt> to indicate that an authentication
* object is not valid for the current session, typically because the same user has
* exceeded the number of sessions they are allowed to have concurrently.
*
* @author Luke Taylor
* @since 3.0
* @see SessionAuthenticationStrategy
* @see ServerSessionAuthenticationStrategy
*/
public class SessionAuthenticationException extends AuthenticationException {

View File

@@ -1,46 +0,0 @@
/*
* 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.jackson2;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.switchuser.SwitchUserGrantedAuthority;
/**
* Jackson mixin class to serialize/deserialize {@link SwitchUserGrantedAuthority}.
*
* @author Markus Heiden
* @since 6.3
* @see WebServletJackson2Module
* @see org.springframework.security.jackson2.SecurityJackson2Modules
*/
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
@JsonIgnoreProperties(ignoreUnknown = true)
abstract class SwitchUserGrantedAuthorityMixIn {
@JsonCreator
SwitchUserGrantedAuthorityMixIn(@JsonProperty("role") String role, @JsonProperty("source") Authentication source) {
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015-2024 the original author or authors.
* Copyright 2015-2016 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.
@@ -22,17 +22,16 @@ import jakarta.servlet.http.Cookie;
import org.springframework.security.jackson2.SecurityJackson2Modules;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.security.web.authentication.switchuser.SwitchUserGrantedAuthority;
import org.springframework.security.web.savedrequest.DefaultSavedRequest;
import org.springframework.security.web.savedrequest.SavedCookie;
/**
* Jackson module for spring-security-web related to servlet. This module registers
* {@link CookieMixin}, {@link SavedCookieMixin}, {@link DefaultSavedRequestMixin},
* {@link WebAuthenticationDetailsMixin}, and {@link SwitchUserGrantedAuthorityMixIn}. If
* no default typing is enabled by default then it will be enabled, because typing info is
* needed to properly serialize/deserialize objects. In order to use this module just add
* this module into your ObjectMapper configuration.
* Jackson module for spring-security-web related to servlet. This module register
* {@link CookieMixin}, {@link SavedCookieMixin}, {@link DefaultSavedRequestMixin} and
* {@link WebAuthenticationDetailsMixin}. If no default typing enabled by default then
* it'll enable it because typing info is needed to properly serialize/deserialize
* objects. In order to use this module just add this module into your ObjectMapper
* configuration.
*
* <pre>
* ObjectMapper mapper = new ObjectMapper();
@@ -57,7 +56,6 @@ public class WebServletJackson2Module extends SimpleModule {
context.setMixInAnnotations(SavedCookie.class, SavedCookieMixin.class);
context.setMixInAnnotations(DefaultSavedRequest.class, DefaultSavedRequestMixin.class);
context.setMixInAnnotations(WebAuthenticationDetails.class, WebAuthenticationDetailsMixin.class);
context.setMixInAnnotations(SwitchUserGrantedAuthority.class, SwitchUserGrantedAuthorityMixIn.class);
}
}

View File

@@ -85,8 +85,7 @@ public final class CurrentSecurityContextArgumentResolver implements HandlerMeth
@Override
public boolean supportsParameter(MethodParameter parameter) {
return SecurityContext.class.isAssignableFrom(parameter.getParameterType())
|| findMethodAnnotation(CurrentSecurityContext.class, parameter) != null;
return findMethodAnnotation(CurrentSecurityContext.class, parameter) != null;
}
@Override
@@ -96,12 +95,26 @@ public final class CurrentSecurityContextArgumentResolver implements HandlerMeth
if (securityContext == null) {
return null;
}
Object securityContextResult = securityContext;
CurrentSecurityContext annotation = findMethodAnnotation(CurrentSecurityContext.class, parameter);
if (annotation != null) {
return resolveSecurityContextFromAnnotation(parameter, annotation, securityContext);
String expressionToParse = annotation.expression();
if (StringUtils.hasLength(expressionToParse)) {
StandardEvaluationContext context = new StandardEvaluationContext();
context.setRootObject(securityContext);
context.setVariable("this", securityContext);
context.setBeanResolver(this.beanResolver);
Expression expression = this.parser.parseExpression(expressionToParse);
securityContextResult = expression.getValue(context);
}
return securityContext;
if (securityContextResult != null
&& !parameter.getParameterType().isAssignableFrom(securityContextResult.getClass())) {
if (annotation.errorOnInvalidType()) {
throw new ClassCastException(
securityContextResult + " is not assignable to " + parameter.getParameterType());
}
return null;
}
return securityContextResult;
}
/**
@@ -124,29 +137,6 @@ public final class CurrentSecurityContextArgumentResolver implements HandlerMeth
this.beanResolver = beanResolver;
}
private Object resolveSecurityContextFromAnnotation(MethodParameter parameter, CurrentSecurityContext annotation,
SecurityContext securityContext) {
Object securityContextResult = securityContext;
String expressionToParse = annotation.expression();
if (StringUtils.hasLength(expressionToParse)) {
StandardEvaluationContext context = new StandardEvaluationContext();
context.setRootObject(securityContext);
context.setVariable("this", securityContext);
context.setBeanResolver(this.beanResolver);
Expression expression = this.parser.parseExpression(expressionToParse);
securityContextResult = expression.getValue(context);
}
if (securityContextResult != null
&& !parameter.getParameterType().isAssignableFrom(securityContextResult.getClass())) {
if (annotation.errorOnInvalidType()) {
throw new ClassCastException(
securityContextResult + " is not assignable to " + parameter.getParameterType());
}
return null;
}
return securityContextResult;
}
/**
* Obtain the specified {@link Annotation} on the specified {@link MethodParameter}.
* @param annotationClass the class of the {@link Annotation} to find on the

View File

@@ -67,21 +67,7 @@ public class CurrentSecurityContextArgumentResolver extends HandlerMethodArgumen
@Override
public boolean supportsParameter(MethodParameter parameter) {
return isMonoSecurityContext(parameter)
|| findMethodAnnotation(CurrentSecurityContext.class, parameter) != null;
}
private boolean isMonoSecurityContext(MethodParameter parameter) {
boolean isParameterPublisher = Publisher.class.isAssignableFrom(parameter.getParameterType());
if (isParameterPublisher) {
ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
Class<?> genericType = resolvableType.resolveGeneric(0);
if (genericType == null) {
return false;
}
return SecurityContext.class.isAssignableFrom(genericType);
}
return false;
return findMethodAnnotation(CurrentSecurityContext.class, parameter) != null;
}
@Override
@@ -109,14 +95,6 @@ public class CurrentSecurityContextArgumentResolver extends HandlerMethodArgumen
*/
private Object resolveSecurityContext(MethodParameter parameter, SecurityContext securityContext) {
CurrentSecurityContext annotation = findMethodAnnotation(CurrentSecurityContext.class, parameter);
if (annotation != null) {
return resolveSecurityContextFromAnnotation(annotation, parameter, securityContext);
}
return securityContext;
}
private Object resolveSecurityContextFromAnnotation(CurrentSecurityContext annotation, MethodParameter parameter,
Object securityContext) {
Object securityContextResult = securityContext;
String expressionToParse = annotation.expression();
if (StringUtils.hasLength(expressionToParse)) {

View File

@@ -98,7 +98,7 @@ public class WebFilterChainProxy implements WebFilter {
/**
* Provide a new {@link FilterChain} that accounts for the provided filters as
* well as the original filter chain.
* well as teh original filter chain.
* @param original the original {@link FilterChain}
* @param filters the security filters
* @return a security-enabled {@link FilterChain} that includes the provided

View File

@@ -1,103 +0,0 @@
/*
* 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.server.authentication;
import java.util.List;
import reactor.core.publisher.Mono;
import reactor.util.function.Tuples;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.session.ReactiveSessionInformation;
import org.springframework.security.core.session.ReactiveSessionRegistry;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.util.Assert;
import org.springframework.web.server.WebSession;
/**
* Controls the number of sessions a user can have concurrently authenticated in an
* application. It also allows for customizing behaviour when an authentication attempt is
* made while the user already has the maximum number of sessions open. By default, it
* allows a maximum of 1 session per user, if the maximum is exceeded, the user's least
* recently used session(s) will be expired.
*
* @author Marcus da Coregio
* @since 6.3
* @see ServerMaximumSessionsExceededHandler
* @see RegisterSessionServerAuthenticationSuccessHandler
*/
public final class ConcurrentSessionControlServerAuthenticationSuccessHandler
implements ServerAuthenticationSuccessHandler {
private final ReactiveSessionRegistry sessionRegistry;
private final ServerMaximumSessionsExceededHandler maximumSessionsExceededHandler;
private SessionLimit sessionLimit = SessionLimit.of(1);
public ConcurrentSessionControlServerAuthenticationSuccessHandler(ReactiveSessionRegistry sessionRegistry,
ServerMaximumSessionsExceededHandler maximumSessionsExceededHandler) {
Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
Assert.notNull(maximumSessionsExceededHandler, "maximumSessionsExceededHandler cannot be null");
this.sessionRegistry = sessionRegistry;
this.maximumSessionsExceededHandler = maximumSessionsExceededHandler;
}
@Override
public Mono<Void> onAuthenticationSuccess(WebFilterExchange exchange, Authentication authentication) {
return this.sessionLimit.apply(authentication)
.flatMap((maxSessions) -> handleConcurrency(exchange, authentication, maxSessions));
}
private Mono<Void> handleConcurrency(WebFilterExchange exchange, Authentication authentication,
Integer maximumSessions) {
return this.sessionRegistry.getAllSessions(authentication.getPrincipal())
.collectList()
.flatMap((registeredSessions) -> exchange.getExchange()
.getSession()
.map((currentSession) -> Tuples.of(currentSession, registeredSessions)))
.flatMap((sessionTuple) -> {
WebSession currentSession = sessionTuple.getT1();
List<ReactiveSessionInformation> registeredSessions = sessionTuple.getT2();
int registeredSessionsCount = registeredSessions.size();
if (registeredSessionsCount < maximumSessions) {
return Mono.empty();
}
if (registeredSessionsCount == maximumSessions) {
for (ReactiveSessionInformation registeredSession : registeredSessions) {
if (registeredSession.getSessionId().equals(currentSession.getId())) {
return Mono.empty();
}
}
}
return this.maximumSessionsExceededHandler.handle(new MaximumSessionsContext(authentication,
registeredSessions, maximumSessions, currentSession));
});
}
/**
* Sets the strategy used to resolve the maximum number of sessions that are allowed
* for a specific {@link Authentication}. By default, it returns {@code 1} for any
* authentication.
* @param sessionLimit the {@link SessionLimit} to use
*/
public void setSessionLimit(SessionLimit sessionLimit) {
Assert.notNull(sessionLimit, "sessionLimit cannot be null");
this.sessionLimit = sessionLimit;
}
}

View File

@@ -1,72 +0,0 @@
/*
* 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.server.authentication;
import java.util.List;
import java.util.function.Function;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.security.core.Authentication;
import org.springframework.util.Assert;
import org.springframework.web.server.ServerWebExchange;
/**
* A {@link ServerAuthenticationConverter} that delegates to other
* {@link ServerAuthenticationConverter} instances.
*
* @author DingHao
* @since 6.3
*/
public final class DelegatingServerAuthenticationConverter implements ServerAuthenticationConverter {
private final List<ServerAuthenticationConverter> delegates;
private boolean continueOnError = false;
private final Log logger = LogFactory.getLog(getClass());
public DelegatingServerAuthenticationConverter(ServerAuthenticationConverter... converters) {
this(List.of(converters));
}
public DelegatingServerAuthenticationConverter(List<ServerAuthenticationConverter> converters) {
Assert.notEmpty(converters, "converters cannot be null");
this.delegates = converters;
}
@Override
public Mono<Authentication> convert(ServerWebExchange exchange) {
Flux<ServerAuthenticationConverter> result = Flux.fromIterable(this.delegates);
Function<ServerAuthenticationConverter, Mono<Authentication>> logging = (
converter) -> converter.convert(exchange).doOnError(this.logger::debug);
return ((this.continueOnError) ? result.concatMapDelayError(logging) : result.concatMap(logging)).next();
}
/**
* Continue iterating when a delegate errors, defaults to {@code false}
* @param continueOnError whether to continue when a delegate errors
* @since 6.3
*/
public void setContinueOnError(boolean continueOnError) {
this.continueOnError = continueOnError;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2018 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.
@@ -42,16 +42,6 @@ public class DelegatingServerAuthenticationSuccessHandler implements ServerAuthe
this.delegates = Arrays.asList(delegates);
}
/**
* Creates a new instance with the provided list of delegates
* @param delegates the {@link List} of {@link ServerAuthenticationSuccessHandler}
* @since 6.3
*/
public DelegatingServerAuthenticationSuccessHandler(List<ServerAuthenticationSuccessHandler> delegates) {
Assert.notEmpty(delegates, "delegates cannot be null or empty");
this.delegates = delegates;
}
@Override
public Mono<Void> onAuthenticationSuccess(WebFilterExchange exchange, Authentication authentication) {
return Flux.fromIterable(this.delegates)

View File

@@ -1,62 +0,0 @@
/*
* 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.server.authentication;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.security.core.session.ReactiveSessionInformation;
import org.springframework.web.server.session.WebSessionStore;
/**
* Implementation of {@link ServerMaximumSessionsExceededHandler} that invalidates the
* least recently used {@link ReactiveSessionInformation} and removes the related sessions
* from the {@link WebSessionStore}. It only invalidates the amount of sessions that
* exceed the maximum allowed. For example, if the maximum was exceeded by 1, only the
* least recently used session will be invalidated.
*
* @author Marcus da Coregio
* @since 6.3
*/
public final class InvalidateLeastUsedServerMaximumSessionsExceededHandler
implements ServerMaximumSessionsExceededHandler {
private final WebSessionStore webSessionStore;
public InvalidateLeastUsedServerMaximumSessionsExceededHandler(WebSessionStore webSessionStore) {
this.webSessionStore = webSessionStore;
}
@Override
public Mono<Void> handle(MaximumSessionsContext context) {
List<ReactiveSessionInformation> sessions = new ArrayList<>(context.getSessions());
sessions.sort(Comparator.comparing(ReactiveSessionInformation::getLastAccessTime));
int maximumSessionsExceededBy = sessions.size() - context.getMaximumSessionsAllowed() + 1;
List<ReactiveSessionInformation> leastRecentlyUsedSessionsToInvalidate = sessions.subList(0,
maximumSessionsExceededBy);
return Flux.fromIterable(leastRecentlyUsedSessionsToInvalidate)
.flatMap((toInvalidate) -> toInvalidate.invalidate().thenReturn(toInvalidate))
.flatMap((toInvalidate) -> this.webSessionStore.removeSession(toInvalidate.getSessionId()))
.then();
}
}

View File

@@ -1,59 +0,0 @@
/*
* 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.server.authentication;
import java.util.List;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.session.ReactiveSessionInformation;
import org.springframework.web.server.WebSession;
public final class MaximumSessionsContext {
private final Authentication authentication;
private final List<ReactiveSessionInformation> sessions;
private final int maximumSessionsAllowed;
private final WebSession currentSession;
public MaximumSessionsContext(Authentication authentication, List<ReactiveSessionInformation> sessions,
int maximumSessionsAllowed, WebSession currentSession) {
this.authentication = authentication;
this.sessions = sessions;
this.maximumSessionsAllowed = maximumSessionsAllowed;
this.currentSession = currentSession;
}
public Authentication getAuthentication() {
return this.authentication;
}
public List<ReactiveSessionInformation> getSessions() {
return this.sessions;
}
public int getMaximumSessionsAllowed() {
return this.maximumSessionsAllowed;
}
public WebSession getCurrentSession() {
return this.currentSession;
}
}

View File

@@ -1,39 +0,0 @@
/*
* 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.server.authentication;
import reactor.core.publisher.Mono;
import org.springframework.security.web.authentication.session.SessionAuthenticationException;
/**
* Returns a {@link Mono} that terminates with {@link SessionAuthenticationException} when
* the maximum number of sessions for a user has been reached.
*
* @author Marcus da Coregio
* @since 6.3
*/
public final class PreventLoginServerMaximumSessionsExceededHandler implements ServerMaximumSessionsExceededHandler {
@Override
public Mono<Void> handle(MaximumSessionsContext context) {
return context.getCurrentSession()
.invalidate()
.then(Mono.defer(() -> Mono.error(new SessionAuthenticationException("Maximum sessions exceeded"))));
}
}

View File

@@ -1,52 +0,0 @@
/*
* 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.
* 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.server.authentication;
import reactor.core.publisher.Mono;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.session.ReactiveSessionInformation;
import org.springframework.security.core.session.ReactiveSessionRegistry;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.util.Assert;
/**
* An implementation of {@link ServerAuthenticationSuccessHandler} that will register a
* {@link ReactiveSessionInformation} with the provided {@link ReactiveSessionRegistry}.
*
* @author Marcus da Coregio
* @since 6.3
*/
public final class RegisterSessionServerAuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler {
private final ReactiveSessionRegistry sessionRegistry;
public RegisterSessionServerAuthenticationSuccessHandler(ReactiveSessionRegistry sessionRegistry) {
Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
this.sessionRegistry = sessionRegistry;
}
@Override
public Mono<Void> onAuthenticationSuccess(WebFilterExchange exchange, Authentication authentication) {
return exchange.getExchange()
.getSession()
.map((session) -> new ReactiveSessionInformation(authentication.getPrincipal(), session.getId(),
session.getLastAccessTime()))
.flatMap(this.sessionRegistry::saveSessionInformation);
}
}

View File

@@ -1,38 +0,0 @@
/*
* 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.
* 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.server.authentication;
import reactor.core.publisher.Mono;
/**
* Strategy for handling the scenario when the maximum number of sessions for a user has
* been reached.
*
* @author Marcus da Coregio
* @since 6.3
*/
public interface ServerMaximumSessionsExceededHandler {
/**
* Handles the scenario when the maximum number of sessions for a user has been
* reached.
* @param context the context with information about the sessions and the user
* @return an empty {@link Mono} that completes when the handling is done
*/
Mono<Void> handle(MaximumSessionsContext context);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -27,6 +27,7 @@ import org.springframework.security.authentication.AuthenticationServiceExceptio
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver;
import org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager;
import org.springframework.security.web.authentication.RequestMatcherDelegatingAuthenticationManagerResolver;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcherEntry;
import org.springframework.util.Assert;
@@ -111,8 +112,7 @@ public final class ServerWebExchangeDelegatingReactiveAuthenticationManagerResol
}
/**
* A builder for
* {@link ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver}.
* A builder for {@link RequestMatcherDelegatingAuthenticationManagerResolver}.
*/
public static final class Builder {
@@ -128,8 +128,8 @@ public final class ServerWebExchangeDelegatingReactiveAuthenticationManagerResol
* @param matcher the {@link ServerWebExchangeMatcher} to use
* @param manager the {@link ReactiveAuthenticationManager} to use
* @return the
* {@link ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver.Builder}
* for further customizations
* {@link RequestMatcherDelegatingAuthenticationManagerResolver.Builder} for
* further customizations
*/
public ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver.Builder add(
ServerWebExchangeMatcher matcher, ReactiveAuthenticationManager manager) {
@@ -140,11 +140,9 @@ public final class ServerWebExchangeDelegatingReactiveAuthenticationManagerResol
}
/**
* Creates a
* {@link ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver}
* Creates a {@link RequestMatcherDelegatingAuthenticationManagerResolver}
* instance.
* @return the
* {@link ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver}
* @return the {@link RequestMatcherDelegatingAuthenticationManagerResolver}
* instance
*/
public ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver build() {

View File

@@ -1,50 +0,0 @@
/*
* 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.
* 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.server.authentication;
import java.util.function.Function;
import reactor.core.publisher.Mono;
import org.springframework.security.core.Authentication;
/**
* Represents the maximum number of sessions allowed. Use {@link #UNLIMITED} to indicate
* that there is no limit.
*
* @author Marcus da Coregio
* @since 6.3
* @see ConcurrentSessionControlServerAuthenticationSuccessHandler
*/
public interface SessionLimit extends Function<Authentication, Mono<Integer>> {
/**
* Represents unlimited sessions. This is just a shortcut to return
* {@link Mono#empty()} for any user.
*/
SessionLimit UNLIMITED = (authentication) -> Mono.empty();
/**
* 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) {
return (authentication) -> Mono.just(maxSessions);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2019 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.
@@ -47,7 +47,6 @@ public final class IpAddressMatcher implements RequestMatcher {
* come.
*/
public IpAddressMatcher(String ipAddress) {
assertStartsWithHexa(ipAddress);
if (ipAddress.indexOf('/') > 0) {
String[] addressAndMask = StringUtils.split(ipAddress, "/");
ipAddress = addressAndMask[0];
@@ -57,9 +56,8 @@ public final class IpAddressMatcher implements RequestMatcher {
this.nMaskBits = -1;
}
this.requiredAddress = parseAddress(ipAddress);
String finalIpAddress = ipAddress;
Assert.isTrue(this.requiredAddress.getAddress().length * 8 >= this.nMaskBits, () -> String
.format("IP address %s is too short for bitmask of length %d", finalIpAddress, this.nMaskBits));
Assert.isTrue(this.requiredAddress.getAddress().length * 8 >= this.nMaskBits,
String.format("IP address %s is too short for bitmask of length %d", ipAddress, this.nMaskBits));
}
@Override
@@ -68,7 +66,6 @@ public final class IpAddressMatcher implements RequestMatcher {
}
public boolean matches(String address) {
assertStartsWithHexa(address);
InetAddress remoteAddress = parseAddress(address);
if (!this.requiredAddress.getClass().equals(remoteAddress.getClass())) {
return false;
@@ -91,13 +88,6 @@ public final class IpAddressMatcher implements RequestMatcher {
return true;
}
private void assertStartsWithHexa(String ipAddress) {
Assert.isTrue(
ipAddress.charAt(0) == '[' || ipAddress.charAt(0) == ':'
|| Character.digit(ipAddress.charAt(0), 16) != -1,
"ipAddress must start with a [, :, or a hexadecimal digit");
}
private InetAddress parseAddress(String address) {
try {
return InetAddress.getByName(address);