Add Max Sessions on WebFlux
Closes gh-6192
This commit is contained in:
committed by
Marcus Hert Da Coregio
parent
64feedf67e
commit
57ab15127a
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -19,12 +19,15 @@ package org.springframework.security.web.authentication.session;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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.
|
||||
*
|
||||
* @author Luke Taylor
|
||||
* @since 3.0
|
||||
* @see SessionAuthenticationStrategy
|
||||
* @see ServerSessionAuthenticationStrategy
|
||||
*/
|
||||
public class SessionAuthenticationException extends AuthenticationException {
|
||||
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* 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.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 SessionLimit sessionLimit = SessionLimit.of(1);
|
||||
|
||||
private ServerMaximumSessionsExceededHandler maximumSessionsExceededHandler = new InvalidateLeastUsedServerMaximumSessionsExceededHandler();
|
||||
|
||||
public ConcurrentSessionControlServerAuthenticationSuccessHandler(ReactiveSessionRegistry sessionRegistry) {
|
||||
Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
|
||||
this.sessionRegistry = sessionRegistry;
|
||||
}
|
||||
|
||||
@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(), false)
|
||||
.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));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link ServerMaximumSessionsExceededHandler} to use. The default is
|
||||
* {@link InvalidateLeastUsedServerMaximumSessionsExceededHandler}.
|
||||
* @param maximumSessionsExceededHandler the
|
||||
* {@link ServerMaximumSessionsExceededHandler} to use
|
||||
*/
|
||||
public void setMaximumSessionsExceededHandler(ServerMaximumSessionsExceededHandler maximumSessionsExceededHandler) {
|
||||
Assert.notNull(maximumSessionsExceededHandler, "maximumSessionsExceededHandler cannot be null");
|
||||
this.maximumSessionsExceededHandler = maximumSessionsExceededHandler;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -42,6 +42,16 @@ 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)
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
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.core.log.LogMessage;
|
||||
import org.springframework.security.core.session.ReactiveSessionInformation;
|
||||
|
||||
/**
|
||||
* Implementation of {@link ServerMaximumSessionsExceededHandler} that invalidates the
|
||||
* least recently used session(s). 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 Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
@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)
|
||||
.doOnComplete(() -> this.logger
|
||||
.debug(LogMessage.format("Invalidated %d least recently used sessions for authentication %s",
|
||||
leastRecentlyUsedSessionsToInvalidate.size(), context.getAuthentication().getName())))
|
||||
.flatMap(ReactiveSessionInformation::invalidate)
|
||||
.then();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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.List;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.session.ReactiveSessionInformation;
|
||||
|
||||
public final class MaximumSessionsContext {
|
||||
|
||||
private final Authentication authentication;
|
||||
|
||||
private final List<ReactiveSessionInformation> sessions;
|
||||
|
||||
private final int maximumSessionsAllowed;
|
||||
|
||||
public MaximumSessionsContext(Authentication authentication, List<ReactiveSessionInformation> sessions,
|
||||
int maximumSessionsAllowed) {
|
||||
this.authentication = authentication;
|
||||
this.sessions = sessions;
|
||||
this.maximumSessionsAllowed = maximumSessionsAllowed;
|
||||
}
|
||||
|
||||
public Authentication getAuthentication() {
|
||||
return this.authentication;
|
||||
}
|
||||
|
||||
public List<ReactiveSessionInformation> getSessions() {
|
||||
return this.sessions;
|
||||
}
|
||||
|
||||
public int getMaximumSessionsAllowed() {
|
||||
return this.maximumSessionsAllowed;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.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 Mono
|
||||
.error(new SessionAuthenticationException("Maximum sessions of " + context.getMaximumSessionsAllowed()
|
||||
+ " for authentication '" + context.getAuthentication().getName() + "' exceeded"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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.session;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.security.core.session.InMemoryReactiveSessionRegistry;
|
||||
import org.springframework.security.core.session.ReactiveSessionInformation;
|
||||
import org.springframework.security.core.session.ReactiveSessionRegistry;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.server.WebSession;
|
||||
import org.springframework.web.server.session.WebSessionStore;
|
||||
|
||||
/**
|
||||
* A {@link ReactiveSessionRegistry} implementation that uses a {@link WebSessionStore} to
|
||||
* invalidate a {@link WebSession} when the {@link ReactiveSessionInformation} is
|
||||
* invalidated.
|
||||
*
|
||||
* @author Marcus da Coregio
|
||||
* @since 6.3
|
||||
*/
|
||||
public final class WebSessionStoreReactiveSessionRegistry implements ReactiveSessionRegistry {
|
||||
|
||||
private final WebSessionStore webSessionStore;
|
||||
|
||||
private ReactiveSessionRegistry sessionRegistry = new InMemoryReactiveSessionRegistry();
|
||||
|
||||
public WebSessionStoreReactiveSessionRegistry(WebSessionStore webSessionStore) {
|
||||
Assert.notNull(webSessionStore, "webSessionStore cannot be null");
|
||||
this.webSessionStore = webSessionStore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<ReactiveSessionInformation> getAllSessions(Object principal, boolean includeExpiredSessions) {
|
||||
return this.sessionRegistry.getAllSessions(principal, includeExpiredSessions).map(WebSessionInformation::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> saveSessionInformation(ReactiveSessionInformation information) {
|
||||
return this.sessionRegistry.saveSessionInformation(new WebSessionInformation(information));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ReactiveSessionInformation> getSessionInformation(String sessionId) {
|
||||
return this.sessionRegistry.getSessionInformation(sessionId).map(WebSessionInformation::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ReactiveSessionInformation> removeSessionInformation(String sessionId) {
|
||||
return this.sessionRegistry.removeSessionInformation(sessionId).map(WebSessionInformation::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ReactiveSessionInformation> updateLastAccessTime(String sessionId) {
|
||||
return this.sessionRegistry.updateLastAccessTime(sessionId).map(WebSessionInformation::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link ReactiveSessionRegistry} to use.
|
||||
* @param sessionRegistry the {@link ReactiveSessionRegistry} to use. Cannot be null.
|
||||
*/
|
||||
public void setSessionRegistry(ReactiveSessionRegistry sessionRegistry) {
|
||||
Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
|
||||
this.sessionRegistry = sessionRegistry;
|
||||
}
|
||||
|
||||
final class WebSessionInformation extends ReactiveSessionInformation {
|
||||
|
||||
WebSessionInformation(ReactiveSessionInformation sessionInformation) {
|
||||
super(sessionInformation.getPrincipal(), sessionInformation.getSessionId(),
|
||||
sessionInformation.getLastAccessTime());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> invalidate() {
|
||||
return WebSessionStoreReactiveSessionRegistry.this.webSessionStore.retrieveSession(getSessionId())
|
||||
.flatMap(WebSession::invalidate)
|
||||
.then(Mono
|
||||
.defer(() -> WebSessionStoreReactiveSessionRegistry.this.removeSessionInformation(getSessionId())))
|
||||
.then(Mono.defer(super::invalidate));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,6 +17,8 @@
|
||||
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;
|
||||
@@ -74,9 +76,15 @@ public class DelegatingServerAuthenticationSuccessHandlerTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenEmptyThenIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(
|
||||
() -> new DelegatingServerAuthenticationSuccessHandler(new ServerAuthenticationSuccessHandler[0]));
|
||||
public void constructorWhenNullListThenIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new DelegatingServerAuthenticationSuccessHandler(
|
||||
(List<ServerAuthenticationSuccessHandler>) null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenEmptyListThenIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new DelegatingServerAuthenticationSuccessHandler(Collections.emptyList()));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* 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 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.strategy.setMaximumSessionsExceededHandler(this.handler);
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructorWhenNullRegistryThenException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new ConcurrentSessionControlServerAuthenticationSuccessHandler(null))
|
||||
.withMessage("sessionRegistry cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void setMaximumSessionsForAuthenticationWhenNullThenException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.strategy.setSessionLimit(null))
|
||||
.withMessage("sessionLimit cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void setMaximumSessionsExceededHandlerWhenNullThenException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.strategy.setMaximumSessionsExceededHandler(null))
|
||||
.withMessage("maximumSessionsExceededHandler cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void onAuthenticationWhenSessionLimitIsUnlimitedThenDoNothing() {
|
||||
ServerMaximumSessionsExceededHandler handler = mock(ServerMaximumSessionsExceededHandler.class);
|
||||
this.strategy.setSessionLimit(SessionLimit.UNLIMITED);
|
||||
this.strategy.setMaximumSessionsExceededHandler(handler);
|
||||
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(), false))
|
||||
.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(), false))
|
||||
.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(), false))
|
||||
.willReturn(Flux.fromIterable(userSessions));
|
||||
given(this.sessionRegistry.getAllSessions(admin.getPrincipal(), false))
|
||||
.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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* 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 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(), false)
|
||||
.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(), false)
|
||||
.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(), false)
|
||||
.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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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 java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
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 static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
|
||||
/**
|
||||
* Tests for {@link InvalidateLeastUsedServerMaximumSessionsExceededHandler}
|
||||
*
|
||||
* @author Marcus da Coregio
|
||||
*/
|
||||
class InvalidateLeastUsedServerMaximumSessionsExceededHandlerTests {
|
||||
|
||||
InvalidateLeastUsedServerMaximumSessionsExceededHandler handler = new InvalidateLeastUsedServerMaximumSessionsExceededHandler();
|
||||
|
||||
@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.invalidate()).willReturn(Mono.empty());
|
||||
MaximumSessionsContext context = new MaximumSessionsContext(mock(Authentication.class),
|
||||
List.of(session1, session2), 2);
|
||||
|
||||
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
|
||||
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());
|
||||
MaximumSessionsContext context = new MaximumSessionsContext(mock(Authentication.class),
|
||||
List.of(session1, session2, session3), 2);
|
||||
|
||||
this.handler.handle(context).block();
|
||||
|
||||
// @formatter:off
|
||||
verify(session1).invalidate();
|
||||
verify(session2).invalidate();
|
||||
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
|
||||
verifyNoMoreInteractions(session1);
|
||||
verifyNoMoreInteractions(session2);
|
||||
verifyNoMoreInteractions(session3);
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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 java.util.Collections;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
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 static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
* Tests for {@link PreventLoginServerMaximumSessionsExceededHandler}.
|
||||
*
|
||||
* @author Marcus da Coregio
|
||||
*/
|
||||
class PreventLoginServerMaximumSessionsExceededHandlerTests {
|
||||
|
||||
@Test
|
||||
void handleWhenInvokedThenThrowsSessionAuthenticationException() {
|
||||
PreventLoginServerMaximumSessionsExceededHandler handler = new PreventLoginServerMaximumSessionsExceededHandler();
|
||||
MaximumSessionsContext context = new MaximumSessionsContext(TestAuthentication.authenticatedUser(),
|
||||
Collections.emptyList(), 1);
|
||||
assertThatExceptionOfType(SessionAuthenticationException.class)
|
||||
.isThrownBy(() -> handler.handle(context).block())
|
||||
.withMessage("Maximum sessions of 1 for authentication 'user' exceeded");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* 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.session;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.security.core.session.ReactiveSessionInformation;
|
||||
import org.springframework.security.core.session.ReactiveSessionRegistry;
|
||||
import org.springframework.web.server.WebSession;
|
||||
import org.springframework.web.server.session.WebSessionStore;
|
||||
|
||||
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.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for {@link WebSessionStoreReactiveSessionRegistry}
|
||||
*
|
||||
* @author Marcus da Coregio
|
||||
*/
|
||||
class WebSessionStoreReactiveSessionRegistryTests {
|
||||
|
||||
WebSessionStore webSessionStore = mock();
|
||||
|
||||
WebSessionStoreReactiveSessionRegistry registry = new WebSessionStoreReactiveSessionRegistry(this.webSessionStore);
|
||||
|
||||
@Test
|
||||
void constructorWhenWebSessionStoreNullThenException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new WebSessionStoreReactiveSessionRegistry(null))
|
||||
.withMessage("webSessionStore cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getSessionInformationWhenSavedThenReturnsWebSessionInformation() {
|
||||
ReactiveSessionInformation session = createSession();
|
||||
this.registry.saveSessionInformation(session).block();
|
||||
ReactiveSessionInformation saved = this.registry.getSessionInformation(session.getSessionId()).block();
|
||||
assertThat(saved).isInstanceOf(WebSessionStoreReactiveSessionRegistry.WebSessionInformation.class);
|
||||
assertThat(saved.getPrincipal()).isEqualTo(session.getPrincipal());
|
||||
assertThat(saved.getSessionId()).isEqualTo(session.getSessionId());
|
||||
assertThat(saved.getLastAccessTime()).isEqualTo(session.getLastAccessTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidateWhenReturnedFromGetSessionInformationThenWebSessionInvalidatedAndRemovedFromRegistry() {
|
||||
ReactiveSessionInformation session = createSession();
|
||||
WebSession webSession = mock();
|
||||
given(webSession.invalidate()).willReturn(Mono.empty());
|
||||
given(this.webSessionStore.retrieveSession(session.getSessionId())).willReturn(Mono.just(webSession));
|
||||
|
||||
this.registry.saveSessionInformation(session).block();
|
||||
ReactiveSessionInformation saved = this.registry.getSessionInformation(session.getSessionId()).block();
|
||||
saved.invalidate().block();
|
||||
verify(webSession).invalidate();
|
||||
assertThat(this.registry.getSessionInformation(saved.getSessionId()).block()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidateWhenReturnedFromRemoveSessionInformationThenWebSessionInvalidatedAndRemovedFromRegistry() {
|
||||
ReactiveSessionInformation session = createSession();
|
||||
WebSession webSession = mock();
|
||||
given(webSession.invalidate()).willReturn(Mono.empty());
|
||||
given(this.webSessionStore.retrieveSession(session.getSessionId())).willReturn(Mono.just(webSession));
|
||||
|
||||
this.registry.saveSessionInformation(session).block();
|
||||
ReactiveSessionInformation saved = this.registry.removeSessionInformation(session.getSessionId()).block();
|
||||
saved.invalidate().block();
|
||||
verify(webSession).invalidate();
|
||||
assertThat(this.registry.getSessionInformation(saved.getSessionId()).block()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidateWhenReturnedFromGetAllSessionsThenWebSessionInvalidatedAndRemovedFromRegistry() {
|
||||
ReactiveSessionInformation session = createSession();
|
||||
WebSession webSession = mock();
|
||||
given(webSession.invalidate()).willReturn(Mono.empty());
|
||||
given(this.webSessionStore.retrieveSession(session.getSessionId())).willReturn(Mono.just(webSession));
|
||||
|
||||
this.registry.saveSessionInformation(session).block();
|
||||
List<ReactiveSessionInformation> saved = this.registry.getAllSessions(session.getPrincipal(), false)
|
||||
.collectList()
|
||||
.block();
|
||||
saved.forEach((info) -> info.invalidate().block());
|
||||
verify(webSession).invalidate();
|
||||
assertThat(this.registry.getAllSessions(session.getPrincipal(), false).collectList().block()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void setSessionRegistryThenUses() {
|
||||
ReactiveSessionRegistry sessionRegistry = mock();
|
||||
given(sessionRegistry.saveSessionInformation(any())).willReturn(Mono.empty());
|
||||
given(sessionRegistry.removeSessionInformation(any())).willReturn(Mono.empty());
|
||||
given(sessionRegistry.updateLastAccessTime(any())).willReturn(Mono.empty());
|
||||
given(sessionRegistry.getSessionInformation(any())).willReturn(Mono.empty());
|
||||
given(sessionRegistry.getAllSessions(any(), anyBoolean())).willReturn(Flux.empty());
|
||||
this.registry.setSessionRegistry(sessionRegistry);
|
||||
ReactiveSessionInformation session = createSession();
|
||||
this.registry.saveSessionInformation(session).block();
|
||||
verify(sessionRegistry).saveSessionInformation(any());
|
||||
this.registry.removeSessionInformation(session.getSessionId()).block();
|
||||
verify(sessionRegistry).removeSessionInformation(any());
|
||||
this.registry.updateLastAccessTime(session.getSessionId()).block();
|
||||
verify(sessionRegistry).updateLastAccessTime(any());
|
||||
this.registry.getSessionInformation(session.getSessionId()).block();
|
||||
verify(sessionRegistry).getSessionInformation(any());
|
||||
this.registry.getAllSessions(session.getPrincipal(), false).blockFirst();
|
||||
verify(sessionRegistry).getAllSessions(any(), eq(false));
|
||||
}
|
||||
|
||||
private static ReactiveSessionInformation createSession() {
|
||||
return new ReactiveSessionInformation("principal", "sessionId", Instant.now());
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user