Revert unnecessary commits from main
Issue gh-15016
This commit is contained in:
@@ -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
|
||||
|
||||
-61
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
-60
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
-117
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-111
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+4
-7
@@ -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 {
|
||||
|
||||
|
||||
-46
@@ -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) {
|
||||
}
|
||||
|
||||
}
|
||||
+7
-9
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+19
-29
@@ -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
|
||||
|
||||
+1
-23
@@ -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)) {
|
||||
|
||||
@@ -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
|
||||
|
||||
-103
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
-72
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+1
-11
@@ -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)
|
||||
|
||||
-62
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
-59
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
-39
@@ -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"))));
|
||||
}
|
||||
|
||||
}
|
||||
-52
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
-38
@@ -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);
|
||||
|
||||
}
|
||||
+7
-9
@@ -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() {
|
||||
|
||||
-50
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+3
-13
@@ -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);
|
||||
|
||||
-135
@@ -1,135 +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 jakarta.servlet.http.HttpServletRequest;
|
||||
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.http.HttpHeaders;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.security.authentication.AuthenticationDetailsSource;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.test.web.CodecTestUtils;
|
||||
import org.springframework.security.web.authentication.www.BasicAuthenticationConverter;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
* Tests for {@link DelegatingAuthenticationConverter}.
|
||||
*
|
||||
* @author Max Batischev
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class DelegatingAuthenticationConverterTests {
|
||||
|
||||
private static final String X_AUTH_TOKEN_HEADER = "X-Auth-Token";
|
||||
|
||||
private static final String TEST_X_AUTH_TOKEN = "test-x-auth-token";
|
||||
|
||||
private static final String TEST_CUSTOM_PRINCIPAL = "test_custom_principal";
|
||||
|
||||
private static final String TEST_CUSTOM_CREDENTIALS = "test_custom_credentials";
|
||||
|
||||
private static final String TEST_BASIC_CREDENTIALS = "username:password";
|
||||
|
||||
private static final String INVALID_BASIC_CREDENTIALS = "invalid_credentials";
|
||||
|
||||
private DelegatingAuthenticationConverter converter;
|
||||
|
||||
@Mock
|
||||
private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource;
|
||||
|
||||
@Test
|
||||
public void requestWhenBasicAuthorizationHeaderIsPresentThenAuthenticates() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader(HttpHeaders.AUTHORIZATION, "Basic " + CodecTestUtils.encodeBase64(TEST_BASIC_CREDENTIALS));
|
||||
this.converter = new DelegatingAuthenticationConverter(
|
||||
new BasicAuthenticationConverter(this.authenticationDetailsSource),
|
||||
new TestNullableAuthenticationConverter());
|
||||
|
||||
Authentication authentication = this.converter.convert(request);
|
||||
|
||||
assertThat(authentication).isNotNull();
|
||||
assertThat(authentication.getName()).isEqualTo("username");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenXAuthHeaderIsPresentThenAuthenticates() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader(X_AUTH_TOKEN_HEADER, TEST_X_AUTH_TOKEN);
|
||||
this.converter = new DelegatingAuthenticationConverter(new TestAuthenticationConverter(),
|
||||
new TestNullableAuthenticationConverter());
|
||||
|
||||
Authentication authentication = this.converter.convert(request);
|
||||
|
||||
assertThat(authentication).isNotNull();
|
||||
assertThat(authentication.getName()).isEqualTo(TEST_CUSTOM_PRINCIPAL);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenXAuthHeaderIsPresentThenDoesntAuthenticate() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader(X_AUTH_TOKEN_HEADER, TEST_X_AUTH_TOKEN);
|
||||
this.converter = new DelegatingAuthenticationConverter(new TestNullableAuthenticationConverter());
|
||||
|
||||
Authentication authentication = this.converter.convert(request);
|
||||
|
||||
assertThat(authentication).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenInvalidBasicAuthorizationTokenThenError() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader(HttpHeaders.AUTHORIZATION, "Basic " + CodecTestUtils.encodeBase64(INVALID_BASIC_CREDENTIALS));
|
||||
this.converter = new DelegatingAuthenticationConverter(
|
||||
new BasicAuthenticationConverter(this.authenticationDetailsSource),
|
||||
new TestNullableAuthenticationConverter());
|
||||
|
||||
assertThatExceptionOfType(BadCredentialsException.class).isThrownBy(() -> this.converter.convert(request));
|
||||
}
|
||||
|
||||
private static class TestAuthenticationConverter implements AuthenticationConverter {
|
||||
|
||||
@Override
|
||||
public Authentication convert(HttpServletRequest request) {
|
||||
String header = request.getHeader(X_AUTH_TOKEN_HEADER);
|
||||
if (header != null) {
|
||||
return new TestingAuthenticationToken(TEST_CUSTOM_PRINCIPAL, TEST_CUSTOM_CREDENTIALS);
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class TestNullableAuthenticationConverter implements AuthenticationConverter {
|
||||
|
||||
@Override
|
||||
public Authentication convert(HttpServletRequest request) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
-99
@@ -1,99 +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.io.IOException;
|
||||
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.security.authentication.password.CompromisedPasswordCheckResult;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
|
||||
class HaveIBeenPwnedRestApiPasswordCheckerTests {
|
||||
|
||||
private final String pwnedPasswords = """
|
||||
2CDE4CDCFA5AD7D223BD1800338FBEAA04E:1
|
||||
2CF90F92EE1941547BB13DFC7D0E0AFE504:1
|
||||
2D10A6654B6D75908AE572559542245CBFA:6
|
||||
2D4FCF535FE92B8B950424E16E65EFBFED3:1
|
||||
2D6980B9098804E7A83DC5831BFBAF3927F:1
|
||||
2D8D1B3FAACCA6A3C6A91617B2FA32E2F57:1
|
||||
2DC183F740EE76F27B78EB39C8AD972A757:300185
|
||||
2DE4C0087846D223DBBCCF071614590F300:3
|
||||
2DEA2B1D02714099E4B7A874B4364D518F6:1
|
||||
2E750AE8C4756A20CE040BF3DDF094FA7EC:1
|
||||
2E90B7B3C5C1181D16C48E273D9AC7F3C16:5
|
||||
2E991A9162F24F01826D8AF73CA20F2B430:1
|
||||
2EAE5EA981BFAF29A8869A40BDDADF3879B:2
|
||||
2F1AC09E3846595E436BBDDDD2189358AF9:1
|
||||
""";
|
||||
|
||||
private final MockWebServer server = new MockWebServer();
|
||||
|
||||
private final HaveIBeenPwnedRestApiPasswordChecker passwordChecker = new HaveIBeenPwnedRestApiPasswordChecker();
|
||||
|
||||
@BeforeEach
|
||||
void setup() throws IOException {
|
||||
this.server.start();
|
||||
HttpUrl url = this.server.url("/range/");
|
||||
this.passwordChecker.setRestClient(RestClient.builder().baseUrl(url.toString()).build());
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() throws IOException {
|
||||
this.server.shutdown();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkWhenPasswordIsLeakedThenIsCompromised() throws InterruptedException {
|
||||
this.server.enqueue(new MockResponse().setBody(this.pwnedPasswords).setResponseCode(200));
|
||||
CompromisedPasswordCheckResult check = this.passwordChecker.check("P@ssw0rd");
|
||||
assertThat(check.isCompromised()).isTrue();
|
||||
assertThat(this.server.takeRequest().getPath()).isEqualTo("/range/21BD1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkWhenPasswordNotLeakedThenNotCompromised() {
|
||||
this.server.enqueue(new MockResponse().setBody(this.pwnedPasswords).setResponseCode(200));
|
||||
CompromisedPasswordCheckResult check = this.passwordChecker.check("My1nCr3d!bL3P@SS0W0RD");
|
||||
assertThat(check.isCompromised()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkWhenNoPasswordsReturnedFromApiCallThenNotCompromised() {
|
||||
this.server.enqueue(new MockResponse().setResponseCode(200));
|
||||
CompromisedPasswordCheckResult check = this.passwordChecker.check("123456");
|
||||
assertThat(check.isCompromised()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkWhenResponseStatusNot200ThenNotCompromised() {
|
||||
this.server.enqueue(new MockResponse().setResponseCode(503));
|
||||
assertThatNoException().isThrownBy(() -> this.passwordChecker.check("123456"));
|
||||
this.server.enqueue(new MockResponse().setResponseCode(404));
|
||||
assertThatNoException().isThrownBy(() -> this.passwordChecker.check("123456"));
|
||||
}
|
||||
|
||||
}
|
||||
-105
@@ -1,105 +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.io.IOException;
|
||||
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class HaveIBeenPwnedRestApiReactivePasswordCheckerTests {
|
||||
|
||||
private final String pwnedPasswords = """
|
||||
2CDE4CDCFA5AD7D223BD1800338FBEAA04E:1
|
||||
2CF90F92EE1941547BB13DFC7D0E0AFE504:1
|
||||
2D10A6654B6D75908AE572559542245CBFA:6
|
||||
2D4FCF535FE92B8B950424E16E65EFBFED3:1
|
||||
2D6980B9098804E7A83DC5831BFBAF3927F:1
|
||||
2D8D1B3FAACCA6A3C6A91617B2FA32E2F57:1
|
||||
2DC183F740EE76F27B78EB39C8AD972A757:300185
|
||||
2DE4C0087846D223DBBCCF071614590F300:3
|
||||
2DEA2B1D02714099E4B7A874B4364D518F6:1
|
||||
2E750AE8C4756A20CE040BF3DDF094FA7EC:1
|
||||
2E90B7B3C5C1181D16C48E273D9AC7F3C16:5
|
||||
2E991A9162F24F01826D8AF73CA20F2B430:1
|
||||
2EAE5EA981BFAF29A8869A40BDDADF3879B:2
|
||||
2F1AC09E3846595E436BBDDDD2189358AF9:1
|
||||
""";
|
||||
|
||||
private final MockWebServer server = new MockWebServer();
|
||||
|
||||
private final HaveIBeenPwnedRestApiReactivePasswordChecker passwordChecker = new HaveIBeenPwnedRestApiReactivePasswordChecker();
|
||||
|
||||
@BeforeEach
|
||||
void setup() throws IOException {
|
||||
this.server.start();
|
||||
HttpUrl url = this.server.url("/range/");
|
||||
this.passwordChecker.setWebClient(WebClient.builder().baseUrl(url.toString()).build());
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() throws IOException {
|
||||
this.server.shutdown();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkWhenPasswordIsLeakedThenIsCompromised() throws InterruptedException {
|
||||
this.server.enqueue(new MockResponse().setBody(this.pwnedPasswords).setResponseCode(200));
|
||||
StepVerifier.create(this.passwordChecker.check("P@ssw0rd"))
|
||||
.assertNext((check) -> assertThat(check.isCompromised()).isTrue())
|
||||
.verifyComplete();
|
||||
assertThat(this.server.takeRequest().getPath()).isEqualTo("/range/21BD1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkWhenPasswordNotLeakedThenNotCompromised() {
|
||||
this.server.enqueue(new MockResponse().setBody(this.pwnedPasswords).setResponseCode(200));
|
||||
StepVerifier.create(this.passwordChecker.check("My1nCr3d!bL3P@SS0W0RD"))
|
||||
.assertNext((check) -> assertThat(check.isCompromised()).isFalse())
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkWhenNoPasswordsReturnedFromApiCallThenNotCompromised() {
|
||||
this.server.enqueue(new MockResponse().setResponseCode(200));
|
||||
StepVerifier.create(this.passwordChecker.check("P@ssw0rd"))
|
||||
.assertNext((check) -> assertThat(check.isCompromised()).isFalse())
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkWhenResponseStatusNot200ThenNotCompromised() {
|
||||
this.server.enqueue(new MockResponse().setResponseCode(503));
|
||||
StepVerifier.create(this.passwordChecker.check("123456"))
|
||||
.assertNext((check) -> assertThat(check.isCompromised()).isFalse())
|
||||
.verifyComplete();
|
||||
this.server.enqueue(new MockResponse().setResponseCode(404));
|
||||
StepVerifier.create(this.passwordChecker.check("123456"))
|
||||
.assertNext((check) -> assertThat(check.isCompromised()).isFalse())
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
}
|
||||
-77
@@ -1,77 +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 org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.skyscreamer.jsonassert.JSONAssert;
|
||||
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.jackson2.AbstractMixinTests;
|
||||
import org.springframework.security.jackson2.SimpleGrantedAuthorityMixinTests;
|
||||
import org.springframework.security.web.authentication.switchuser.SwitchUserGrantedAuthority;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Markus Heiden
|
||||
* @since 6.3
|
||||
*/
|
||||
public class SwitchUserGrantedAuthorityMixInTests extends AbstractMixinTests {
|
||||
|
||||
// language=JSON
|
||||
private static final String SWITCH_JSON = """
|
||||
{
|
||||
"@class": "org.springframework.security.web.authentication.switchuser.SwitchUserGrantedAuthority",
|
||||
"role": "switched",
|
||||
"source": {
|
||||
"@class": "org.springframework.security.authentication.UsernamePasswordAuthenticationToken",
|
||||
"principal": "principal",
|
||||
"credentials": "credentials",
|
||||
"authenticated": true,
|
||||
"details": null,
|
||||
"authorities": %s
|
||||
}
|
||||
}
|
||||
""".formatted(SimpleGrantedAuthorityMixinTests.AUTHORITIES_ARRAYLIST_JSON);
|
||||
|
||||
private Authentication source;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
this.source = new UsernamePasswordAuthenticationToken("principal", "credentials",
|
||||
AuthorityUtils.createAuthorityList("ROLE_USER"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serializeWhenPrincipalCredentialsAuthoritiesThenSuccess() throws Exception {
|
||||
SwitchUserGrantedAuthority expected = new SwitchUserGrantedAuthority("switched", this.source);
|
||||
String serializedJson = this.mapper.writeValueAsString(expected);
|
||||
JSONAssert.assertEquals(SWITCH_JSON, serializedJson, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deserializeWhenSourceIsUsernamePasswordAuthenticationTokenThenSuccess() throws Exception {
|
||||
SwitchUserGrantedAuthority deserialized = this.mapper.readValue(SWITCH_JSON, SwitchUserGrantedAuthority.class);
|
||||
assertThat(deserialized).isNotNull();
|
||||
assertThat(deserialized.getAuthority()).isEqualTo("switched");
|
||||
assertThat(deserialized.getSource()).isEqualTo(this.source);
|
||||
}
|
||||
|
||||
}
|
||||
+2
-52
@@ -69,26 +69,9 @@ public class CurrentSecurityContextArgumentResolverTests {
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void supportsParameterNoAnnotationWrongType() {
|
||||
assertThat(this.resolver.supportsParameter(showSecurityContextNoAnnotationTypeMismatch())).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void supportsParameterNoAnnotation() {
|
||||
assertThat(this.resolver.supportsParameter(showSecurityContextNoAnnotation())).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void supportsParameterCustomSecurityContextNoAnnotation() {
|
||||
assertThat(this.resolver.supportsParameter(showSecurityContextWithCustomSecurityContextNoAnnotation()))
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void supportsParameterNoAnnotationCustomType() {
|
||||
assertThat(this.resolver.supportsParameter(showSecurityContextWithCustomSecurityContextNoAnnotation()))
|
||||
.isTrue();
|
||||
assertThat(this.resolver.supportsParameter(showSecurityContextNoAnnotation())).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -105,24 +88,6 @@ public class CurrentSecurityContextArgumentResolverTests {
|
||||
assertThat(customSecurityContext.getAuthentication().getPrincipal()).isEqualTo(principal);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveArgumentWithCustomSecurityContextNoAnnotation() {
|
||||
String principal = "custom_security_context";
|
||||
setAuthenticationPrincipalWithCustomSecurityContext(principal);
|
||||
CustomSecurityContext customSecurityContext = (CustomSecurityContext) this.resolver
|
||||
.resolveArgument(showSecurityContextWithCustomSecurityContextNoAnnotation(), null, null, null);
|
||||
assertThat(customSecurityContext.getAuthentication().getPrincipal()).isEqualTo(principal);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveArgumentWithNoAnnotation() {
|
||||
String principal = "custom_security_context";
|
||||
setAuthenticationPrincipal(principal);
|
||||
SecurityContext securityContext = (SecurityContext) this.resolver
|
||||
.resolveArgument(showSecurityContextNoAnnotation(), null, null, null);
|
||||
assertThat(securityContext.getAuthentication().getPrincipal()).isEqualTo(principal);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveArgumentWithCustomSecurityContextTypeMatch() {
|
||||
String principal = "custom_security_context_type_match";
|
||||
@@ -247,12 +212,8 @@ public class CurrentSecurityContextArgumentResolverTests {
|
||||
.resolveArgument(showCurrentSecurityWithErrorOnInvalidTypeMisMatch(), null, null, null));
|
||||
}
|
||||
|
||||
private MethodParameter showSecurityContextNoAnnotationTypeMismatch() {
|
||||
return getMethodParameter("showSecurityContextNoAnnotation", String.class);
|
||||
}
|
||||
|
||||
private MethodParameter showSecurityContextNoAnnotation() {
|
||||
return getMethodParameter("showSecurityContextNoAnnotation", SecurityContext.class);
|
||||
return getMethodParameter("showSecurityContextNoAnnotation", String.class);
|
||||
}
|
||||
|
||||
private MethodParameter showSecurityContextAnnotation() {
|
||||
@@ -315,11 +276,6 @@ public class CurrentSecurityContextArgumentResolverTests {
|
||||
return getMethodParameter("showCurrentSecurityWithErrorOnInvalidTypeMisMatch", String.class);
|
||||
}
|
||||
|
||||
public MethodParameter showSecurityContextWithCustomSecurityContextNoAnnotation() {
|
||||
return getMethodParameter("showSecurityContextWithCustomSecurityContextNoAnnotation",
|
||||
CustomSecurityContext.class);
|
||||
}
|
||||
|
||||
private MethodParameter getMethodParameter(String methodName, Class<?>... paramTypes) {
|
||||
Method method = ReflectionUtils.findMethod(TestController.class, methodName, paramTypes);
|
||||
return new MethodParameter(method, 0);
|
||||
@@ -402,12 +358,6 @@ public class CurrentSecurityContextArgumentResolverTests {
|
||||
@CurrentSecurityWithErrorOnInvalidType String typeMisMatch) {
|
||||
}
|
||||
|
||||
public void showSecurityContextNoAnnotation(SecurityContext context) {
|
||||
}
|
||||
|
||||
public void showSecurityContextWithCustomSecurityContextNoAnnotation(CustomSecurityContext context) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class CustomSecurityContext implements SecurityContext {
|
||||
|
||||
-61
@@ -69,14 +69,6 @@ public class CurrentSecurityContextArgumentResolverTests {
|
||||
|
||||
ResolvableMethod securityContextMethod = ResolvableMethod.on(getClass()).named("securityContext").build();
|
||||
|
||||
ResolvableMethod securityContextNoAnnotationMethod = ResolvableMethod.on(getClass())
|
||||
.named("securityContextNoAnnotation")
|
||||
.build();
|
||||
|
||||
ResolvableMethod customSecurityContextNoAnnotationMethod = ResolvableMethod.on(getClass())
|
||||
.named("customSecurityContextNoAnnotation")
|
||||
.build();
|
||||
|
||||
ResolvableMethod securityContextWithAuthentication = ResolvableMethod.on(getClass())
|
||||
.named("securityContextWithAuthentication")
|
||||
.build();
|
||||
@@ -95,19 +87,6 @@ public class CurrentSecurityContextArgumentResolverTests {
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void supportsParameterCurrentSecurityContextNoAnnotation() {
|
||||
assertThat(this.resolver
|
||||
.supportsParameter(this.securityContextNoAnnotationMethod.arg(Mono.class, SecurityContext.class))).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void supportsParameterCurrentCustomSecurityContextNoAnnotation() {
|
||||
assertThat(this.resolver.supportsParameter(
|
||||
this.customSecurityContextNoAnnotationMethod.arg(Mono.class, CustomSecurityContext.class)))
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void supportsParameterWithAuthentication() {
|
||||
assertThat(this.resolver
|
||||
@@ -144,40 +123,6 @@ public class CurrentSecurityContextArgumentResolverTests {
|
||||
ReactiveSecurityContextHolder.clearContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveArgumentWithSecurityContextNoAnnotation() {
|
||||
MethodParameter parameter = ResolvableMethod.on(getClass())
|
||||
.named("securityContextNoAnnotation")
|
||||
.build()
|
||||
.arg(Mono.class, SecurityContext.class);
|
||||
Authentication auth = buildAuthenticationWithPrincipal("hello");
|
||||
Context context = ReactiveSecurityContextHolder.withAuthentication(auth);
|
||||
Mono<Object> argument = this.resolver.resolveArgument(parameter, this.bindingContext, this.exchange);
|
||||
SecurityContext securityContext = (SecurityContext) argument.contextWrite(context)
|
||||
.cast(Mono.class)
|
||||
.block()
|
||||
.block();
|
||||
assertThat(securityContext.getAuthentication()).isSameAs(auth);
|
||||
ReactiveSecurityContextHolder.clearContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveArgumentWithCustomSecurityContextNoAnnotation() {
|
||||
MethodParameter parameter = ResolvableMethod.on(getClass())
|
||||
.named("customSecurityContextNoAnnotation")
|
||||
.build()
|
||||
.arg(Mono.class, CustomSecurityContext.class);
|
||||
Authentication auth = buildAuthenticationWithPrincipal("hello");
|
||||
Context context = ReactiveSecurityContextHolder.withSecurityContext(Mono.just(new CustomSecurityContext(auth)));
|
||||
Mono<Object> argument = this.resolver.resolveArgument(parameter, this.bindingContext, this.exchange);
|
||||
CustomSecurityContext securityContext = (CustomSecurityContext) argument.contextWrite(context)
|
||||
.cast(Mono.class)
|
||||
.block()
|
||||
.block();
|
||||
assertThat(securityContext.getAuthentication()).isSameAs(auth);
|
||||
ReactiveSecurityContextHolder.clearContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveArgumentWithCustomSecurityContext() {
|
||||
MethodParameter parameter = ResolvableMethod.on(getClass())
|
||||
@@ -405,12 +350,6 @@ public class CurrentSecurityContextArgumentResolverTests {
|
||||
void securityContext(@CurrentSecurityContext Mono<SecurityContext> monoSecurityContext) {
|
||||
}
|
||||
|
||||
void securityContextNoAnnotation(Mono<SecurityContext> securityContextMono) {
|
||||
}
|
||||
|
||||
void customSecurityContextNoAnnotation(Mono<CustomSecurityContext> securityContextMono) {
|
||||
}
|
||||
|
||||
void customSecurityContext(@CurrentSecurityContext Mono<SecurityContext> monoSecurityContext) {
|
||||
}
|
||||
|
||||
|
||||
-137
@@ -1,137 +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 org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author DingHao
|
||||
* @since 6.3
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class DelegatingServerAuthenticationConverterTests {
|
||||
|
||||
DelegatingServerAuthenticationConverter converter = new DelegatingServerAuthenticationConverter(
|
||||
new ApkServerAuthenticationConverter(), new ServerHttpBasicAuthenticationConverter());
|
||||
|
||||
MockServerHttpRequest.BaseBuilder<?> request = MockServerHttpRequest.get("/");
|
||||
|
||||
@Test
|
||||
public void applyServerHttpBasicAuthenticationConverter() {
|
||||
Mono<Authentication> result = this.converter.convert(MockServerWebExchange
|
||||
.from(this.request.header(HttpHeaders.AUTHORIZATION, "Basic dXNlcjpwYXNzd29yZA==").build()));
|
||||
UsernamePasswordAuthenticationToken authentication = result.cast(UsernamePasswordAuthenticationToken.class)
|
||||
.block();
|
||||
assertThat(authentication.getPrincipal()).isEqualTo("user");
|
||||
assertThat(authentication.getCredentials()).isEqualTo("password");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void applyApkServerAuthenticationConverter() {
|
||||
String apk = "123e4567e89b12d3a456426655440000";
|
||||
Mono<Authentication> result = this.converter
|
||||
.convert(MockServerWebExchange.from(this.request.header("APK", apk).build()));
|
||||
ApkTokenAuthenticationToken authentication = result.cast(ApkTokenAuthenticationToken.class).block();
|
||||
assertThat(authentication.getApk()).isEqualTo(apk);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void applyServerHttpBasicAuthenticationConverterWhenFirstAuthenticationConverterException() {
|
||||
this.converter.setContinueOnError(true);
|
||||
Mono<Authentication> result = this.converter.convert(MockServerWebExchange.from(this.request.header("APK", "")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Basic dXNlcjpwYXNzd29yZA==")
|
||||
.build()));
|
||||
UsernamePasswordAuthenticationToken authentication = result.cast(UsernamePasswordAuthenticationToken.class)
|
||||
.block();
|
||||
assertThat(authentication.getPrincipal()).isEqualTo("user");
|
||||
assertThat(authentication.getCredentials()).isEqualTo("password");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void applyApkServerAuthenticationConverterThenDelegate2NotInvokedAndError() {
|
||||
Mono<Authentication> result = this.converter.convert(MockServerWebExchange.from(this.request.header("APK", "")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Basic dXNlcjpwYXNzd29yZA==")
|
||||
.build()));
|
||||
StepVerifier.create(result.cast(ApkTokenAuthenticationToken.class))
|
||||
.expectError(ApkAuthenticationException.class)
|
||||
.verify();
|
||||
}
|
||||
|
||||
public static class ApkServerAuthenticationConverter implements ServerAuthenticationConverter {
|
||||
|
||||
@Override
|
||||
public Mono<Authentication> convert(ServerWebExchange exchange) {
|
||||
return Mono.fromCallable(() -> exchange.getRequest().getHeaders().getFirst("APK")).map((apk) -> {
|
||||
if (apk.isEmpty()) {
|
||||
throw new ApkAuthenticationException("apk invalid");
|
||||
}
|
||||
return new ApkTokenAuthenticationToken(apk);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class ApkTokenAuthenticationToken extends AbstractAuthenticationToken {
|
||||
|
||||
private final String apk;
|
||||
|
||||
public ApkTokenAuthenticationToken(String apk) {
|
||||
super(AuthorityUtils.NO_AUTHORITIES);
|
||||
this.apk = apk;
|
||||
}
|
||||
|
||||
public String getApk() {
|
||||
return this.apk;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return this.getApk();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return this.getApk();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class ApkAuthenticationException extends AuthenticationException {
|
||||
|
||||
public ApkAuthenticationException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+4
-12
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -17,8 +17,6 @@
|
||||
package org.springframework.security.web.server.authentication;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
@@ -76,15 +74,9 @@ public class DelegatingServerAuthenticationSuccessHandlerTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenNullListThenIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new DelegatingServerAuthenticationSuccessHandler(
|
||||
(List<ServerAuthenticationSuccessHandler>) null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenEmptyListThenIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new DelegatingServerAuthenticationSuccessHandler(Collections.emptyList()));
|
||||
public void constructorWhenEmptyThenIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(
|
||||
() -> new DelegatingServerAuthenticationSuccessHandler(new ServerAuthenticationSuccessHandler[0]));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
-171
@@ -1,171 +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.session;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpResponse;
|
||||
import org.springframework.mock.web.server.MockWebSession;
|
||||
import org.springframework.security.authentication.TestAuthentication;
|
||||
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.security.web.server.authentication.ConcurrentSessionControlServerAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.server.authentication.MaximumSessionsContext;
|
||||
import org.springframework.security.web.server.authentication.ServerMaximumSessionsExceededHandler;
|
||||
import org.springframework.security.web.server.authentication.SessionLimit;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
|
||||
/**
|
||||
* Tests for {@link ConcurrentSessionControlServerAuthenticationSuccessHandler}.
|
||||
*
|
||||
* @author Marcus da Coregio
|
||||
*/
|
||||
class ConcurrentSessionControlServerAuthenticationSuccessHandlerTests {
|
||||
|
||||
private ConcurrentSessionControlServerAuthenticationSuccessHandler strategy;
|
||||
|
||||
ReactiveSessionRegistry sessionRegistry = mock();
|
||||
|
||||
ServerWebExchange exchange = mock();
|
||||
|
||||
WebFilterChain chain = mock();
|
||||
|
||||
ServerMaximumSessionsExceededHandler handler = mock();
|
||||
|
||||
ArgumentCaptor<MaximumSessionsContext> contextCaptor = ArgumentCaptor.forClass(MaximumSessionsContext.class);
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
given(this.exchange.getResponse()).willReturn(new MockServerHttpResponse());
|
||||
given(this.exchange.getRequest()).willReturn(MockServerHttpRequest.get("/").build());
|
||||
given(this.exchange.getSession()).willReturn(Mono.just(new MockWebSession()));
|
||||
given(this.handler.handle(any())).willReturn(Mono.empty());
|
||||
this.strategy = new ConcurrentSessionControlServerAuthenticationSuccessHandler(this.sessionRegistry,
|
||||
this.handler);
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructorWhenNullRegistryThenException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new ConcurrentSessionControlServerAuthenticationSuccessHandler(null, this.handler))
|
||||
.withMessage("sessionRegistry cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructorWhenNullHandlerThenException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(
|
||||
() -> new ConcurrentSessionControlServerAuthenticationSuccessHandler(this.sessionRegistry, null))
|
||||
.withMessage("maximumSessionsExceededHandler cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void setMaximumSessionsForAuthenticationWhenNullThenException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.strategy.setSessionLimit(null))
|
||||
.withMessage("sessionLimit cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void onAuthenticationWhenSessionLimitIsUnlimitedThenDoNothing() {
|
||||
ServerMaximumSessionsExceededHandler handler = mock(ServerMaximumSessionsExceededHandler.class);
|
||||
this.strategy = new ConcurrentSessionControlServerAuthenticationSuccessHandler(this.sessionRegistry, handler);
|
||||
this.strategy.setSessionLimit(SessionLimit.UNLIMITED);
|
||||
this.strategy.onAuthenticationSuccess(null, TestAuthentication.authenticatedUser()).block();
|
||||
verifyNoInteractions(handler, this.sessionRegistry);
|
||||
}
|
||||
|
||||
@Test
|
||||
void onAuthenticationWhenMaximumSessionsIsOneAndExceededThenHandlerIsCalled() {
|
||||
Authentication authentication = TestAuthentication.authenticatedUser();
|
||||
List<ReactiveSessionInformation> sessions = Arrays.asList(createSessionInformation("100"),
|
||||
createSessionInformation("101"));
|
||||
given(this.sessionRegistry.getAllSessions(authentication.getPrincipal()))
|
||||
.willReturn(Flux.fromIterable(sessions));
|
||||
this.strategy.onAuthenticationSuccess(new WebFilterExchange(this.exchange, this.chain), authentication).block();
|
||||
verify(this.handler).handle(this.contextCaptor.capture());
|
||||
assertThat(this.contextCaptor.getValue().getMaximumSessionsAllowed()).isEqualTo(1);
|
||||
assertThat(this.contextCaptor.getValue().getSessions()).isEqualTo(sessions);
|
||||
assertThat(this.contextCaptor.getValue().getAuthentication()).isEqualTo(authentication);
|
||||
}
|
||||
|
||||
@Test
|
||||
void onAuthenticationWhenMaximumSessionsIsGreaterThanOneAndExceededThenHandlerIsCalled() {
|
||||
this.strategy.setSessionLimit(SessionLimit.of(5));
|
||||
Authentication authentication = TestAuthentication.authenticatedUser();
|
||||
List<ReactiveSessionInformation> sessions = Arrays.asList(createSessionInformation("100"),
|
||||
createSessionInformation("101"), createSessionInformation("102"), createSessionInformation("103"),
|
||||
createSessionInformation("104"));
|
||||
given(this.sessionRegistry.getAllSessions(authentication.getPrincipal()))
|
||||
.willReturn(Flux.fromIterable(sessions));
|
||||
this.strategy.onAuthenticationSuccess(new WebFilterExchange(this.exchange, this.chain), authentication).block();
|
||||
verify(this.handler).handle(this.contextCaptor.capture());
|
||||
assertThat(this.contextCaptor.getValue().getMaximumSessionsAllowed()).isEqualTo(5);
|
||||
assertThat(this.contextCaptor.getValue().getSessions()).isEqualTo(sessions);
|
||||
assertThat(this.contextCaptor.getValue().getAuthentication()).isEqualTo(authentication);
|
||||
}
|
||||
|
||||
@Test
|
||||
void onAuthenticationWhenMaximumSessionsForUsersAreDifferentThenHandlerIsCalledWhereNeeded() {
|
||||
Authentication user = TestAuthentication.authenticatedUser();
|
||||
Authentication admin = TestAuthentication.authenticatedAdmin();
|
||||
this.strategy.setSessionLimit((authentication) -> {
|
||||
if (authentication.equals(user)) {
|
||||
return Mono.just(1);
|
||||
}
|
||||
return Mono.just(3);
|
||||
});
|
||||
|
||||
List<ReactiveSessionInformation> userSessions = Arrays.asList(createSessionInformation("100"));
|
||||
List<ReactiveSessionInformation> adminSessions = Arrays.asList(createSessionInformation("200"),
|
||||
createSessionInformation("201"));
|
||||
|
||||
given(this.sessionRegistry.getAllSessions(user.getPrincipal())).willReturn(Flux.fromIterable(userSessions));
|
||||
given(this.sessionRegistry.getAllSessions(admin.getPrincipal())).willReturn(Flux.fromIterable(adminSessions));
|
||||
|
||||
this.strategy.onAuthenticationSuccess(new WebFilterExchange(this.exchange, this.chain), user).block();
|
||||
this.strategy.onAuthenticationSuccess(new WebFilterExchange(this.exchange, this.chain), admin).block();
|
||||
verify(this.handler).handle(this.contextCaptor.capture());
|
||||
assertThat(this.contextCaptor.getValue().getMaximumSessionsAllowed()).isEqualTo(1);
|
||||
assertThat(this.contextCaptor.getValue().getSessions()).isEqualTo(userSessions);
|
||||
assertThat(this.contextCaptor.getValue().getAuthentication()).isEqualTo(user);
|
||||
}
|
||||
|
||||
private ReactiveSessionInformation createSessionInformation(String sessionId) {
|
||||
return new ReactiveSessionInformation(sessionId, "principal", Instant.now());
|
||||
}
|
||||
|
||||
}
|
||||
-106
@@ -1,106 +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.session;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.security.authentication.TestAuthentication;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.session.InMemoryReactiveSessionRegistry;
|
||||
import org.springframework.security.core.session.ReactiveSessionInformation;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link InMemoryReactiveSessionRegistry}.
|
||||
*/
|
||||
class InMemoryReactiveSessionRegistryTests {
|
||||
|
||||
InMemoryReactiveSessionRegistry sessionRegistry = new InMemoryReactiveSessionRegistry();
|
||||
|
||||
Instant now = LocalDate.of(2023, 11, 21).atStartOfDay().toInstant(ZoneOffset.UTC);
|
||||
|
||||
@Test
|
||||
void saveWhenPrincipalThenRegisterPrincipalSession() {
|
||||
Authentication authentication = TestAuthentication.authenticatedUser();
|
||||
ReactiveSessionInformation sessionInformation = new ReactiveSessionInformation(authentication.getPrincipal(),
|
||||
"1234", this.now);
|
||||
this.sessionRegistry.saveSessionInformation(sessionInformation).block();
|
||||
List<ReactiveSessionInformation> principalSessions = this.sessionRegistry
|
||||
.getAllSessions(authentication.getPrincipal())
|
||||
.collectList()
|
||||
.block();
|
||||
assertThat(principalSessions).hasSize(1);
|
||||
assertThat(this.sessionRegistry.getSessionInformation("1234").block()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAllSessionsWhenMultipleSessionsThenReturnAll() {
|
||||
Authentication authentication = TestAuthentication.authenticatedUser();
|
||||
ReactiveSessionInformation sessionInformation1 = new ReactiveSessionInformation(authentication.getPrincipal(),
|
||||
"1234", this.now);
|
||||
ReactiveSessionInformation sessionInformation2 = new ReactiveSessionInformation(authentication.getPrincipal(),
|
||||
"4321", this.now);
|
||||
ReactiveSessionInformation sessionInformation3 = new ReactiveSessionInformation(authentication.getPrincipal(),
|
||||
"9876", this.now);
|
||||
this.sessionRegistry.saveSessionInformation(sessionInformation1).block();
|
||||
this.sessionRegistry.saveSessionInformation(sessionInformation2).block();
|
||||
this.sessionRegistry.saveSessionInformation(sessionInformation3).block();
|
||||
List<ReactiveSessionInformation> sessions = this.sessionRegistry.getAllSessions(authentication.getPrincipal())
|
||||
.collectList()
|
||||
.block();
|
||||
assertThat(sessions).hasSize(3);
|
||||
assertThat(this.sessionRegistry.getSessionInformation("1234").block()).isNotNull();
|
||||
assertThat(this.sessionRegistry.getSessionInformation("4321").block()).isNotNull();
|
||||
assertThat(this.sessionRegistry.getSessionInformation("9876").block()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeSessionInformationThenSessionIsRemoved() {
|
||||
Authentication authentication = TestAuthentication.authenticatedUser();
|
||||
ReactiveSessionInformation sessionInformation = new ReactiveSessionInformation(authentication.getPrincipal(),
|
||||
"1234", this.now);
|
||||
this.sessionRegistry.saveSessionInformation(sessionInformation).block();
|
||||
this.sessionRegistry.removeSessionInformation("1234").block();
|
||||
List<ReactiveSessionInformation> sessions = this.sessionRegistry.getAllSessions(authentication.getName())
|
||||
.collectList()
|
||||
.block();
|
||||
assertThat(this.sessionRegistry.getSessionInformation("1234").block()).isNull();
|
||||
assertThat(sessions).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateLastAccessTimeThenUpdated() {
|
||||
Authentication authentication = TestAuthentication.authenticatedUser();
|
||||
ReactiveSessionInformation sessionInformation = new ReactiveSessionInformation(authentication.getPrincipal(),
|
||||
"1234", this.now);
|
||||
this.sessionRegistry.saveSessionInformation(sessionInformation).block();
|
||||
ReactiveSessionInformation saved = this.sessionRegistry.getSessionInformation("1234").block();
|
||||
assertThat(saved.getLastAccessTime()).isNotNull();
|
||||
Instant lastAccessTimeBefore = saved.getLastAccessTime();
|
||||
this.sessionRegistry.updateLastAccessTime("1234").block();
|
||||
saved = this.sessionRegistry.getSessionInformation("1234").block();
|
||||
assertThat(saved.getLastAccessTime()).isNotNull();
|
||||
assertThat(saved.getLastAccessTime()).isAfter(lastAccessTimeBefore);
|
||||
}
|
||||
|
||||
}
|
||||
-115
@@ -1,115 +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.session;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.session.ReactiveSessionInformation;
|
||||
import org.springframework.security.web.server.authentication.InvalidateLeastUsedServerMaximumSessionsExceededHandler;
|
||||
import org.springframework.security.web.server.authentication.MaximumSessionsContext;
|
||||
import org.springframework.web.server.session.InMemoryWebSessionStore;
|
||||
import org.springframework.web.server.session.WebSessionStore;
|
||||
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
|
||||
/**
|
||||
* Tests for {@link InvalidateLeastUsedServerMaximumSessionsExceededHandler}
|
||||
*
|
||||
* @author Marcus da Coregio
|
||||
*/
|
||||
class InvalidateLeastUsedServerMaximumSessionsExceededHandlerTests {
|
||||
|
||||
InvalidateLeastUsedServerMaximumSessionsExceededHandler handler;
|
||||
|
||||
WebSessionStore webSessionStore = spy(new InMemoryWebSessionStore());
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
this.handler = new InvalidateLeastUsedServerMaximumSessionsExceededHandler(this.webSessionStore);
|
||||
}
|
||||
|
||||
@Test
|
||||
void handleWhenInvokedThenInvalidatesLeastRecentlyUsedSessions() {
|
||||
ReactiveSessionInformation session1 = mock(ReactiveSessionInformation.class);
|
||||
ReactiveSessionInformation session2 = mock(ReactiveSessionInformation.class);
|
||||
given(session1.getLastAccessTime()).willReturn(Instant.ofEpochMilli(1700827760010L));
|
||||
given(session2.getLastAccessTime()).willReturn(Instant.ofEpochMilli(1700827760000L));
|
||||
given(session2.getSessionId()).willReturn("session2");
|
||||
given(session2.invalidate()).willReturn(Mono.empty());
|
||||
|
||||
MaximumSessionsContext context = new MaximumSessionsContext(mock(Authentication.class),
|
||||
List.of(session1, session2), 2, null);
|
||||
|
||||
this.handler.handle(context).block();
|
||||
|
||||
verify(session2).invalidate();
|
||||
verify(session1).getLastAccessTime(); // used by comparator to sort the sessions
|
||||
verify(session2).getLastAccessTime(); // used by comparator to sort the sessions
|
||||
verify(session2).getSessionId(); // used to invalidate session against the
|
||||
// WebSessionStore
|
||||
verify(this.webSessionStore).removeSession("session2");
|
||||
verifyNoMoreInteractions(this.webSessionStore);
|
||||
verifyNoMoreInteractions(session2);
|
||||
verifyNoMoreInteractions(session1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void handleWhenMoreThanOneSessionToInvalidateThenInvalidatesAllOfThem() {
|
||||
ReactiveSessionInformation session1 = mock(ReactiveSessionInformation.class);
|
||||
ReactiveSessionInformation session2 = mock(ReactiveSessionInformation.class);
|
||||
ReactiveSessionInformation session3 = mock(ReactiveSessionInformation.class);
|
||||
given(session1.getLastAccessTime()).willReturn(Instant.ofEpochMilli(1700827760010L));
|
||||
given(session2.getLastAccessTime()).willReturn(Instant.ofEpochMilli(1700827760020L));
|
||||
given(session3.getLastAccessTime()).willReturn(Instant.ofEpochMilli(1700827760030L));
|
||||
given(session1.invalidate()).willReturn(Mono.empty());
|
||||
given(session2.invalidate()).willReturn(Mono.empty());
|
||||
given(session1.getSessionId()).willReturn("session1");
|
||||
given(session2.getSessionId()).willReturn("session2");
|
||||
|
||||
MaximumSessionsContext context = new MaximumSessionsContext(mock(Authentication.class),
|
||||
List.of(session1, session2, session3), 2, null);
|
||||
this.handler.handle(context).block();
|
||||
|
||||
// @formatter:off
|
||||
verify(session1).invalidate();
|
||||
verify(session2).invalidate();
|
||||
verify(session1).getSessionId();
|
||||
verify(session2).getSessionId();
|
||||
verify(session1, atLeastOnce()).getLastAccessTime(); // used by comparator to sort the sessions
|
||||
verify(session2, atLeastOnce()).getLastAccessTime(); // used by comparator to sort the sessions
|
||||
verify(session3, atLeastOnce()).getLastAccessTime(); // used by comparator to sort the sessions
|
||||
verify(this.webSessionStore).removeSession("session1");
|
||||
verify(this.webSessionStore).removeSession("session2");
|
||||
verifyNoMoreInteractions(this.webSessionStore);
|
||||
verifyNoMoreInteractions(session1);
|
||||
verifyNoMoreInteractions(session2);
|
||||
verifyNoMoreInteractions(session3);
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
}
|
||||
-57
@@ -1,57 +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.session;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.security.authentication.TestAuthentication;
|
||||
import org.springframework.security.web.authentication.session.SessionAuthenticationException;
|
||||
import org.springframework.security.web.server.authentication.MaximumSessionsContext;
|
||||
import org.springframework.security.web.server.authentication.PreventLoginServerMaximumSessionsExceededHandler;
|
||||
import org.springframework.web.server.WebSession;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for {@link PreventLoginServerMaximumSessionsExceededHandler}.
|
||||
*
|
||||
* @author Marcus da Coregio
|
||||
*/
|
||||
class PreventLoginServerMaximumSessionsExceededHandlerTests {
|
||||
|
||||
@Test
|
||||
void handleWhenInvokedThenInvalidateWebSessionAndThrowsSessionAuthenticationException() {
|
||||
PreventLoginServerMaximumSessionsExceededHandler handler = new PreventLoginServerMaximumSessionsExceededHandler();
|
||||
WebSession webSession = mock();
|
||||
given(webSession.invalidate()).willReturn(Mono.empty());
|
||||
MaximumSessionsContext context = new MaximumSessionsContext(TestAuthentication.authenticatedUser(),
|
||||
Collections.emptyList(), 1, webSession);
|
||||
StepVerifier.create(handler.handle(context)).expectErrorSatisfies((ex) -> {
|
||||
assertThat(ex).isInstanceOf(SessionAuthenticationException.class);
|
||||
assertThat(ex.getMessage()).isEqualTo("Maximum sessions exceeded");
|
||||
}).verify();
|
||||
verify(webSession).invalidate();
|
||||
}
|
||||
|
||||
}
|
||||
-84
@@ -1,84 +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.session;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.mock.web.server.MockWebSession;
|
||||
import org.springframework.security.authentication.TestAuthentication;
|
||||
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.security.web.server.authentication.RegisterSessionServerAuthenticationSuccessHandler;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import org.springframework.web.server.WebSession;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class RegisterSessionServerAuthenticationSuccessHandlerTests {
|
||||
|
||||
@InjectMocks
|
||||
RegisterSessionServerAuthenticationSuccessHandler strategy;
|
||||
|
||||
@Mock
|
||||
ReactiveSessionRegistry sessionRegistry;
|
||||
|
||||
@Mock
|
||||
WebFilterChain filterChain;
|
||||
|
||||
WebSession session = new MockWebSession();
|
||||
|
||||
ServerWebExchange serverWebExchange = MockServerWebExchange.builder(MockServerHttpRequest.get(""))
|
||||
.session(this.session)
|
||||
.build();
|
||||
|
||||
@Test
|
||||
void constructorWhenSessionRegistryNullThenException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new RegisterSessionServerAuthenticationSuccessHandler(null))
|
||||
.withMessage("sessionRegistry cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void onAuthenticationWhenSessionExistsThenSaveSessionInformation() {
|
||||
given(this.sessionRegistry.saveSessionInformation(any())).willReturn(Mono.empty());
|
||||
WebFilterExchange webFilterExchange = new WebFilterExchange(this.serverWebExchange, this.filterChain);
|
||||
Authentication authentication = TestAuthentication.authenticatedUser();
|
||||
this.strategy.onAuthenticationSuccess(webFilterExchange, authentication).block();
|
||||
ArgumentCaptor<ReactiveSessionInformation> captor = ArgumentCaptor.forClass(ReactiveSessionInformation.class);
|
||||
verify(this.sessionRegistry).saveSessionInformation(captor.capture());
|
||||
assertThat(captor.getValue().getSessionId()).isEqualTo(this.session.getId());
|
||||
assertThat(captor.getValue().getLastAccessTime()).isEqualTo(this.session.getLastAccessTime());
|
||||
assertThat(captor.getValue().getPrincipal()).isEqualTo(authentication.getPrincipal());
|
||||
}
|
||||
|
||||
}
|
||||
-6
@@ -105,10 +105,4 @@ public class IpAddressMatcherTests {
|
||||
"fe80::21f:5bff:fe33:bd68", 129));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidAddressThenIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new IpAddressMatcher("invalid-ip"))
|
||||
.withMessage("ipAddress must start with a [, :, or a hexadecimal digit");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user