diff --git a/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java b/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java index 6225164e3..32b81a871 100644 --- a/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java +++ b/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java @@ -151,10 +151,10 @@ public class EndpointInterceptor implements HandlerInterceptor { if ((userSessions >= maxUserSessions || totalSessionsNonExpired >= maxApplicationSessions) && !hasUserActiveSession) { - response.sendError( - HttpServletResponse.SC_UNAUTHORIZED, + 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; } @@ -203,10 +203,10 @@ public class EndpointInterceptor implements HandlerInterceptor { if (totalSessions >= maxApplicationSessions && !hasUserActiveSession) { sessionsInterface.removeSession(finalSession); - response.sendError( - HttpServletResponse.SC_UNAUTHORIZED, + 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; } if (!hasUserActiveSession) { diff --git a/src/main/java/stirling/software/SPDF/config/anonymus/session/AnonymusSessionListener.java b/src/main/java/stirling/software/SPDF/config/anonymus/session/AnonymusSessionListener.java index 93b56cee9..37b4cfe22 100644 --- a/src/main/java/stirling/software/SPDF/config/anonymus/session/AnonymusSessionListener.java +++ b/src/main/java/stirling/software/SPDF/config/anonymus/session/AnonymusSessionListener.java @@ -193,8 +193,15 @@ public class AnonymusSessionListener implements HttpSessionListener, SessionsInt @Override public void removeSession(HttpSession session) { AnonymusSessionInfo sessionsInfo = (AnonymusSessionInfo) sessions.get(session.getId()); - sessionsInfo.setExpired(true); - session.invalidate(); + if (sessionsInfo != null) { + sessionsInfo.setExpired(true); + } + try { + session.invalidate(); + } catch (IllegalStateException e) { + log.debug("Session {} already invalidated", session.getId()); + } + sessions.remove(session.getId()); sessions.remove(session.getId()); } diff --git a/src/main/java/stirling/software/SPDF/config/anonymus/session/AnonymusSessionStatusController.java b/src/main/java/stirling/software/SPDF/config/anonymus/session/AnonymusSessionStatusController.java index af669e309..2ef42c01c 100644 --- a/src/main/java/stirling/software/SPDF/config/anonymus/session/AnonymusSessionStatusController.java +++ b/src/main/java/stirling/software/SPDF/config/anonymus/session/AnonymusSessionStatusController.java @@ -1,9 +1,10 @@ package stirling.software.SPDF.config.anonymus.session; +import java.util.List; + import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -12,72 +13,44 @@ import jakarta.servlet.http.HttpSession; import lombok.extern.slf4j.Slf4j; +import stirling.software.SPDF.config.interfaces.SessionsModelInterface; + @Controller @Slf4j public class AnonymusSessionStatusController { @Autowired private AnonymusSessionListener sessionRegistry; - @GetMapping("/session/status") - public ResponseEntity getSessionStatus(HttpServletRequest request) { - HttpSession session = request.getSession(false); - if (session == null) { - // No session found - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("No session found"); - } - - boolean isActiveSession = - sessionRegistry.getAllSessions().stream() - .filter(s -> s.getSessionId().equals(session.getId())) - .anyMatch(s -> !s.isExpired()); - - long sessionCount = - sessionRegistry.getAllSessions().stream().filter(s -> !s.isExpired()).count(); - - long userSessions = sessionCount; - int maxUserSessions = sessionRegistry.getMaxUserSessions(); - - // Session invalid or expired - if (userSessions >= maxUserSessions && !isActiveSession) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED) - .body("Session invalid or expired"); - } - // Valid session - else if (session.getId() != null && isActiveSession) { - return ResponseEntity.ok("Valid session: " + session.getId()); - } - // Fallback message with session count - else { - return ResponseEntity.ok("User has " + userSessions + " sessions"); - } - } - - @GetMapping("/session/expire") - public String expireSession(HttpServletRequest request) { + @GetMapping("/userSession") + public String getUserSessions(HttpServletRequest request, Model model) { HttpSession session = request.getSession(false); if (session != null) { - // Invalidate current session - sessionRegistry.expireFirstSession(session.getId()); - log.info("Session invalidated: {}", session.getId()); - // return ResponseEntity.ok("Session invalidated"); - } else { - log.info("No session to invalidate"); - // return ResponseEntity.ok("No session to invalidate"); + + boolean isSessionValid = + sessionRegistry.getAllNonExpiredSessions().stream() + .allMatch( + sessionEntity -> + sessionEntity.getSessionId().equals(session.getId())); + + // Get all sessions for the user + List sessionList = + sessionRegistry.getAllNonExpiredSessions().stream() + .filter( + sessionEntity -> + !sessionEntity.getSessionId().equals(session.getId())) + .toList(); + + model.addAttribute("sessionList", sessionList); + return "userSession"; } return "redirect:/"; } - @GetMapping("/session/expire/all") - public ResponseEntity expireAllSessions() { - // Invalidate all sessions - sessionRegistry.expireAllSessions(); - return ResponseEntity.ok("All sessions invalidated"); - } - - @GetMapping("/session/expire/{username}") - public ResponseEntity expireAllSessionsByUsername(@PathVariable String username) { - // Invalidate all sessions for specific user - sessionRegistry.expireAllSessionsByUsername(username); - return ResponseEntity.ok("All sessions invalidated for user: " + username); + @GetMapping("/userSession/invalidate/{sessionId}") + public String invalidateUserSession( + HttpServletRequest request, @PathVariable String sessionId) { + sessionRegistry.expireSession(sessionId); + sessionRegistry.registerSession(request.getSession(false)); + return "redirect:/userSession"; } } diff --git a/src/main/java/stirling/software/SPDF/config/security/session/CustomHttpSessionListener.java b/src/main/java/stirling/software/SPDF/config/security/session/CustomHttpSessionListener.java index a6b43cdee..93c861600 100644 --- a/src/main/java/stirling/software/SPDF/config/security/session/CustomHttpSessionListener.java +++ b/src/main/java/stirling/software/SPDF/config/security/session/CustomHttpSessionListener.java @@ -142,25 +142,25 @@ public class CustomHttpSessionListener implements HttpSessionListener, SessionsI .filter(s -> !s.isExpired() && s.getPrincipalName().equals(principalName)) .toList() .size(); - boolean isAnonymousUserWithoutLogin = "anonymousUser".equals(principalName) && loginEnabled; + boolean isAnonymousUserWithLogin = "anonymousUser".equals(principalName) && loginEnabled; log.info( - "all {} allNonExpiredSessions {} {} isAnonymousUserWithoutLogin {}", + "all {} allNonExpiredSessions {} {} isAnonymousUserWithLogin {}", all, allNonExpiredSessions, getMaxUserSessions(), - isAnonymousUserWithoutLogin); + isAnonymousUserWithLogin); - if (allNonExpiredSessions >= getMaxApplicationSessions() && !isAnonymousUserWithoutLogin) { + if (allNonExpiredSessions >= getMaxApplicationSessions() && !isAnonymousUserWithLogin) { log.info("Session {} Expired=TRUE", session.getId()); sessionPersistentRegistry.expireSession(session.getId()); sessionPersistentRegistry.removeSessionInformation(se.getSession().getId()); // if (allNonExpiredSessions > getMaxUserSessions()) { // enforceMaxSessionsForPrincipal(principalName); // } - } else if (all >= getMaxUserSessions() && !isAnonymousUserWithoutLogin) { + } else if (all >= getMaxUserSessions() && !isAnonymousUserWithLogin) { enforceMaxSessionsForPrincipal(principalName); log.info("Session {} Expired=TRUE", session.getId()); - } else if (isAnonymousUserWithoutLogin) { + } else if (isAnonymousUserWithLogin) { sessionPersistentRegistry.expireSession(session.getId()); sessionPersistentRegistry.removeSessionInformation(se.getSession().getId()); } else { 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 4eb70ec72..328fc9c9c 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 @@ -3,11 +3,8 @@ package stirling.software.SPDF.config.anonymus.session; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.security.core.Authentication; -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; @@ -29,35 +26,32 @@ import stirling.software.SPDF.config.security.session.SessionPersistentRegistry; @Slf4j public class SessionStatusController { + @Qualifier("loginEnabled") + private boolean loginEnabled; + @Autowired private SessionPersistentRegistry sessionPersistentRegistry; @Autowired private SessionsInterface sessionInterface; @Autowired private CustomHttpSessionListener customHttpSessionListener; - // Returns the current session ID or 401 if no session exists - @GetMapping("/session") - public ResponseEntity getSession(HttpServletRequest request) { - HttpSession session = request.getSession(false); - if (session == null) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("No session found"); - } else { - return ResponseEntity.ok(session.getId()); - } - } - // 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()) { + if ((authentication == null || !authentication.isAuthenticated()) && loginEnabled) { 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"; + String principalName = null; + if (authentication != null && authentication.isAuthenticated()) { + Object principal = authentication.getPrincipal(); + principalName = UserUtils.getUsernameFromPrincipal(principal); + if (principalName == null) { + return "redirect:/login"; + } + } else { + principalName = "anonymousUser"; } boolean isSessionValid = @@ -139,81 +133,4 @@ public class SessionStatusController { return "redirect:/login"; } } - - // Checks if the session is active and valid according to user session limits - @GetMapping("/session/status") - public ResponseEntity getSessionStatus(HttpServletRequest request) { - HttpSession session = request.getSession(false); - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - - if (authentication != null && authentication.isAuthenticated()) { - Object principalTest = authentication.getPrincipal(); - String username = UserUtils.getUsernameFromPrincipal(principalTest); - - List allSessions = - sessionPersistentRegistry.getAllSessions(username, false); - - boolean isActivSession = - sessionPersistentRegistry.getAllSessions().stream() - .filter( - sessionEntity -> - session.getId().equals(sessionEntity.getSessionId())) - .anyMatch(sessionEntity -> !sessionEntity.isExpired()); - - int userSessions = allSessions.size(); - int maxUserSessions = sessionInterface.getMaxUserSessions(); - - // Check if the current session is valid or expired based on the session registry - if (userSessions >= maxUserSessions && !isActivSession) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED) - .body("Session invalid or expired"); - } else if (session.getId() != null && isActivSession) { - return ResponseEntity.ok("Valid session: " + session.getId()); - } else { - return ResponseEntity.ok( - "User: " + username + " has " + userSessions + " sessions"); - } - } else { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED) - .body("Session invalid or expired"); - } - } - - // Invalidates the current session - @GetMapping("/session/expire") - public ResponseEntity expireSession(HttpServletRequest request) { - HttpSession session = request.getSession(false); - if (session != null) { - sessionPersistentRegistry.expireSession(session.getId()); - return ResponseEntity.ok("Session invalidated"); - } else { - return ResponseEntity.ok("No session to invalidate"); - } - } - - // Invalidates all sessions - @GetMapping("/session/expire/all") - public ResponseEntity expireAllSessions() { - log.debug("Expire all sessions"); - sessionPersistentRegistry.expireAllSessions(); - return ResponseEntity.ok("All sessions invalidated"); - } - - // Invalidates all sessions for a specific user, only if requested by the same user - @GetMapping("/session/expire/{username}") - public ResponseEntity expireAllSessionsByUsername(@PathVariable String username) { - SecurityContext cxt = SecurityContextHolder.getContext(); - Authentication auth = cxt.getAuthentication(); - if (auth != null && auth.isAuthenticated()) { - Object principal = auth.getPrincipal(); - String principalName = UserUtils.getUsernameFromPrincipal(principal); - if (principalName.equals(username)) { - sessionPersistentRegistry.expireAllSessionsByUsername(username); - return ResponseEntity.ok("All sessions invalidated for user: " + username); - } else { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Unauthorized"); - } - } - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Unauthorized"); - } } diff --git a/src/main/resources/messages_de_DE.properties b/src/main/resources/messages_de_DE.properties index fc74a15da..709966ffb 100644 --- a/src/main/resources/messages_de_DE.properties +++ b/src/main/resources/messages_de_DE.properties @@ -287,6 +287,14 @@ database.notSupported=Diese Funktion ist für deine Datenbankverbindung nicht ve session.expired=Ihre Sitzung ist abgelaufen. Bitte laden Sie die Seite neu und versuchen Sie es erneut. session.refreshPage=Seite aktualisieren +################# +# USER SESSION # +################# +userSession.title=Benutzersitzungen +userSession.header=Benutzersitzungen +userSession.maxUserSession=Wenn die maximale Anzahl Sitzungen für diesen Benutzer erreicht ist, können Sie hier andere Anmeldungen beenden, um auf diesem Gerät fortzufahren. +userSession.lastRequest=Letzte Aufrufe + ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_en_GB.properties b/src/main/resources/messages_en_GB.properties index 4bcc1a618..af3c58125 100644 --- a/src/main/resources/messages_en_GB.properties +++ b/src/main/resources/messages_en_GB.properties @@ -287,6 +287,14 @@ database.notSupported=This function is not available for your database connectio session.expired=Your session has expired. Please refresh the page and try again. session.refreshPage=Refresh Page +################# +# USER SESSION # +################# +userSession.title=User Sessions +userSession.header=User Sessions +userSession.maxUserSession=If the maximum number of sessions for this user is reached, you can end other logins here to continue on this device. +userSession.lastRequest=last Request + ############# # HOME-PAGE # ############# @@ -1428,7 +1436,7 @@ cookieBanner.preferencesModal.description.2=Stirling PDF cannot—and will never cookieBanner.preferencesModal.description.3=Your privacy and trust are at the core of what we do. cookieBanner.preferencesModal.necessary.title.1=Strictly Necessary Cookies cookieBanner.preferencesModal.necessary.title.2=Always Enabled -cookieBanner.preferencesModal.necessary.description=These cookies are essential for the website to function properly. They enable core features like setting your privacy preferences, logging in, and filling out forms—which is why they can’t be turned off. +cookieBanner.preferencesModal.necessary.description=These cookies are essential for the website to function properly. They enable core features like setting your privacy preferences, logging in, and filling out forms—which is why they can’t be turned off. cookieBanner.preferencesModal.analytics.title=Analytics cookieBanner.preferencesModal.analytics.description=These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with. diff --git a/src/main/resources/templates/error.html b/src/main/resources/templates/error.html index a99f02832..daf9029ab 100644 --- a/src/main/resources/templates/error.html +++ b/src/main/resources/templates/error.html @@ -15,8 +15,7 @@

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


diff --git a/src/main/resources/templates/userSession.html b/src/main/resources/templates/userSession.html index f08b589d8..fa02014c3 100644 --- a/src/main/resources/templates/userSession.html +++ b/src/main/resources/templates/userSession.html @@ -3,7 +3,7 @@ xmlns:th="https://www.thymeleaf.org"> - + @@ -16,21 +16,21 @@
key - User Session + User Session
- Max sessions reached for this user. To continue on this device, please close your session in another browser. - + Max sessions reached for this user. +
- - - + + + - +
Session IDlast RequestLogoutlast Request