Skip to content

Commit

Permalink
Merge pull request #6380 from effective-webwork/automatic-logout
Browse files Browse the repository at this point in the history
Logout idle user automatically when HTTP session expires
  • Loading branch information
solth authored Jan 27, 2025
2 parents 9185945 + 56e7145 commit c8255e7
Show file tree
Hide file tree
Showing 10 changed files with 194 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import javax.enterprise.context.RequestScoped;
import javax.faces.context.FacesContext;
import javax.inject.Named;
import javax.servlet.http.HttpSession;

import org.kitodo.data.database.beans.Client;
import org.kitodo.data.database.beans.Project;
Expand Down Expand Up @@ -183,4 +184,29 @@ public List<Client> getAvailableClientsOfCurrentUserSortedByName() {
return getAvailableClientsOfCurrentUser().stream().sorted(Comparator.comparing(Client::getName))
.collect(Collectors.toList());
}

/**
* Get amount of time that warning message is displayed to inform user that he will be logged
* out of the system automatically due to inactivity. Value returned in seconds.
* If the session HTTP session timeout configured in the 'web.xml' file is 60 seconds or less,
* the message will be shown 30 seconds before logout. If the timeout is between 1 and 5 minutes,
* the message will appear 60 seconds before logout. For any session timeout larger than 5 Minutes,
* it will be shown 300 seconds in advance.
* @return number of seconds the warning message is displayed to the user before automatic logout
*/
public int getAutomaticLogoutWarningSeconds() {
FacesContext facesContext = FacesContext.getCurrentInstance();
if (Objects.nonNull(facesContext)) {
HttpSession session = (HttpSession) facesContext.getExternalContext().getSession(false);
int maxInactiveInterval = session.getMaxInactiveInterval();
if (maxInactiveInterval <= 60) {
return 30;
} else if (maxInactiveInterval < 300) {
return 60;
} else {
return 300;
}
}
return 60;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* (c) Kitodo. Key to digital objects e. V. <[email protected]>
*
* This file is part of the Kitodo project.
*
* It is licensed under GNU General Public License version 3 or later.
*
* For the full copyright and license information, please read the
* GPL3-License.txt file that was distributed with this source code.
*/

package org.kitodo.production.helper;

import java.util.Iterator;

import javax.enterprise.context.RequestScoped;
import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;
import javax.inject.Named;

import org.primefaces.PrimeFaces;

@Named
@RequestScoped
public class ActivityMonitor {

/**
* Event handler for 'idle' event. Triggered when user becomes idle and is about to be logged out automatically.
* Displays a warning message to inform the user he is about to get logged out soon.
*/
public void onIdle() {
String warningTitle = Helper.getTranslation("automaticLogoutWarningTitle");
String warningDescription = Helper.getTranslation("automaticLogoutWarningDescription");
PrimeFaces.current().executeScript("PF('sticky-notifications').renderMessage("
+ "{'summary':'" + warningTitle + "','detail':'" + warningDescription + "','severity':'error'});");
}

/**
* Event handler for 'active' event. Triggered when user becomes active again after being idle.
* Removes the warning message about pending automatic logout.
*/
public void onActive() {
Iterator<FacesMessage> messageIterator = FacesContext.getCurrentInstance().getMessages();
while (messageIterator.hasNext()) {
messageIterator.next();
messageIterator.remove();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
package org.kitodo.production.security;

import java.io.IOException;
import java.text.MessageFormat;
import java.util.Objects;

import javax.servlet.http.HttpServletRequest;
Expand Down Expand Up @@ -53,8 +52,8 @@ public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse resp
UserDetails user = (UserDetails) principal;
ServiceManager.getSessionService().expireSessionsOfUser(user);
} else {
logger.warn(MessageFormat.format("Cannot expire session: {0} is not an instance of UserDetails",
Helper.getObjectDescription(principal)));
logger.warn("Cannot expire session: {} is not an instance of UserDetails",
Helper.getObjectDescription(principal));
}
} else {
logger.warn("Cannot expire session: authentication.getDetails() is null");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,21 @@

package org.kitodo.production.services.security;

import java.text.MessageFormat;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.kitodo.production.helper.Helper;
import org.kitodo.production.metadata.MetadataLock;
import org.kitodo.production.security.SecurityConfig;
import org.kitodo.production.security.SecuritySession;
import org.kitodo.production.security.SecurityUserDetails;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.userdetails.UserDetails;

public class SessionService implements HttpSessionListener {
public class SessionService {

private static final Logger logger = LogManager.getLogger(SessionService.class);
private static volatile SessionService instance = null;
private final SessionRegistry sessionRegistry;

Expand All @@ -46,27 +37,6 @@ private SessionService() {
this.sessionRegistry = securityConfig.getSessionRegistry();
}

/*
* This function is called when the session from the servlet container expires.
*/
@Override
public void sessionDestroyed(HttpSessionEvent se) {
Object securityContextObject = se.getSession().getAttribute("SPRING_SECURITY_CONTEXT");
if (securityContextObject instanceof SecurityContextImpl) {
SecurityContextImpl securityContext = (SecurityContextImpl) securityContextObject;
Object principal = securityContext.getAuthentication().getPrincipal();
if (principal instanceof SecurityUserDetails) {
expireSessionsOfUser((SecurityUserDetails) principal);
} else {
logger.warn(MessageFormat.format("Cannot expire session: {0} is not an instance of SecurityUserDetails",
Helper.getObjectDescription(principal)));
}
} else {
logger.warn(MessageFormat.format("Cannot expire session: {0} is not an instance of SecurityContextImpl",
Helper.getObjectDescription(securityContextObject)));
}
}

/**
* Expires all active sessions of a spring security UserDetails object.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* (c) Kitodo. Key to digital objects e. V. <[email protected]>
*
* This file is part of the Kitodo project.
*
* It is licensed under GNU General Public License version 3 or later.
*
* For the full copyright and license information, please read the
* GPL3-License.txt file that was distributed with this source code.
*/


package org.kitodo.production.session;

import java.util.Objects;

import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.kitodo.production.helper.Helper;
import org.kitodo.production.security.SecurityUserDetails;
import org.kitodo.production.services.ServiceManager;
import org.springframework.security.core.context.SecurityContextImpl;


@WebListener
public class CustomHttpSessionListener implements HttpSessionListener {

private static final Logger logger = LogManager.getLogger(CustomHttpSessionListener.class);

/**
* Event handler that is triggere when an HTTP session is created.
*
* @param sessionEvent the notification event
*/
@Override
public void sessionCreated(HttpSessionEvent sessionEvent) {
logger.debug("Session created: {}", sessionEvent.getSession().getId());
}

/**
* Event handler that is triggered when an HTTP session expires.
*
* @param sessionEvent the notification event
*/
@Override
public void sessionDestroyed(HttpSessionEvent sessionEvent) {
Object securityContextObject = sessionEvent.getSession().getAttribute("SPRING_SECURITY_CONTEXT");
if (Objects.nonNull(securityContextObject) && securityContextObject instanceof SecurityContextImpl) {
SecurityContextImpl securityContext = (SecurityContextImpl) securityContextObject;
Object principal = securityContext.getAuthentication().getPrincipal();
if (principal instanceof SecurityUserDetails) {
logger.debug("Session expired: {}", sessionEvent.getSession().getId());
ServiceManager.getSessionService().expireSessionsOfUser((SecurityUserDetails) principal);
} else {
logger.debug("Cannot expire session: {} is not an instance of SecurityUserDetails",
Helper.getObjectDescription(principal));
}
} else {
logger.debug("Cannot expire session: {} is not an instance of SecurityContextImpl",
Helper.getObjectDescription(securityContextObject));
}
}
}
2 changes: 2 additions & 0 deletions Kitodo/src/main/resources/messages/messages_de.properties
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ actions=Aktionen
audio=Audio
automatic=automatisch
automaticDmsImport=Automatischer DMS-Export
automaticLogoutWarningDescription=Aufgrund von Inaktivit\u00E4t werden Sie in K\u00FCrze automatisch ausgeloggt...
automaticLogoutWarningTitle=Keine Aktivit\u00E4t festgestellt
automaticTask=Automatische Aufgabe
automaticTasks=Automatische Aufgaben
author=Autor
Expand Down
2 changes: 2 additions & 0 deletions Kitodo/src/main/resources/messages/messages_en.properties
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ actions=Actions
audio=Audio
automatic=automatic
automaticDmsImport=Automatic DMS export
automaticLogoutWarningDescription=Pending logout due to inactivity...
automaticLogoutWarningTitle=No activity registered
automaticTask=Automatic task
automaticTasks=Automatic tasks
author=Author
Expand Down
18 changes: 18 additions & 0 deletions Kitodo/src/main/webapp/WEB-INF/resources/js/defaultScript.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,21 @@
$(document).ready(function() {
$('#loadingScreen').hide();
});

window.updateLogoutCountdown = function(t) {
let growlMessage = $('#sticky-notifications_container div.ui-growl-message p');
let currentTime;
let minutes = Math.floor(t.current / 60);
let seconds = t.current % 60;
if (seconds < 10) {
currentTime = minutes + ":0" + seconds;
} else {
currentTime = minutes + ":" + seconds;
}
let currentMessage = growlMessage.text();
if (currentMessage.match(/\d+:\d+/g)) {
growlMessage.text(currentMessage.replace(/\d+:\d+/g, currentTime));
} else {
growlMessage.text(currentMessage + " " + currentTime);
}
};
26 changes: 26 additions & 0 deletions Kitodo/src/main/webapp/WEB-INF/templates/base.xhtml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
xmlns:pe="http://primefaces.org/ui/extensions"
xmlns:o="http://omnifaces.org/ui">

<f:view locale="#{LanguageForm.locale}">
Expand Down Expand Up @@ -80,6 +81,31 @@
target="body" />
</ui:insert>
<o:highlight styleClass="ui-state-error" />
<ui:fragment id="activityMonitors"
rendered="#{not empty SessionClientController.currentSessionClient and session.maxInactiveInterval gt 0}">
<p:idleMonitor timeout="#{(session.maxInactiveInterval - SessionClientController.getAutomaticLogoutWarningSeconds()) * 1000}"
multiWindowSupport="true"
onidle="PF('logoutTimer').start()"
onactive="PF('logoutTimer').pause()">
<p:ajax event="idle"
listener="#{activityMonitor.onIdle}"/>
<p:ajax event="active"
oncomplete="PF('logoutTimer').currentTimeout = #{SessionClientController.getAutomaticLogoutWarningSeconds()};"
listener="#{activityMonitor.onActive}"
update="sticky-notifications"/>
</p:idleMonitor>
<h:form id="timerForm">
<pe:timer id="timer"
resetValues="pause"
visible="false"
widgetVar="logoutTimer"
autoStart="false"
timeout="#{SessionClientController.getAutomaticLogoutWarningSeconds()}"
update="sticky-notifications"
ontimerstep="updateLogoutCountdown(intervalData)"
ontimercomplete="window.location.replace('/kitodo/logout');"/>
</h:form>
</ui:fragment>
</h:body>
</f:view>
</html>
1 change: 1 addition & 0 deletions Kitodo/src/main/webapp/pages/metadataEditor.xhtml
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@
</ui:define>

<ui:define name="page-scripts">
<h:outputScript name="js/defaultScript.js" target="body"/>
<h:outputScript name="js/resize.js" target="body"/>
<h:outputScript name="js/scroll.js" target="body"/>
<h:outputScript name="js/metadata_editor.js" target="body"/>
Expand Down

0 comments on commit c8255e7

Please sign in to comment.