Skip to content

Commit a5ce8ae

Browse files
Polish Max Sessions on WebFlux
This commit changes the PreventLoginServerMaximumSessionsExceededHandler to invalidate the WebSession in addition to throwing the error, this is needed otherwise the session would still be saved with the security context. It also changes the SessionRegistryWebSession to first perform the operation on the delegate and then invoke the needed method on the ReactiveSessionRegistry Issue gh-6192
1 parent c639d0a commit a5ce8ae

File tree

7 files changed

+56
-28
lines changed

7 files changed

+56
-28
lines changed

config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java

+12-8
Original file line numberDiff line numberDiff line change
@@ -2143,19 +2143,23 @@ public boolean isStarted() {
21432143
@Override
21442144
public Mono<Void> changeSessionId() {
21452145
String currentId = this.session.getId();
2146-
return SessionRegistryWebFilter.this.sessionRegistry.removeSessionInformation(currentId)
2147-
.flatMap((information) -> this.session.changeSessionId().thenReturn(information))
2148-
.flatMap((information) -> {
2149-
information = information.withSessionId(this.session.getId());
2150-
return SessionRegistryWebFilter.this.sessionRegistry.saveSessionInformation(information);
2151-
});
2146+
return this.session.changeSessionId()
2147+
.then(Mono.defer(
2148+
() -> SessionRegistryWebFilter.this.sessionRegistry.removeSessionInformation(currentId)
2149+
.flatMap((information) -> {
2150+
information = information.withSessionId(this.session.getId());
2151+
return SessionRegistryWebFilter.this.sessionRegistry
2152+
.saveSessionInformation(information);
2153+
})));
21522154
}
21532155

21542156
@Override
21552157
public Mono<Void> invalidate() {
21562158
String currentId = this.session.getId();
2157-
return SessionRegistryWebFilter.this.sessionRegistry.removeSessionInformation(currentId)
2158-
.flatMap((information) -> this.session.invalidate());
2159+
return this.session.invalidate()
2160+
.then(Mono.defer(() -> SessionRegistryWebFilter.this.sessionRegistry
2161+
.removeSessionInformation(currentId)))
2162+
.then();
21592163
}
21602164

21612165
@Override

config/src/test/java/org/springframework/security/config/web/server/SessionManagementSpecTests.java

+8-2
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
6868
import org.springframework.web.server.session.DefaultWebSessionManager;
6969

70+
import static org.assertj.core.api.Assertions.assertThat;
7071
import static org.mockito.ArgumentMatchers.any;
7172
import static org.mockito.BDDMockito.given;
7273
import static org.mockito.Mockito.mock;
@@ -95,14 +96,19 @@ void loginWhenMaxSessionPreventsLoginThenSecondLoginFails() {
9596
ResponseCookie firstLoginSessionCookie = loginReturningCookie(data);
9697

9798
// second login should fail
98-
this.client.mutateWith(csrf())
99+
ResponseCookie secondLoginSessionCookie = this.client.mutateWith(csrf())
99100
.post()
100101
.uri("/login")
101102
.contentType(MediaType.MULTIPART_FORM_DATA)
102103
.body(BodyInserters.fromFormData(data))
103104
.exchange()
104105
.expectHeader()
105-
.location("/login?error");
106+
.location("/login?error")
107+
.returnResult(Void.class)
108+
.getResponseCookies()
109+
.getFirst("SESSION");
110+
111+
assertThat(secondLoginSessionCookie).isNull();
106112

107113
// first login should still be valid
108114
this.client.mutateWith(csrf())

web/src/main/java/org/springframework/security/web/server/authentication/ConcurrentSessionControlServerAuthenticationSuccessHandler.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,8 @@ private Mono<Void> handleConcurrency(WebFilterExchange exchange, Authentication
8181
}
8282
}
8383
}
84-
return this.maximumSessionsExceededHandler
85-
.handle(new MaximumSessionsContext(authentication, registeredSessions, maximumSessions));
84+
return this.maximumSessionsExceededHandler.handle(new MaximumSessionsContext(authentication,
85+
registeredSessions, maximumSessions, currentSession));
8686
});
8787
}
8888

web/src/main/java/org/springframework/security/web/server/authentication/MaximumSessionsContext.java

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@
2020

2121
import org.springframework.security.core.Authentication;
2222
import org.springframework.security.core.session.ReactiveSessionInformation;
23+
import org.springframework.web.server.WebSession;
2324

2425
public final class MaximumSessionsContext {
2526

@@ -29,11 +30,14 @@ public final class MaximumSessionsContext {
2930

3031
private final int maximumSessionsAllowed;
3132

33+
private final WebSession currentSession;
34+
3235
public MaximumSessionsContext(Authentication authentication, List<ReactiveSessionInformation> sessions,
33-
int maximumSessionsAllowed) {
36+
int maximumSessionsAllowed, WebSession currentSession) {
3437
this.authentication = authentication;
3538
this.sessions = sessions;
3639
this.maximumSessionsAllowed = maximumSessionsAllowed;
40+
this.currentSession = currentSession;
3741
}
3842

3943
public Authentication getAuthentication() {
@@ -48,4 +52,8 @@ public int getMaximumSessionsAllowed() {
4852
return this.maximumSessionsAllowed;
4953
}
5054

55+
public WebSession getCurrentSession() {
56+
return this.currentSession;
57+
}
58+
5159
}

web/src/main/java/org/springframework/security/web/server/authentication/PreventLoginServerMaximumSessionsExceededHandler.java

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -31,9 +31,9 @@ public final class PreventLoginServerMaximumSessionsExceededHandler implements S
3131

3232
@Override
3333
public Mono<Void> handle(MaximumSessionsContext context) {
34-
return Mono
35-
.error(new SessionAuthenticationException("Maximum sessions of " + context.getMaximumSessionsAllowed()
36-
+ " for authentication '" + context.getAuthentication().getName() + "' exceeded"));
34+
return context.getCurrentSession()
35+
.invalidate()
36+
.then(Mono.defer(() -> Mono.error(new SessionAuthenticationException("Maximum sessions exceeded"))));
3737
}
3838

3939
}

web/src/test/java/org/springframework/security/web/server/authentication/session/InvalidateLeastUsedServerMaximumSessionsExceededHandlerTests.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -50,7 +50,7 @@ void handleWhenInvokedThenInvalidatesLeastRecentlyUsedSessions() {
5050
given(session2.getLastAccessTime()).willReturn(Instant.ofEpochMilli(1700827760000L));
5151
given(session2.invalidate()).willReturn(Mono.empty());
5252
MaximumSessionsContext context = new MaximumSessionsContext(mock(Authentication.class),
53-
List.of(session1, session2), 2);
53+
List.of(session1, session2), 2, null);
5454

5555
this.handler.handle(context).block();
5656

@@ -72,7 +72,7 @@ void handleWhenMoreThanOneSessionToInvalidateThenInvalidatesAllOfThem() {
7272
given(session1.invalidate()).willReturn(Mono.empty());
7373
given(session2.invalidate()).willReturn(Mono.empty());
7474
MaximumSessionsContext context = new MaximumSessionsContext(mock(Authentication.class),
75-
List.of(session1, session2, session3), 2);
75+
List.of(session1, session2, session3), 2, null);
7676

7777
this.handler.handle(context).block();
7878

web/src/test/java/org/springframework/security/web/server/authentication/session/PreventLoginServerMaximumSessionsExceededHandlerTests.java

+17-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,13 +19,19 @@
1919
import java.util.Collections;
2020

2121
import org.junit.jupiter.api.Test;
22+
import reactor.core.publisher.Mono;
23+
import reactor.test.StepVerifier;
2224

2325
import org.springframework.security.authentication.TestAuthentication;
2426
import org.springframework.security.web.authentication.session.SessionAuthenticationException;
2527
import org.springframework.security.web.server.authentication.MaximumSessionsContext;
2628
import org.springframework.security.web.server.authentication.PreventLoginServerMaximumSessionsExceededHandler;
29+
import org.springframework.web.server.WebSession;
2730

28-
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
31+
import static org.assertj.core.api.Assertions.assertThat;
32+
import static org.mockito.BDDMockito.given;
33+
import static org.mockito.Mockito.mock;
34+
import static org.mockito.Mockito.verify;
2935

3036
/**
3137
* Tests for {@link PreventLoginServerMaximumSessionsExceededHandler}.
@@ -35,13 +41,17 @@
3541
class PreventLoginServerMaximumSessionsExceededHandlerTests {
3642

3743
@Test
38-
void handleWhenInvokedThenThrowsSessionAuthenticationException() {
44+
void handleWhenInvokedThenInvalidateWebSessionAndThrowsSessionAuthenticationException() {
3945
PreventLoginServerMaximumSessionsExceededHandler handler = new PreventLoginServerMaximumSessionsExceededHandler();
46+
WebSession webSession = mock();
47+
given(webSession.invalidate()).willReturn(Mono.empty());
4048
MaximumSessionsContext context = new MaximumSessionsContext(TestAuthentication.authenticatedUser(),
41-
Collections.emptyList(), 1);
42-
assertThatExceptionOfType(SessionAuthenticationException.class)
43-
.isThrownBy(() -> handler.handle(context).block())
44-
.withMessage("Maximum sessions of 1 for authentication 'user' exceeded");
49+
Collections.emptyList(), 1, webSession);
50+
StepVerifier.create(handler.handle(context)).expectErrorSatisfies((ex) -> {
51+
assertThat(ex).isInstanceOf(SessionAuthenticationException.class);
52+
assertThat(ex.getMessage()).isEqualTo("Maximum sessions exceeded");
53+
}).verify();
54+
verify(webSession).invalidate();
4555
}
4656

4757
}

0 commit comments

Comments
 (0)