-
Notifications
You must be signed in to change notification settings - Fork 44
Expand file tree
/
Copy pathSessionInvalidationService.java
More file actions
102 lines (90 loc) · 4.53 KB
/
Copy pathSessionInvalidationService.java
File metadata and controls
102 lines (90 loc) · 4.53 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
package com.digitalsanctuary.spring.user.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.stereotype.Service;
import com.digitalsanctuary.spring.user.persistence.model.User;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* Service for invalidating user sessions. This is useful for admin-initiated password resets
* and other security operations that require forcing users to re-authenticate.
*
* <p><strong>Race Condition Note:</strong> This service uses Spring's SessionRegistry to track
* and invalidate sessions. Due to the nature of the SessionRegistry API, there is an inherent
* race condition: sessions created after {@link SessionRegistry#getAllPrincipals()} is called
* but before {@link SessionInformation#expireNow()} completes will not be invalidated. This is
* a known limitation of the SessionRegistry approach. For most use cases (admin password reset),
* this is acceptable as the window is very small.</p>
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class SessionInvalidationService {
private final SessionRegistry sessionRegistry;
/** Threshold for warning about high principal count that may impact performance. */
@Value("${user.session.invalidation.warn-threshold:1000}")
private int warnThreshold;
/**
* Invalidates all active sessions for the given user.
* This forces the user to re-authenticate on their next request.
*
* <p><strong>Note:</strong> Sessions created after this method starts iterating
* but before it completes will not be invalidated. This race condition is inherent
* to the SessionRegistry API and is acceptable for most security operations.</p>
*
* @param user the user whose sessions should be invalidated
* @return the number of sessions that were invalidated
*/
public int invalidateUserSessions(User user) {
if (user == null) {
log.warn("SessionInvalidationService.invalidateUserSessions: user is null");
return 0;
}
int invalidatedCount = 0;
List<Object> principals = sessionRegistry.getAllPrincipals();
// Performance monitoring: warn if principal count is high
if (principals.size() > warnThreshold) {
log.warn("SessionInvalidationService.invalidateUserSessions: high principal count ({}) may impact performance",
principals.size());
}
log.debug("SessionInvalidationService.invalidateUserSessions: scanning {} principals for user {}",
principals.size(), user.getEmail());
// NOTE: Sessions created after getAllPrincipals() but before expireNow()
// will not be invalidated. This is a known limitation of SessionRegistry.
for (Object principal : principals) {
User principalUser = extractUser(principal);
if (principalUser != null && principalUser.getId().equals(user.getId())) {
List<SessionInformation> sessions = sessionRegistry.getAllSessions(principal, false);
for (SessionInformation session : sessions) {
session.expireNow();
invalidatedCount++;
// Log truncated session ID to avoid exposing full session identifiers
String sessionId = session.getSessionId();
String safeSessionId = sessionId.length() > 8 ? sessionId.substring(0, 8) + "..." : sessionId;
log.debug("SessionInvalidationService.invalidateUserSessions: expired session {} for user {}",
safeSessionId, user.getEmail());
}
}
}
log.info("SessionInvalidationService.invalidateUserSessions: invalidated {} sessions for user {} (scanned {} principals)",
invalidatedCount, user.getEmail(), principals.size());
return invalidatedCount;
}
/**
* Extracts the User object from a principal.
* Handles both User and DSUserDetails principal types.
*
* @param principal the security principal
* @return the User object, or null if not extractable
*/
private User extractUser(Object principal) {
if (principal instanceof User user) {
return user;
} else if (principal instanceof DSUserDetails dsUserDetails) {
return dsUserDetails.getUser();
}
return null;
}
}