diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 0b0d91af..2f600907 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -58,7 +58,7 @@ org-slf4j-jcl-over-slf4j = { module = "org.slf4j:jcl-over-slf4j", version.ref =
org-slf4j-log4j-over-slf4j = { module = "org.slf4j:log4j-over-slf4j", version.ref = "org-slf4j" }
org-slf4j-slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "org-slf4j" }
org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2024.0.0-M1"
-org-springframework-security-spring-security-bom = "org.springframework.security:spring-security-bom:6.3.0-M2"
+org-springframework-security-spring-security-bom = "org.springframework.security:spring-security-bom:6.3.0-SNAPSHOT"
org-springframework-spring-framework-bom = "org.springframework:spring-framework-bom:6.1.4"
org-springframework-boot-spring-boot-dependencies = { module = "org.springframework.boot:spring-boot-dependencies", version.ref = "org-springframework-boot" }
org-springframework-boot-spring-boot-gradle-plugin = { module = "org.springframework.boot:spring-boot-gradle-plugin", version.ref = "org-springframework-boot" }
diff --git a/spring-session-core/src/main/java/org/springframework/session/security/SpringSessionBackedReactiveSessionRegistry.java b/spring-session-core/src/main/java/org/springframework/session/security/SpringSessionBackedReactiveSessionRegistry.java
new file mode 100644
index 00000000..71dac096
--- /dev/null
+++ b/spring-session-core/src/main/java/org/springframework/session/security/SpringSessionBackedReactiveSessionRegistry.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2014-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.session.security;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.session.ReactiveSessionInformation;
+import org.springframework.security.core.session.ReactiveSessionRegistry;
+import org.springframework.session.ReactiveFindByIndexNameSessionRepository;
+import org.springframework.session.ReactiveSessionRepository;
+import org.springframework.session.Session;
+import org.springframework.util.Assert;
+
+/**
+ * A {@link ReactiveSessionRegistry} that retrieves session information from Spring
+ * Session, rather than maintaining it itself. This allows concurrent session management
+ * with Spring Security in a clustered environment.
+ *
+ * Relies on being able to derive the same String-based representation of the principal
+ * given to {@link #getAllSessions(Object)} as used by Spring Session in order to look up
+ * the user's sessions.
+ *
+ *
+ * @param the {@link Session} type.
+ * @author Marcus da Coregio
+ * @since 3.3
+ */
+public final class SpringSessionBackedReactiveSessionRegistry implements ReactiveSessionRegistry {
+
+ private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
+
+ private final ReactiveSessionRepository sessionRepository;
+
+ private final ReactiveFindByIndexNameSessionRepository indexedSessionRepository;
+
+ public SpringSessionBackedReactiveSessionRegistry(ReactiveSessionRepository sessionRepository,
+ ReactiveFindByIndexNameSessionRepository indexedSessionRepository) {
+ Assert.notNull(sessionRepository, "sessionRepository cannot be null");
+ Assert.notNull(indexedSessionRepository, "indexedSessionRepository cannot be null");
+ this.sessionRepository = sessionRepository;
+ this.indexedSessionRepository = indexedSessionRepository;
+ }
+
+ @Override
+ public Flux getAllSessions(Object principal) {
+ Authentication authenticationToken = getAuthenticationToken(principal);
+ return this.indexedSessionRepository.findByPrincipalName(authenticationToken.getName())
+ .flatMapMany((sessionMap) -> Flux.fromIterable(sessionMap.entrySet()))
+ .map((entry) -> new SpringSessionBackedReactiveSessionInformation(entry.getValue()));
+ }
+
+ @Override
+ public Mono saveSessionInformation(ReactiveSessionInformation information) {
+ return Mono.empty();
+ }
+
+ @Override
+ public Mono getSessionInformation(String sessionId) {
+ return this.sessionRepository.findById(sessionId).map(SpringSessionBackedReactiveSessionInformation::new);
+ }
+
+ @Override
+ public Mono removeSessionInformation(String sessionId) {
+ return Mono.empty();
+ }
+
+ @Override
+ public Mono updateLastAccessTime(String sessionId) {
+ return Mono.empty();
+ }
+
+ private static Authentication getAuthenticationToken(Object principal) {
+ return new AbstractAuthenticationToken(AuthorityUtils.NO_AUTHORITIES) {
+
+ @Override
+ public Object getCredentials() {
+ return null;
+ }
+
+ @Override
+ public Object getPrincipal() {
+ return principal;
+ }
+
+ };
+ }
+
+ class SpringSessionBackedReactiveSessionInformation extends ReactiveSessionInformation {
+
+ SpringSessionBackedReactiveSessionInformation(S session) {
+ super(resolvePrincipalName(session), session.getId(), session.getLastAccessedTime());
+ }
+
+ private static String resolvePrincipalName(Session session) {
+ String principalName = session
+ .getAttribute(ReactiveFindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
+ if (principalName != null) {
+ return principalName;
+ }
+ SecurityContext securityContext = session.getAttribute(SPRING_SECURITY_CONTEXT);
+ if (securityContext != null && securityContext.getAuthentication() != null) {
+ return securityContext.getAuthentication().getName();
+ }
+ return "";
+ }
+
+ @Override
+ public Mono invalidate() {
+ return super.invalidate()
+ .then(Mono.defer(() -> SpringSessionBackedReactiveSessionRegistry.this.sessionRepository
+ .deleteById(getSessionId())));
+ }
+
+ }
+
+}
diff --git a/spring-session-core/src/test/java/org/springframework/session/security/SpringSessionBackedReactiveSessionRegistryTests.java b/spring-session-core/src/test/java/org/springframework/session/security/SpringSessionBackedReactiveSessionRegistryTests.java
new file mode 100644
index 00000000..ce4df35b
--- /dev/null
+++ b/spring-session-core/src/test/java/org/springframework/session/security/SpringSessionBackedReactiveSessionRegistryTests.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2014-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.session.security;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+
+import org.springframework.security.authentication.TestingAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextImpl;
+import org.springframework.security.core.session.ReactiveSessionInformation;
+import org.springframework.session.MapSession;
+import org.springframework.session.ReactiveFindByIndexNameSessionRepository;
+import org.springframework.session.ReactiveMapSessionRepository;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class SpringSessionBackedReactiveSessionRegistryTests {
+
+ static MapSession johnSession1 = new MapSession();
+ static MapSession johnSession2 = new MapSession();
+ static MapSession johnSession3 = new MapSession();
+
+ SpringSessionBackedReactiveSessionRegistry sessionRegistry;
+
+ ReactiveFindByIndexNameSessionRepository indexedSessionRepository = new StubIndexedSessionRepository();
+
+ ReactiveMapSessionRepository sessionRepository = new ReactiveMapSessionRepository(new ConcurrentHashMap<>());
+
+ static {
+ johnSession1.setAttribute(ReactiveFindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, "johndoe");
+ johnSession2.setAttribute(ReactiveFindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, "johndoe");
+ johnSession3.setAttribute(ReactiveFindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, "johndoe");
+ }
+
+ @BeforeEach
+ void setup() {
+ this.sessionRegistry = new SpringSessionBackedReactiveSessionRegistry<>(this.sessionRepository,
+ this.indexedSessionRepository);
+ this.sessionRepository.save(johnSession1).block();
+ this.sessionRepository.save(johnSession2).block();
+ this.sessionRepository.save(johnSession3).block();
+ }
+
+ @Test
+ void saveSessionInformationThenDoNothing() {
+ StepVerifier.create(this.sessionRegistry.saveSessionInformation(null)).expectComplete().verify();
+ }
+
+ @Test
+ void removeSessionInformationThenDoNothing() {
+ StepVerifier.create(this.sessionRegistry.removeSessionInformation(null)).expectComplete().verify();
+ }
+
+ @Test
+ void updateLastAccessTimeThenDoNothing() {
+ StepVerifier.create(this.sessionRegistry.updateLastAccessTime(null)).expectComplete().verify();
+ }
+
+ @Test
+ void getSessionInformationWhenPrincipalIndexNamePresentThenPrincipalResolved() {
+ MapSession session = this.sessionRepository.createSession().block();
+ session.setAttribute(ReactiveFindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, "johndoe");
+ this.sessionRepository.save(session).block();
+ StepVerifier.create(this.sessionRegistry.getSessionInformation(session.getId()))
+ .assertNext((sessionInformation) -> {
+ assertThat(sessionInformation.getSessionId()).isEqualTo(session.getId());
+ assertThat(sessionInformation.getLastAccessTime()).isEqualTo(session.getLastAccessedTime());
+ assertThat(sessionInformation.getPrincipal()).isEqualTo("johndoe");
+ })
+ .verifyComplete();
+ }
+
+ @Test
+ void getSessionInformationWhenSecurityContextAttributePresentThenPrincipalResolved() {
+ MapSession session = this.sessionRepository.createSession().block();
+ TestingAuthenticationToken authentication = new TestingAuthenticationToken("johndoe", "n/a");
+ SecurityContextImpl securityContext = new SecurityContextImpl();
+ securityContext.setAuthentication(authentication);
+ session.setAttribute("SPRING_SECURITY_CONTEXT", securityContext);
+ this.sessionRepository.save(session).block();
+ StepVerifier.create(this.sessionRegistry.getSessionInformation(session.getId()))
+ .assertNext((sessionInformation) -> {
+ assertThat(sessionInformation.getSessionId()).isEqualTo(session.getId());
+ assertThat(sessionInformation.getLastAccessTime()).isEqualTo(session.getLastAccessedTime());
+ assertThat(sessionInformation.getPrincipal()).isEqualTo("johndoe");
+ })
+ .verifyComplete();
+ }
+
+ @Test
+ void getSessionInformationWhenNoResolvablePrincipalThenPrincipalBlank() {
+ MapSession session = this.sessionRepository.createSession().block();
+ this.sessionRepository.save(session).block();
+ StepVerifier.create(this.sessionRegistry.getSessionInformation(session.getId()))
+ .assertNext((sessionInformation) -> {
+ assertThat(sessionInformation.getSessionId()).isEqualTo(session.getId());
+ assertThat(sessionInformation.getLastAccessTime()).isEqualTo(session.getLastAccessedTime());
+ assertThat(sessionInformation.getPrincipal()).isEqualTo("");
+ })
+ .verifyComplete();
+ }
+
+ @Test
+ void getSessionInformationWhenInvalidateThenRemovedFromSessionRepository() {
+ MapSession session = this.sessionRepository.createSession().block();
+ this.sessionRepository.save(session).block();
+ Mono publisher = this.sessionRegistry.getSessionInformation(session.getId())
+ .flatMap(ReactiveSessionInformation::invalidate);
+ StepVerifier.create(publisher).verifyComplete();
+ StepVerifier.create(this.sessionRepository.findById(session.getId())).expectComplete().verify();
+ }
+
+ @Test
+ void getAllSessionsWhenSessionsExistsThenReturned() {
+ Flux sessions = this.sessionRegistry.getAllSessions("johndoe");
+ StepVerifier.create(sessions)
+ .assertNext((sessionInformation) -> assertThat(sessionInformation.getPrincipal()).isEqualTo("johndoe"))
+ .assertNext((sessionInformation) -> assertThat(sessionInformation.getPrincipal()).isEqualTo("johndoe"))
+ .assertNext((sessionInformation) -> assertThat(sessionInformation.getPrincipal()).isEqualTo("johndoe"))
+ .verifyComplete();
+ }
+
+ @Test
+ void getAllSessionsWhenInvalidateThenSessionsRemovedFromRepository() {
+ this.sessionRegistry.getAllSessions("johndoe").flatMap(ReactiveSessionInformation::invalidate).blockLast();
+ StepVerifier.create(this.sessionRepository.findById(johnSession1.getId())).expectComplete().verify();
+ StepVerifier.create(this.sessionRepository.findById(johnSession2.getId())).expectComplete().verify();
+ StepVerifier.create(this.sessionRepository.findById(johnSession3.getId())).expectComplete().verify();
+ }
+
+ static class StubIndexedSessionRepository implements ReactiveFindByIndexNameSessionRepository {
+
+ Map johnSessions = Map.of(johnSession1.getId(), johnSession1, johnSession2.getId(),
+ johnSession2, johnSession3.getId(), johnSession3);
+
+ @Override
+ public Mono