From 1e7f25c1aba60fd01d95775c8b47d4f152815c3f Mon Sep 17 00:00:00 2001 From: Andrey Litvitski Date: Sun, 13 Jul 2025 21:10:59 +0300 Subject: [PATCH] Add `createdTime` field to `SessionInformation` Closes: gh-17359 Signed-off-by: Andrey Litvitski --- .../security/SerializationSamples.java | 2 +- .../NamespaceSessionManagementTests.java | 3 ++- .../web/session/SessionConcurrencyDslTests.kt | 4 ++-- .../security/core/session/SessionInformation.java | 11 ++++++++++- .../security/core/session/SessionRegistryImpl.java | 4 +++- .../core/session/SessionInformationTests.java | 5 ++++- .../oidc/session/OidcSessionInformation.java | 6 +++++- ...tSessionControlAuthenticationStrategyTests.java | 14 +++++++------- .../concurrent/ConcurrentSessionFilterTests.java | 2 +- .../SessionInformationExpiredEventTests.java | 8 +++++--- 10 files changed, 40 insertions(+), 19 deletions(-) diff --git a/config/src/test/java/org/springframework/security/SerializationSamples.java b/config/src/test/java/org/springframework/security/SerializationSamples.java index fe9f75cd50f..9250ea7766a 100644 --- a/config/src/test/java/org/springframework/security/SerializationSamples.java +++ b/config/src/test/java/org/springframework/security/SerializationSamples.java @@ -256,7 +256,7 @@ final class SerializationSamples { generatorByClassName.put(OAuth2AuthorizationExchange.class, (r) -> TestOAuth2AuthorizationExchanges.success()); generatorByClassName.put(OidcUserInfo.class, (r) -> OidcUserInfo.builder().email("email@example.com").build()); generatorByClassName.put(SessionInformation.class, - (r) -> new SessionInformation(user, r.alphanumeric(4), new Date(1704378933936L))); + (r) -> new SessionInformation(user, r.alphanumeric(4), new Date(1704378933936L), new Date())); generatorByClassName.put(ReactiveSessionInformation.class, (r) -> new ReactiveSessionInformation(user, r.alphanumeric(4), Instant.ofEpochMilli(1704378933936L))); generatorByClassName.put(OAuth2AccessToken.class, (r) -> TestOAuth2AccessTokens.scopes("scope")); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceSessionManagementTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceSessionManagementTests.java index 1efd63aab14..0b7dda3124f 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceSessionManagementTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceSessionManagementTests.java @@ -111,7 +111,8 @@ public void authenticateWhenUsingInvalidSessionUrlThenMatchesNamespace() throws public void authenticateWhenUsingExpiredUrlThenMatchesNamespace() throws Exception { this.spring.register(CustomSessionManagementConfig.class).autowire(); MockHttpSession session = new MockHttpSession(); - SessionInformation sessionInformation = new SessionInformation(new Object(), session.getId(), new Date(0)); + SessionInformation sessionInformation = new SessionInformation(new Object(), session.getId(), new Date(0), + new Date()); sessionInformation.expireNow(); SessionRegistry sessionRegistry = this.spring.getContext().getBean(SessionRegistry.class); given(sessionRegistry.getSessionInformation(session.getId())).willReturn(sessionInformation); diff --git a/config/src/test/kotlin/org/springframework/security/config/annotation/web/session/SessionConcurrencyDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/annotation/web/session/SessionConcurrencyDslTests.kt index 9117ae757a7..1e91c214da6 100644 --- a/config/src/test/kotlin/org/springframework/security/config/annotation/web/session/SessionConcurrencyDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/annotation/web/session/SessionConcurrencyDslTests.kt @@ -101,7 +101,7 @@ class SessionConcurrencyDslTests { mockkObject(ExpiredUrlConfig.SESSION_REGISTRY) val session = MockHttpSession() - val sessionInformation = SessionInformation("", session.id, Date(0)) + val sessionInformation = SessionInformation("", session.id, Date(0), Date()) sessionInformation.expireNow() every { ExpiredUrlConfig.SESSION_REGISTRY.getSessionInformation(any()) } returns sessionInformation @@ -141,7 +141,7 @@ class SessionConcurrencyDslTests { mockkObject(ExpiredSessionStrategyConfig.SESSION_REGISTRY) val session = MockHttpSession() - val sessionInformation = SessionInformation("", session.id, Date(0)) + val sessionInformation = SessionInformation("", session.id, Date(0), Date()) sessionInformation.expireNow() every { ExpiredSessionStrategyConfig.SESSION_REGISTRY.getSessionInformation(any()) } returns sessionInformation diff --git a/core/src/main/java/org/springframework/security/core/session/SessionInformation.java b/core/src/main/java/org/springframework/security/core/session/SessionInformation.java index 54b05bbbb08..1fba14b0624 100644 --- a/core/src/main/java/org/springframework/security/core/session/SessionInformation.java +++ b/core/src/main/java/org/springframework/security/core/session/SessionInformation.java @@ -36,6 +36,7 @@ * Filter. * * @author Ben Alex + * @author Andrey Litvitski */ public class SessionInformation implements Serializable { @@ -49,13 +50,17 @@ public class SessionInformation implements Serializable { private boolean expired = false; - public SessionInformation(Object principal, String sessionId, Date lastRequest) { + private final Date createdTime; + + public SessionInformation(Object principal, String sessionId, Date lastRequest, Date createdTime) { Assert.notNull(principal, "Principal required"); Assert.hasText(sessionId, "SessionId required"); Assert.notNull(lastRequest, "LastRequest required"); + Assert.notNull(lastRequest, "CreatedTime required"); this.principal = principal; this.sessionId = sessionId; this.lastRequest = lastRequest; + this.createdTime = createdTime; } public void expireNow() { @@ -74,6 +79,10 @@ public String getSessionId() { return this.sessionId; } + public Date getCreatedTime() { + return this.createdTime; + } + public boolean isExpired() { return this.expired; } diff --git a/core/src/main/java/org/springframework/security/core/session/SessionRegistryImpl.java b/core/src/main/java/org/springframework/security/core/session/SessionRegistryImpl.java index a3909315415..36fcfefca18 100644 --- a/core/src/main/java/org/springframework/security/core/session/SessionRegistryImpl.java +++ b/core/src/main/java/org/springframework/security/core/session/SessionRegistryImpl.java @@ -133,7 +133,9 @@ public void registerNewSession(String sessionId, Object principal) { if (this.logger.isDebugEnabled()) { this.logger.debug(LogMessage.format("Registering session %s, for principal %s", sessionId, principal)); } - this.sessionIds.put(sessionId, new SessionInformation(principal, sessionId, new Date())); + Date currentDate = new Date(); + this.sessionIds.put(sessionId, new SessionInformation(principal, sessionId, new Date(currentDate.getTime()), + new Date(currentDate.getTime()))); this.principals.compute(principal, (key, sessionsUsedByPrincipal) -> { if (sessionsUsedByPrincipal == null) { sessionsUsedByPrincipal = new CopyOnWriteArraySet<>(); diff --git a/core/src/test/java/org/springframework/security/core/session/SessionInformationTests.java b/core/src/test/java/org/springframework/security/core/session/SessionInformationTests.java index e66335017f1..eb0c3e135a6 100644 --- a/core/src/test/java/org/springframework/security/core/session/SessionInformationTests.java +++ b/core/src/test/java/org/springframework/security/core/session/SessionInformationTests.java @@ -26,6 +26,7 @@ * Tests {@link SessionInformation}. * * @author Ben Alex + * @author Andrey Litvitski */ public class SessionInformationTests { @@ -34,10 +35,12 @@ public void testObject() throws Exception { Object principal = "Some principal object"; String sessionId = "1234567890"; Date currentDate = new Date(); - SessionInformation info = new SessionInformation(principal, sessionId, currentDate); + SessionInformation info = new SessionInformation(principal, sessionId, new Date(currentDate.getTime()), + new Date(currentDate.getTime())); assertThat(info.getPrincipal()).isEqualTo(principal); assertThat(info.getSessionId()).isEqualTo(sessionId); assertThat(info.getLastRequest()).isEqualTo(currentDate); + assertThat(info.getCreatedTime()).isEqualTo(currentDate); Thread.sleep(10); info.refreshLastRequest(); assertThat(info.getLastRequest().after(currentDate)).isTrue(); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/session/OidcSessionInformation.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/session/OidcSessionInformation.java index 693a06d3aa4..8ebc3d7044a 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/session/OidcSessionInformation.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/session/OidcSessionInformation.java @@ -46,7 +46,11 @@ public class OidcSessionInformation extends SessionInformation { * @param user the OIDC Provider's session and end user */ public OidcSessionInformation(String sessionId, Map authorities, OidcUser user) { - super(user, sessionId, new Date()); + this(sessionId, authorities, user, new Date()); + } + + private OidcSessionInformation(String sessionId, Map authorities, OidcUser user, Date now) { + super(user, sessionId, new Date(now.getTime()), new Date(now.getTime())); this.authorities = (authorities != null) ? new LinkedHashMap<>(authorities) : Collections.emptyMap(); } diff --git a/web/src/test/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategyTests.java b/web/src/test/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategyTests.java index aa1bed6d8f8..faf9beba109 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategyTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategyTests.java @@ -70,7 +70,7 @@ public void setup() { this.request = new MockHttpServletRequest(); this.response = new MockHttpServletResponse(); this.sessionInformation = new SessionInformation(this.authentication.getPrincipal(), "unique", - new Date(1374766134216L)); + new Date(1374766134216L), new Date()); this.strategy = new ConcurrentSessionControlAuthenticationStrategy(this.sessionRegistry); } @@ -123,7 +123,7 @@ public void maxSessionsExpireExistingUser() { @Test public void maxSessionsExpireLeastRecentExistingUser() { SessionInformation moreRecentSessionInfo = new SessionInformation(this.authentication.getPrincipal(), "unique", - new Date(1374766999999L)); + new Date(1374766999999L), new Date()); given(this.sessionRegistry.getAllSessions(any(), anyBoolean())) .willReturn(Arrays.asList(moreRecentSessionInfo, this.sessionInformation)); this.strategy.setMaximumSessions(2); @@ -134,9 +134,9 @@ public void maxSessionsExpireLeastRecentExistingUser() { @Test public void onAuthenticationWhenMaxSessionsExceededByTwoThenTwoSessionsExpired() { SessionInformation oldestSessionInfo = new SessionInformation(this.authentication.getPrincipal(), "unique1", - new Date(1374766134214L)); + new Date(1374766134214L), new Date()); SessionInformation secondOldestSessionInfo = new SessionInformation(this.authentication.getPrincipal(), - "unique2", new Date(1374766134215L)); + "unique2", new Date(1374766134215L), new Date()); given(this.sessionRegistry.getAllSessions(any(), anyBoolean())).willReturn( Arrays.asList(oldestSessionInfo, secondOldestSessionInfo, this.sessionInformation)); this.strategy.setMaximumSessions(2); @@ -196,7 +196,7 @@ public void maxSessionsExpireExistingUserUsingSessionLimit() { @Test public void maxSessionsExpireLeastRecentExistingUserUsingSessionLimit() { SessionInformation moreRecentSessionInfo = new SessionInformation(this.authentication.getPrincipal(), "unique", - new Date(1374766999999L)); + new Date(1374766999999L), new Date()); given(this.sessionRegistry.getAllSessions(any(), anyBoolean())) .willReturn(Arrays.asList(moreRecentSessionInfo, this.sessionInformation)); this.strategy.setMaximumSessions(SessionLimit.of(2)); @@ -207,9 +207,9 @@ public void maxSessionsExpireLeastRecentExistingUserUsingSessionLimit() { @Test public void onAuthenticationWhenMaxSessionsExceededByTwoThenTwoSessionsExpiredUsingSessionLimit() { SessionInformation oldestSessionInfo = new SessionInformation(this.authentication.getPrincipal(), "unique1", - new Date(1374766134214L)); + new Date(1374766134214L), new Date()); SessionInformation secondOldestSessionInfo = new SessionInformation(this.authentication.getPrincipal(), - "unique2", new Date(1374766134215L)); + "unique2", new Date(1374766134215L), new Date()); given(this.sessionRegistry.getAllSessions(any(), anyBoolean())) .willReturn(Arrays.asList(oldestSessionInfo, secondOldestSessionInfo, this.sessionInformation)); this.strategy.setMaximumSessions(SessionLimit.of(2)); diff --git a/web/src/test/java/org/springframework/security/web/concurrent/ConcurrentSessionFilterTests.java b/web/src/test/java/org/springframework/security/web/concurrent/ConcurrentSessionFilterTests.java index e0c6769fd80..3a424342e43 100644 --- a/web/src/test/java/org/springframework/security/web/concurrent/ConcurrentSessionFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/concurrent/ConcurrentSessionFilterTests.java @@ -276,7 +276,7 @@ public void setLogoutHandlersWhenEmptyThenThrowsException() { private SessionRegistry mockSessionRegistry() { SessionRegistry registry = mock(SessionRegistry.class); SessionInformation information = new SessionInformation("user", "sessionId", - new Date(System.currentTimeMillis() - 1000)); + new Date(System.currentTimeMillis() - 1000), new Date()); information.expireNow(); given(registry.getSessionInformation(anyString())).willReturn(information); return registry; diff --git a/web/src/test/java/org/springframework/security/web/session/SessionInformationExpiredEventTests.java b/web/src/test/java/org/springframework/security/web/session/SessionInformationExpiredEventTests.java index 99d0cbef939..b00976da7ab 100644 --- a/web/src/test/java/org/springframework/security/web/session/SessionInformationExpiredEventTests.java +++ b/web/src/test/java/org/springframework/security/web/session/SessionInformationExpiredEventTests.java @@ -43,20 +43,22 @@ public void constructorWhenSessionInformationNullThenThrowsException() { @Test public void constructorWhenRequestNullThenThrowsException() { assertThatIllegalArgumentException().isThrownBy(() -> new SessionInformationExpiredEvent( - new SessionInformation("fake", "sessionId", new Date()), null, new MockHttpServletResponse())); + new SessionInformation("fake", "sessionId", new Date(), new Date()), null, + new MockHttpServletResponse())); } @Test public void constructorWhenResponseNullThenThrowsException() { assertThatIllegalArgumentException().isThrownBy(() -> new SessionInformationExpiredEvent( - new SessionInformation("fake", "sessionId", new Date()), new MockHttpServletRequest(), null)); + new SessionInformation("fake", "sessionId", new Date(), new Date()), new MockHttpServletRequest(), + null)); } @Test void constructorWhenFilterChainThenGetFilterChainReturnsNotNull() { MockFilterChain filterChain = new MockFilterChain(); SessionInformationExpiredEvent event = new SessionInformationExpiredEvent( - new SessionInformation("fake", "sessionId", new Date()), new MockHttpServletRequest(), + new SessionInformation("fake", "sessionId", new Date(), new Date()), new MockHttpServletRequest(), new MockHttpServletResponse(), filterChain); assertThat(event.getFilterChain()).isSameAs(filterChain); }