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