From 7d642803031fe7f5ff70f07326af4a90a4fd1a6e Mon Sep 17 00:00:00 2001
From: felmer <felmer>
Date: Mon, 8 Sep 2008 12:53:50 +0000
Subject: [PATCH] LMS-445 introducing DefaultSessionManager in 'authentication'
 based on the code of ch.systemsx.cisd.lims.webservice.DefaultSessionManager

SVN: 8227
---
 .../authentication/DefaultSessionManager.java | 346 ++++++++++++++++++
 .../ILogMessagePrefixGenerator.java           |  37 ++
 .../cisd/authentication/ISessionManager.java  |   2 +-
 .../DefaultSessionManagerTest.java            | 302 +++++++++++++++
 4 files changed, 686 insertions(+), 1 deletion(-)
 create mode 100644 authentication/source/java/ch/systemsx/cisd/authentication/DefaultSessionManager.java
 create mode 100644 authentication/source/java/ch/systemsx/cisd/authentication/ILogMessagePrefixGenerator.java
 create mode 100644 authentication/sourceTest/java/ch/systemsx/cisd/authentication/DefaultSessionManagerTest.java

diff --git a/authentication/source/java/ch/systemsx/cisd/authentication/DefaultSessionManager.java b/authentication/source/java/ch/systemsx/cisd/authentication/DefaultSessionManager.java
new file mode 100644
index 00000000000..fcbeed50625
--- /dev/null
+++ b/authentication/source/java/ch/systemsx/cisd/authentication/DefaultSessionManager.java
@@ -0,0 +1,346 @@
+/*
+ * Copyright 2008 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.authentication;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang.time.DateUtils;
+import org.apache.commons.lang.time.DurationFormatUtils;
+import org.apache.log4j.Logger;
+
+import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException;
+import ch.systemsx.cisd.common.exceptions.InvalidSessionException;
+import ch.systemsx.cisd.common.exceptions.UserFailureException;
+import ch.systemsx.cisd.common.logging.LogCategory;
+import ch.systemsx.cisd.common.logging.LogFactory;
+import ch.systemsx.cisd.common.server.IRemoteHostProvider;
+import ch.systemsx.cisd.common.utilities.TokenGenerator;
+
+/**
+ * Default session manager. Needs  
+ * <ul><li>a {@link ISessionFactory} for creating new session objects, 
+ *     <li>a {@link ILogMessagePrefixGenerator} for generating log messages which are
+ *         logged by a logger with category {@link LogCategory#AUTH},
+ *     <li>a {@link IAuthenticationService} for authenticating users,
+ *     <li>a {@link IRemoteHostProvider} for providing the remote host of the user client.
+ * </ul>  
+ *
+ * @author Franz-Josef Elmer
+ */
+public class DefaultSessionManager<T extends BasicSession> implements ISessionManager<T>
+{
+    private static final String LOGOUT_PREFIX = "LOGOUT: ";
+    private static final String LOGIN_PREFIX = "LOGIN: ";
+    
+
+    private static final Logger authenticationLog =
+            LogFactory.getLogger(LogCategory.AUTH, DefaultSessionManager.class);
+    
+    private static final Logger operationLog =
+        LogFactory.getLogger(LogCategory.OPERATION, DefaultSessionManager.class);
+
+    private static final TokenGenerator tokenGenerator = new TokenGenerator();
+
+    private static final class FullSession<S>
+    {
+        /** Session data. */
+        private final S session;
+
+        /** The time period of inactivity (in milliseconds) after which the session will expire. */
+        private final long expirationPeriodMillis;
+
+        /** The last time when this session has been used (in milliseconds since 1970-01-01). */
+        private long lastActiveTime;
+
+        FullSession(final S session, final long expirationPeriodMillis)
+        {
+            assert session != null : "Undefined session";
+            assert expirationPeriodMillis >= 0; // == 0 is for unit tests
+
+            this.session = session;
+            this.expirationPeriodMillis = expirationPeriodMillis;
+            touch();
+        }
+
+        /**
+         * Returns the session.
+         */
+        public S getSession()
+        {
+            return session;
+        }
+
+        /**
+         * Sets the time of last activity (used to determine whether the session
+         * {@link #hasExpired()}.
+         */
+        void touch()
+        {
+            this.lastActiveTime = System.currentTimeMillis();
+        }
+
+        /**
+         * Returns <code>true</code> if the session has expired.
+         */
+        boolean hasExpired()
+        {
+            return System.currentTimeMillis() - lastActiveTime > expirationPeriodMillis;
+        }
+    }
+
+    private final ISessionFactory<T> sessionFactory;
+    
+    private final ILogMessagePrefixGenerator<T> prefixGenerator;
+    
+    /**
+     * The map of session tokens to sessions. Access to this data structure needs to be
+     * synchronized.
+     */
+    private final Map<String, FullSession<T>> sessions = new LinkedHashMap<String, FullSession<T>>();
+
+    private final IAuthenticationService authenticationService;
+
+    private final IRemoteHostProvider remoteHostProvider;
+
+    /** The time after which an inactive session will be expired (in milliseconds). */
+    private final long sessionExpirationPeriodMillis;
+
+    public DefaultSessionManager(final ISessionFactory<T> sessionFactory,
+            final ILogMessagePrefixGenerator<T> prefixGenerator,
+            final IAuthenticationService authenticationService,
+            final IRemoteHostProvider remoteHostProvider, final int sessionExpirationPeriodMinutes)
+    {
+        assert sessionFactory != null : "Missing session factory.";
+        assert prefixGenerator != null : "Missing prefix generator";
+        assert authenticationService != null : "Missing authentication service.";
+        assert remoteHostProvider != null : "Missing remote host provider.";
+        assert sessionExpirationPeriodMinutes >= 0 : "Session experation time has to be a positive value: "
+                + sessionExpirationPeriodMinutes; // == 0 is for unit test
+        
+        this.sessionFactory = sessionFactory;
+        this.prefixGenerator = prefixGenerator;
+        this.authenticationService = authenticationService;
+        this.remoteHostProvider = remoteHostProvider;
+        sessionExpirationPeriodMillis =
+                sessionExpirationPeriodMinutes * DateUtils.MILLIS_PER_MINUTE;
+
+        operationLog.info(String.format("Authentication service: '%s'", authenticationService
+                .getClass().getName()));
+        operationLog.info(String.format("Session expiration period (ms): %d",
+                sessionExpirationPeriodMillis));
+        authenticationService.check();
+    }
+    
+    private final T createAndStoreSession(final String user, final Principal principal,
+            final long now)
+    {
+        final String sessionToken = user + "-" + tokenGenerator.getNewToken(now);
+        synchronized (sessions)
+        {
+            final T session =
+                    sessionFactory.create(sessionToken, user, principal, getRemoteHost(), now);
+            final FullSession<T> createdSession =
+                    new FullSession<T>(session, sessionExpirationPeriodMillis);
+            sessions.put(user, createdSession);
+            return session;
+        }
+    }
+
+    private static void checkIfNotBlank(final String object, final String name)
+            throws UserFailureException
+    {
+        if (StringUtils.isBlank(object))
+        {
+            throw UserFailureException.fromTemplate("No '%s' specified.", name);
+        }
+    }
+    
+    private boolean isSessionUnavailable(final FullSession<T> session)
+    {
+        return session == null || doSessionExpiration(session);
+    }
+
+    private boolean doSessionExpiration(final FullSession<T> session)
+    {
+        return session != null && session.hasExpired();
+    }
+
+    private void logAuthenticed(T session)
+    {
+        if (operationLog.isInfoEnabled())
+        {
+            operationLog.info(LOGIN_PREFIX + "User '" + session.getUserName()
+                    + "' has been successfully authenticated from host '" + getRemoteHost()
+                    + "'. Session token: '" + session.getSessionToken() + "'.");
+        }
+        String prefix = prefixGenerator.createPrefix(session);
+        authenticationLog.info(prefix + ": login");
+    }
+
+    private void logFailedAuthentication(String user)
+    {
+        operationLog.warn(LOGIN_PREFIX + "User '" + user + "' failed to authenticate from host '"
+                + getRemoteHost() + "'.");
+        logAuthenticationFailure(user);
+    }
+
+    private void logSessionFailure(String user, RuntimeException ex)
+    {
+        logAuthenticationFailure(user);
+        operationLog.error(LOGIN_PREFIX + "Error when trying to authenticate user '" + user + "'.",
+                ex);
+    }
+    
+    private void logAuthenticationFailure(String user)
+    {
+        String prefix = prefixGenerator.createPrefix(user, getRemoteHost());
+        authenticationLog.info(prefix + ": login   ...FAILED");
+    }
+    
+    private void logSessionExpired(FullSession<T> fullSession)
+    {
+        T session = fullSession.getSession();
+        if (operationLog.isInfoEnabled())
+        {
+            operationLog.info(String.format("%sExpiring session '%s' for user '%s' "
+                    + "after %d minutes of inactivity.", LOGOUT_PREFIX,
+                    session.getSessionToken(), session.getUserName(),
+                    sessionExpirationPeriodMillis / DateUtils.MILLIS_PER_MINUTE));
+        }
+        String prefix = prefixGenerator.createPrefix(session);
+        authenticationLog.info(prefix + ": session_expired  [inactive "
+                + DurationFormatUtils.formatDurationHMS(sessionExpirationPeriodMillis) + "]");
+    }
+
+    private void logLogout(T session)
+    {
+        String prefix = prefixGenerator.createPrefix(session);
+        authenticationLog.info(prefix + ": logout");
+        if (operationLog.isInfoEnabled())
+        {
+            String user = session.getUserName();
+            operationLog.info(LOGOUT_PREFIX + "Session '" + session.getSessionToken()
+                    + "' of user '" + user + "' has been closed from host '" + getRemoteHost()
+                    + "'.");
+        }
+    }
+    
+    public T getSession(String sessionToken) throws UserFailureException
+    {
+        checkIfNotBlank(sessionToken, "sessionToken");
+
+        synchronized (sessions)
+        {
+            final String user = StringUtils.split(sessionToken, '-')[0];
+            final FullSession<T> session = sessions.get(user);
+            if (session == null)
+            {
+                final String msg =
+                        "Session token '" + sessionToken + "' is invalid: user is not logged in.";
+                if (operationLog.isInfoEnabled())
+                {
+                    operationLog.info(msg);
+                }
+                throw new InvalidSessionException(msg);
+            }
+            if (sessionToken.equals(session.getSession().getSessionToken()) == false)
+            {
+                final String msg = "Session token '" + sessionToken + "' is invalid: wrong token.";
+                if (operationLog.isInfoEnabled())
+                {
+                    operationLog.info(msg);
+                }
+                throw new InvalidSessionException(msg);
+            }
+            if (doSessionExpiration(session))
+            {
+                logSessionExpired(session);
+                sessions.remove(user);
+            }
+            if (isSessionUnavailable(session))
+            {
+                throw new InvalidSessionException(
+                        "Session no longer available. Please login again.");
+            }
+            // This is where we know for sure we have a session.
+            session.touch();
+            return session.getSession();
+        }
+    }
+    
+    public String tryToOpenSession(String user, String password)
+    {
+        checkIfNotBlank(user, "user");
+        checkIfNotBlank(password, "password");
+        try
+        {
+            final String applicationToken = authenticationService.authenticateApplication();
+            if (applicationToken == null)
+            {
+                operationLog.error("User '" + user
+                        + "' failed to authenticate: application not authenticated.");
+                return null;
+            }
+            String sessionToken = null;
+            final long now = System.currentTimeMillis();
+            final boolean isAuthenticated =
+                    authenticationService.authenticateUser(applicationToken, user, password);
+            if (isAuthenticated)
+            {
+                try
+                {
+                    final Principal principal =
+                            authenticationService.getPrincipal(applicationToken, user);
+                    T session = createAndStoreSession(user, principal, now);
+                    sessionToken = session.getSessionToken();
+                    logAuthenticed(session);
+                } catch (final IllegalArgumentException ex)
+                {
+                    // getPrincipal() of an authenticated user should not fail, if it does, this
+                    // is an environment failure.
+                    throw new EnvironmentFailureException(ex.getMessage(), ex);
+                }
+            } else
+            {
+                logFailedAuthentication(user);
+            }
+            return sessionToken;
+        } catch (final RuntimeException ex)
+        {
+            logSessionFailure(user, ex);
+            throw ex;
+        }
+    }
+    
+    public void closeSession(String sessionToken)
+    {
+        synchronized (sessions)
+        {
+            final T session = getSession(sessionToken);
+            sessions.remove(session.getUserName());
+            logLogout(session);
+        }
+    }
+
+    public String getRemoteHost()
+    {
+        return remoteHostProvider.getRemoteHost();
+    }
+    
+}
diff --git a/authentication/source/java/ch/systemsx/cisd/authentication/ILogMessagePrefixGenerator.java b/authentication/source/java/ch/systemsx/cisd/authentication/ILogMessagePrefixGenerator.java
new file mode 100644
index 00000000000..1a0c6e12d50
--- /dev/null
+++ b/authentication/source/java/ch/systemsx/cisd/authentication/ILogMessagePrefixGenerator.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2008 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.authentication;
+
+/**
+ * Generator of a prefix for authentication log messages. The prefix contains user information.
+ * Minimum information is user name or ID and remote host (i.e. IP address of the user client
+ * computer).
+ * 
+ * @author Franz-Josef Elmer
+ */
+public interface ILogMessagePrefixGenerator<T extends BasicSession>
+{
+    /**
+     * Creates a prefix based on the specified session.
+     */
+    public String createPrefix(T session);
+    
+    /**
+     * Creates a prefix for specified user and remote host.
+     */
+    public String createPrefix(String user, String remoteHost);
+}
diff --git a/authentication/source/java/ch/systemsx/cisd/authentication/ISessionManager.java b/authentication/source/java/ch/systemsx/cisd/authentication/ISessionManager.java
index b0c096643ed..8dda0ccc636 100644
--- a/authentication/source/java/ch/systemsx/cisd/authentication/ISessionManager.java
+++ b/authentication/source/java/ch/systemsx/cisd/authentication/ISessionManager.java
@@ -37,7 +37,7 @@ public interface ISessionManager<T extends BasicSession> extends IRemoteHostProv
      * @return A session token that is used afterwards to get the <code>Session</code> object, or
      *         <code>null</code>, if the user could not be authenticated.
      */
-    public String openSession(String user, String password);
+    public String tryToOpenSession(String user, String password);
 
     /** Closes session by removing given <code>sessionToken</code> from active sessions. */
     public void closeSession(String sessionToken);
diff --git a/authentication/sourceTest/java/ch/systemsx/cisd/authentication/DefaultSessionManagerTest.java b/authentication/sourceTest/java/ch/systemsx/cisd/authentication/DefaultSessionManagerTest.java
new file mode 100644
index 00000000000..2f25c0e9045
--- /dev/null
+++ b/authentication/sourceTest/java/ch/systemsx/cisd/authentication/DefaultSessionManagerTest.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright 2007 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.authentication;
+
+import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertTrue;
+import static org.testng.AssertJUnit.fail;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.log4j.Level;
+import org.jmock.Expectations;
+import org.jmock.Mockery;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException;
+import ch.systemsx.cisd.common.exceptions.UserFailureException;
+import ch.systemsx.cisd.common.logging.BufferedAppender;
+import ch.systemsx.cisd.common.server.IRemoteHostProvider;
+import ch.systemsx.cisd.common.utilities.OSUtilities;
+
+/**
+ * Test cases for the {@link DefaultSessionManager}.
+ * 
+ * @author Bernd Rinn
+ */
+public class DefaultSessionManagerTest
+{
+
+    private static final String REMOTE_HOST = "remote-host";
+
+    /** Kind of dummy <code>Principal</code> to point out that the login was successful. */
+    private static final Principal principal =
+            new Principal(StringUtils.EMPTY, StringUtils.EMPTY, StringUtils.EMPTY,
+                    StringUtils.EMPTY);
+
+    private static final int SESSION_EXPIRATION_PERIOD_MINUTES = 1;
+
+    private Mockery context;
+
+    private IAuthenticationService authenticationService;
+
+    private ISessionFactory<BasicSession> sessionFactory;
+    
+    private ILogMessagePrefixGenerator<BasicSession> prefixGenerator;
+    
+    private IRemoteHostProvider remoteHostProvider;
+    
+    private ISessionManager<BasicSession> sessionManager;
+
+    private BufferedAppender logRecorder;
+
+    private void assertExceptionMessageForInvalidSessionToken(final UserFailureException ex)
+    {
+        final String message = ex.getMessage();
+        assertTrue(message, message.indexOf("login again") > 0);
+    }
+
+    @SuppressWarnings("unchecked")
+    @BeforeMethod
+    public void setUp()
+    {
+        context = new Mockery();
+        sessionFactory = context.mock(ISessionFactory.class);
+        prefixGenerator = context.mock(ILogMessagePrefixGenerator.class);
+        authenticationService = context.mock(IAuthenticationService.class);
+        remoteHostProvider = context.mock(IRemoteHostProvider.class);
+        context.checking(new Expectations()
+            {
+                {
+                    one(authenticationService).check();
+                }
+            });
+        sessionManager = createSessionManager(SESSION_EXPIRATION_PERIOD_MINUTES);
+        logRecorder = new BufferedAppender("%-5p %c - %m%n", Level.DEBUG);
+    }
+
+    @SuppressWarnings("unchecked")
+    private ISessionManager<BasicSession> createSessionManager(int sessionExpiration)
+    {
+        return new DefaultSessionManager(sessionFactory, prefixGenerator, authenticationService,
+                remoteHostProvider, sessionExpiration);
+    }
+
+    @AfterMethod
+    public void tearDown()
+    {
+        logRecorder.reset();
+        // To following line of code should also be called at the end of each test method.
+        // Otherwise one does not known which test failed.
+        context.assertIsSatisfied();
+    }
+    
+    private void prepareRemoteHostSessionFactoryAndPrefixGenerator(final String user)
+    {
+        context.checking(new Expectations()
+            {
+                {
+                    allowing(remoteHostProvider).getRemoteHost();
+                    will(returnValue(REMOTE_HOST));
+
+                    one(sessionFactory)
+                    .create(with(any(String.class)), with(equal(user)),
+                            with(equal(principal)), with(equal(REMOTE_HOST)),
+                            with(any(Long.class)));
+                    BasicSession session =
+                        new BasicSession(user + "-1", user, principal, REMOTE_HOST, 42L);
+                    will(returnValue(session));
+                    
+                    atLeast(1).of(prefixGenerator).createPrefix(session);
+                    will(returnValue("[USER:'bla', HOST:'remote-host']"));
+                }
+            });
+    }
+
+    @Test
+    public void testSuccessfulAuthentication()
+    {
+        final String applicationToken = "ole";
+        final String user = "bla";
+        prepareRemoteHostSessionFactoryAndPrefixGenerator(user);
+        context.checking(new Expectations()
+            {
+                {
+                    one(authenticationService).authenticateUser(applicationToken, user, "blub");
+                    will(returnValue(true));
+
+                    one(authenticationService).authenticateApplication();
+                    will(returnValue(applicationToken));
+
+                    one(authenticationService).getPrincipal(applicationToken, user);
+                    will(returnValue(principal));
+                }
+            });
+
+        final String token = sessionManager.tryToOpenSession("bla", "blub");
+        assertEquals("bla-1", token);
+        assertEquals(
+                "INFO  OPERATION.DefaultSessionManager - "
+                        + "LOGIN: User 'bla' has been successfully authenticated from host 'remote-host'. Session token: '"
+                        + token + "'." + OSUtilities.LINE_SEPARATOR
+                        + "INFO  AUTH.DefaultSessionManager - [USER:'bla', HOST:'remote-host']: login", logRecorder
+                        .getLogContent());
+
+        context.assertIsSatisfied();
+    }
+
+    @Test
+    public void testFailedAuthentication()
+    {
+        final String applicationToken = "ole";
+        final String user = "bla";
+        context.checking(new Expectations()
+            {
+                {
+                    one(authenticationService).authenticateUser("ole", user, "blub");
+                    will(returnValue(false));
+
+                    one(authenticationService).authenticateApplication();
+                    will(returnValue(applicationToken));
+
+                    allowing(remoteHostProvider).getRemoteHost();
+                    will(returnValue(REMOTE_HOST));
+                    
+                    one(prefixGenerator).createPrefix(user, REMOTE_HOST);
+                    will(returnValue("[USER:'bla', HOST:'remote-host']"));
+                }
+            });
+        assert null == sessionManager.tryToOpenSession(user, "blub");
+        assertEquals("WARN  OPERATION.DefaultSessionManager - "
+                + "LOGIN: User 'bla' failed to authenticate from host 'remote-host'."
+                + OSUtilities.LINE_SEPARATOR + "INFO  AUTH.DefaultSessionManager - [USER:'bla', HOST:'remote-host']: login   ...FAILED",
+                logRecorder.getLogContent());
+
+        context.assertIsSatisfied();
+    }
+
+    @Test
+    public void testAuthenticationForUnavailableAuthenticationService()
+    {
+        final String errorMsg = "I pretend to be not here!";
+        context.checking(new Expectations()
+            {
+                {
+                    one(authenticationService).check();
+                    will(throwException(new EnvironmentFailureException(errorMsg)));
+                }
+            });
+        try
+        {
+            createSessionManager(1);
+            fail("Unavailable authentication service not expected");
+        } catch (final EnvironmentFailureException e)
+        {
+            assertEquals(errorMsg, e.getMessage());
+        }
+        context.assertIsSatisfied();
+    }
+
+    @Test
+    public void testExpirationOfSession()
+    {
+        final String applicationToken = "ole";
+        final String user = "bla";
+        prepareRemoteHostSessionFactoryAndPrefixGenerator(user);
+        context.checking(new Expectations()
+            {
+                {
+                    one(authenticationService).check();
+
+                    one(authenticationService).authenticateUser(applicationToken, user, "blub");
+                    will(returnValue(true));
+
+                    one(authenticationService).getPrincipal(applicationToken, user);
+                    will(returnValue(principal));
+
+                    one(authenticationService).authenticateApplication();
+                    will(returnValue(applicationToken));
+                }
+            });
+
+        
+        sessionManager = createSessionManager(0);
+        final String sessionToken = sessionManager.tryToOpenSession("bla", "blub");
+        assert sessionToken.length() > 0;
+        try
+        {
+            Thread.sleep(100);
+        } catch (final InterruptedException ex)
+        {
+            // ignored
+        }
+        logRecorder.resetLogContent();
+        try
+        {
+            sessionManager.getSession(sessionToken);
+            fail("UserFailureException expected because session has expired.");
+        } catch (final UserFailureException e)
+        {
+            assertExceptionMessageForInvalidSessionToken(e);
+        }
+        assertEquals("INFO  OPERATION.DefaultSessionManager - " + "LOGOUT: Expiring session '"
+                + sessionToken + "' for user 'bla' after 0 minutes of inactivity."
+                + OSUtilities.LINE_SEPARATOR + "INFO  AUTH.DefaultSessionManager - [USER:'bla', HOST:'remote-host']: session_expired  [inactive 0:00:00.000]", logRecorder
+                .getLogContent());
+
+        context.assertIsSatisfied();
+    }
+
+    @Test
+    public void testSessionRemoval()
+    {
+        final String applicationToken = "ole";
+        final String user = "bla";
+        final String password = "blub";
+        prepareRemoteHostSessionFactoryAndPrefixGenerator(user);
+        context.checking(new Expectations()
+            {
+                {
+                    one(authenticationService).authenticateUser(applicationToken, user, password);
+                    will(returnValue(true));
+
+                    one(authenticationService).authenticateApplication();
+                    will(returnValue(applicationToken));
+
+                    one(authenticationService).getPrincipal(applicationToken, user);
+                    will(returnValue(principal));
+                }
+            });
+
+        final String sessionToken = sessionManager.tryToOpenSession(user, password);
+
+        sessionManager.closeSession(sessionToken);
+        try
+        {
+            sessionManager.getSession(user);
+            fail("UserFailureException expected because session token no longer valid.");
+        } catch (final UserFailureException ex)
+        {
+            final String message = ex.getMessage();
+            assertTrue(message, message.indexOf("user is not logged in") > 0);
+        }
+
+        context.assertIsSatisfied();
+    }
+
+}
-- 
GitLab