diff --git a/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java b/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java index 562f847da..6225164e3 100644 --- a/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java +++ b/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java @@ -46,10 +46,12 @@ public class EndpointInterceptor implements HandlerInterceptor { Principal principal = request.getUserPrincipal(); // allowlist for public or static routes - if ("/".equals(requestURI) - || "/login".equals(requestURI) - || "/home".equals(requestURI) - || "/home-legacy".equals(requestURI) + if ("/login".equals(requestURI) + // || "/".equals(requestURI) + // || "/home".equals(requestURI) + // || "/home-legacy".equals(requestURI) + || "/userSession".equals(requestURI) + || requestURI.contains("/userSession/invalidate/") || requestURI.contains("/js/") || requestURI.contains("/css/") || requestURI.contains("/fonts/") @@ -76,10 +78,22 @@ public class EndpointInterceptor implements HandlerInterceptor { .filter(s -> s.getSessionId().equals(finalSession.getId())) .anyMatch(s -> s.isExpired()); - if (isExpiredByAdmin) { + if (isExpiredByAdmin + && !"/".equals(requestURI) + && !"/home".equals(requestURI) + && !"/home-legacy".equals(requestURI)) { response.sendRedirect("/logout"); log.info("Session expired. Logging out user {}", principal.getName()); return false; + } else if (isExpiredByAdmin + && ("/".equals(requestURI) + || "/home".equals(requestURI) + || "/home-legacy".equals(requestURI))) { + log.info( + "Max sessions reached for this user. To continue on this device, please" + + " close your session in another browser. "); + response.sendError(HttpServletResponse.SC_EXPECTATION_FAILED); + return false; } int maxApplicationSessions = sessionsInterface.getMaxApplicationSessions(); @@ -101,7 +115,15 @@ public class EndpointInterceptor implements HandlerInterceptor { .toList(); boolean hasUserActiveSession = - activeSessions.stream().anyMatch(s -> s.getSessionId().equals(sessionId)); + // activeSessions.stream().anyMatch(s -> + // s.getSessionId().equals(sessionId)); + activeSessions.stream() + .anyMatch( + s -> + s.getSessionId().equals(sessionId) + // && !s.isExpired() + && s.getPrincipalName() + .equals(principal.getName())); final String currentPrincipal = principal.getName(); diff --git a/src/main/java/stirling/software/SPDF/config/security/session/SessionPersistentRegistry.java b/src/main/java/stirling/software/SPDF/config/security/session/SessionPersistentRegistry.java index 085d209f2..f7d1dedc8 100644 --- a/src/main/java/stirling/software/SPDF/config/security/session/SessionPersistentRegistry.java +++ b/src/main/java/stirling/software/SPDF/config/security/session/SessionPersistentRegistry.java @@ -83,9 +83,6 @@ public class SessionPersistentRegistry implements SessionRegistry { int sessionUserCount = getAllSessions(principalName, false).size(); - if (sessionUserCount >= getMaxUserSessions()) { - return; - } SessionEntity sessionEntity = sessionRepository.findBySessionId(sessionId); if (sessionEntity == null) { sessionEntity = new SessionEntity(); @@ -94,7 +91,12 @@ public class SessionPersistentRegistry implements SessionRegistry { } sessionEntity.setPrincipalName(principalName); sessionEntity.setLastRequest(new Date()); // Set lastRequest to the current date - sessionEntity.setExpired(false); + + if (sessionUserCount >= getMaxUserSessions()) { + sessionEntity.setExpired(true); + } else { + sessionEntity.setExpired(false); + } sessionRepository.save(sessionEntity); sessionRepository.flush(); } diff --git a/src/main/java/stirling/software/SPDF/config/security/session/SessionStatusController.java b/src/main/java/stirling/software/SPDF/config/security/session/SessionStatusController.java index 9459d760d..4eb70ec72 100644 --- a/src/main/java/stirling/software/SPDF/config/security/session/SessionStatusController.java +++ b/src/main/java/stirling/software/SPDF/config/security/session/SessionStatusController.java @@ -10,9 +10,11 @@ import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.session.SessionInformation; import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; @@ -43,6 +45,72 @@ public class SessionStatusController { } } + // list all sessions from authentication user, return String redirect userSession.html + @GetMapping("/userSession") + public String getUserSessions( + HttpServletRequest request, Model model, Authentication authentication) { + if (authentication == null || !authentication.isAuthenticated()) { + return "redirect:/login"; + } + HttpSession session = request.getSession(false); + if (session != null) { + Object principal = authentication.getPrincipal(); + String principalName = UserUtils.getUsernameFromPrincipal(principal); + if (principalName == null) { + return "redirect:/login"; + } + + boolean isSessionValid = + sessionPersistentRegistry.getAllSessions(principalName, false).stream() + .allMatch( + sessionEntity -> + sessionEntity.getSessionId().equals(session.getId())); + + if (isSessionValid) { + return "redirect:/"; + } + // Get all sessions for the user + List sessionList = + sessionPersistentRegistry.getAllSessions(principalName, false).stream() + .filter( + sessionEntity -> + !sessionEntity.getSessionId().equals(session.getId())) + .toList(); + + model.addAttribute("sessionList", sessionList); + return "userSession"; + } + return "redirect:/login"; + } + + @GetMapping("/userSession/invalidate/{sessionId}") + public String invalidateUserSession( + HttpServletRequest request, + Authentication authentication, + @PathVariable String sessionId) + throws ServletException { + if (authentication == null || !authentication.isAuthenticated()) { + return "redirect:/login"; + } + Object principal = authentication.getPrincipal(); + String principalName = UserUtils.getUsernameFromPrincipal(principal); + if (principalName == null) { + return "redirect:/login"; + } + boolean isOwner = + sessionPersistentRegistry.getAllSessions(principalName, false).stream() + .anyMatch(session -> session.getSessionId().equals(sessionId)); + if (isOwner) { + customHttpSessionListener.expireSession(sessionId, false); + sessionPersistentRegistry.registerNewSession( + request.getRequestedSessionId().split(".node0")[0], principal); + // return "redirect:/userSession?messageType=sessionInvalidated" + return "redirect:/userSession"; + } else { + return "redirect:/login"; + } + } + @GetMapping("/session/invalidate/{sessionId}") public String invalidateSession( HttpServletRequest request, diff --git a/src/main/resources/templates/error.html b/src/main/resources/templates/error.html index 00f22c27b..a99f02832 100644 --- a/src/main/resources/templates/error.html +++ b/src/main/resources/templates/error.html @@ -14,6 +14,10 @@

+

+ Max sessions reached for this user. To continue on this device, please close your + session in another browser. +


@@ -21,7 +25,7 @@ - + diff --git a/src/main/resources/templates/fragments/navbar.html b/src/main/resources/templates/fragments/navbar.html index b338f3f7d..f2d6c2ff6 100644 --- a/src/main/resources/templates/fragments/navbar.html +++ b/src/main/resources/templates/fragments/navbar.html @@ -259,6 +259,8 @@ diff --git a/src/main/resources/templates/userSession.html b/src/main/resources/templates/userSession.html new file mode 100644 index 000000000..f08b589d8 --- /dev/null +++ b/src/main/resources/templates/userSession.html @@ -0,0 +1,52 @@ + + + + + + + + +
+
+ +

+
+
+
+
+ key + User Session +
+
+ Max sessions reached for this user. To continue on this device, please close your session in another browser. + + + + + + + + + + + + + + + +
Session IDlast RequestLogout
+ logout + +
+
+
+
+
+
+ +
+ + + \ No newline at end of file