diff --git a/authentication/source/java/ch/systemsx/cisd/authentication/file/CachingAuthenticationService.java b/authentication/source/java/ch/systemsx/cisd/authentication/file/CachingAuthenticationService.java
new file mode 100644
index 0000000000000000000000000000000000000000..fea56bd82e64eee36818e770988f3dacc665e22e
--- /dev/null
+++ b/authentication/source/java/ch/systemsx/cisd/authentication/file/CachingAuthenticationService.java
@@ -0,0 +1,578 @@
+/*
+ * Copyright 2013 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.file;
+
+import java.io.File;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.log4j.Logger;
+
+import ch.systemsx.cisd.authentication.IAuthenticationService;
+import ch.systemsx.cisd.authentication.Principal;
+import ch.systemsx.cisd.authentication.file.LineBasedUserStore.IUserEntryFactory;
+import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException;
+import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException;
+import ch.systemsx.cisd.common.logging.LogCategory;
+import ch.systemsx.cisd.common.logging.LogFactory;
+import ch.systemsx.cisd.common.utilities.ITimeProvider;
+import ch.systemsx.cisd.common.utilities.SystemTimeProvider;
+
+/**
+ * An {@link IAuthenticationService} that delegates to another {@link IAuthenticationService} and
+ * keeps the returned value for user authentication in a local cache which is written out to a
+ * password cache file which will be used to populate the cache on restart, so the password cache
+ * survives restarts. Changing the password cache file will change the cache without restart, e.g.
+ * deleting a line from it will remove the cache entry with immediate effect.
+ * <p>
+ * In order to make caching as smooth as possible for regular users of the system, a authentication
+ * request which is served from the cache under some conditions triggers a re-validation request
+ * with the delegate authentication service. As re-validation is done asynchronously, it does not
+ * block the user from working.
+ * <p>
+ * Two configurable time periods (in milli-seconds) are relevant:
+ * <ul>
+ * <li><code>cacheTimeMillis</code> is the time period after caching that a cache entry is kept.
+ * Older cache entries are treated as invalid and ignored. The default is 28 hours, which means that
+ * any user who logs into the system once a day will never have to wait for the delegate
+ * authentication system to respond as all cache updates are performed in asynchronous
+ * re-validations.</li>
+ * <li><code>cacheTimeNoRevalidationMillis</code> is the time period after caching in which
+ * successful authentication requests do not trigger a re-validation request. The default is 1 hour.
+ * This feature is meant to reduce the load on the delegate authentication system. Set it to
+ * <code>cacheTimeMillis</code> to never re-validate or set to 0 to always re-validate.</li>
+ * </ul>
+ * 
+ * @author Bernd Rinn
+ */
+public class CachingAuthenticationService implements IAuthenticationService
+{
+    public final static long ONE_MINUTE = 60 * 1000L;
+
+    public final static long ONE_HOUR = 60 * ONE_MINUTE;
+
+    private final static long CACHE_TIME_MILLIS_NO_REVALIDATION = ONE_HOUR;
+
+    private final static long CACHE_TIME_MILLIS = 28 * ONE_HOUR;
+
+    private static final Logger operationLog =
+            LogFactory.getLogger(LogCategory.OPERATION, CachingAuthenticationService.class);
+
+    private enum CacheEntryStatus
+    {
+        /** Entry OK without validation. */
+        OK,
+        /** Entry OK but should request re-validation. */
+        OK_REVALIDATE,
+        /** No entry or entry expired. */
+        NO_ENTRY,
+    }
+
+    /** A request for validation of a user. */
+    final class ValidationRequest
+    {
+        private final UserCacheEntry user;
+
+        private final long queuedAt;
+
+        private final String passwordOrNull;
+
+        private final boolean authenticated;
+
+        ValidationRequest(UserCacheEntry user, String passwordOrNull, boolean authenticated,
+                long now)
+        {
+            this.user = user;
+            this.passwordOrNull = passwordOrNull;
+            this.authenticated = authenticated;
+            this.queuedAt = now;
+        }
+
+        /**
+         * Returns the user entry to re-validate.
+         */
+        UserCacheEntry getUser()
+        {
+            return user;
+        }
+
+        /**
+         * Returns the password the user supplied, or <code>null</code>, if this is a
+         * non-authenticating
+         * validation request.
+         */
+        String tryGetPassword()
+        {
+            return passwordOrNull;
+        }
+
+        /**
+         * Returns <code>true</code> if this request does not include authentication.
+         */
+        boolean withoutAuthentication()
+        {
+            return (passwordOrNull == null);
+        }
+
+        /**
+         * Returns <code>true</code>, if the user has been successfully authenticated from the
+         * cached entry with the given password.
+         */
+        boolean isAuthenticated()
+        {
+            return authenticated;
+        }
+
+        /**
+         * Returns <code>true</code> if this is a valid request at the time when this method is
+         * called.
+         */
+        boolean isValid()
+        {
+            final UserCacheEntry current = userStore.tryGetUserById(user.getUserId());
+            // OK, if there is no cache entry.
+            if (current == null)
+            {
+                return true;
+            }
+            // Not OK, if the request has no password but the cache entry has one.
+            if (passwordOrNull == null && current.hasPassword())
+            {
+                return false;
+            }
+            // OK, if the request has a password but the cache entry has none.
+            if (passwordOrNull != null && current.hasPassword() == false)
+            {
+                return true;
+            }
+            // OK, if the time of request queuing is after the time of caching.
+            return (current.getCachedAt() < queuedAt);
+        }
+    }
+
+    /**
+     * The class that performs re-validation of users and their passwords.
+     * 
+     * @author Bernd Rinn
+     */
+    final class RevalidationRunnable implements Runnable
+    {
+        void runOnce() throws InterruptedException
+        {
+            final ValidationRequest request = validationQueue.take();
+            if (request.isValid() == false)
+            {
+                return;
+            }
+            final String userId = request.getUser().getUserId();
+            final Principal p =
+                    delegate.tryGetAndAuthenticateUser(userId,
+                            request.tryGetPassword());
+            // If a user got remove from the delegate system, remove the cache entry.
+            if (p == null)
+            {
+                userStore.removeUser(userId);
+                if (request.isAuthenticated())
+                {
+                    operationLog
+                            .warn(String.format("User '%s' has been logged in which is no "
+                                    + "longer a valid user.", userId));
+                }
+                return;
+            }
+            // Only update the cache if no authentication was requested or if the
+            // authentication was successful.
+            if (p.isAuthenticated() || request.withoutAuthentication())
+            {
+                userStore.addOrUpdateUser(new UserCacheEntry(p, request
+                        .tryGetPassword(), timeProvider.getTimeInMilliseconds()));
+            }
+            if (request.isAuthenticated() && p.isAuthenticated() == false)
+            {
+                operationLog.warn(String.format(
+                        "User '%s' has been logged in with an outdated password.",
+                        userId));
+            }
+        }
+        
+        @Override
+        public void run()
+        {
+            while (true)
+            {
+                try
+                {
+                    runOnce();
+                } catch (Throwable th)
+                {
+                    operationLog.error(
+                            "Exception in " + CachingAuthenticationService.class.getSimpleName()
+                                    + " Revalidator: ", th);
+                }
+            }
+        }
+    }
+
+    /** A simple interface to authenticate by one type of id. */
+    interface IAuthenticator
+    {
+        Principal tryGetAndAuthenticate(String id, String passwordOrNull);
+    }
+
+    private final IUserStore<UserCacheEntry> userStore;
+
+    private final IAuthenticationService delegate;
+
+    private final IAuthenticator userIdAuthenticator;
+
+    private final IAuthenticator emailAuthenticator;
+
+    private final long cacheTimeNoRevalidationMillis;
+
+    private final long cacheTimeMillis;
+
+    private final BlockingQueue<ValidationRequest> validationQueue;
+
+    private final ITimeProvider timeProvider;
+
+    CachingAuthenticationService(IAuthenticationService delegate,
+            String passwordCacheFileName)
+    {
+        this(delegate, createUserStore(passwordCacheFileName));
+    }
+
+    CachingAuthenticationService(IAuthenticationService authenticationService,
+            IUserStore<UserCacheEntry> store)
+    {
+        this(authenticationService, store, CACHE_TIME_MILLIS_NO_REVALIDATION,
+                CACHE_TIME_MILLIS);
+    }
+
+    CachingAuthenticationService(IAuthenticationService delegate,
+            String passwordCacheFileName,
+            long cacheTimeNoRevalidationMillis,
+            long cacheTimeMillis)
+    {
+        this(delegate, createUserStore(passwordCacheFileName),
+                cacheTimeNoRevalidationMillis, cacheTimeMillis);
+    }
+
+    CachingAuthenticationService(IAuthenticationService delegate,
+            IUserStore<UserCacheEntry> userStore,
+            long cacheTimeNoRevalidationMillis,
+            long cacheTimeMillis)
+    {
+        this(delegate, userStore, cacheTimeNoRevalidationMillis, cacheTimeMillis, true,
+                SystemTimeProvider.SYSTEM_TIME_PROVIDER);
+    }
+
+    // For unit tests.
+    CachingAuthenticationService(IAuthenticationService delegate,
+            IUserStore<UserCacheEntry> userStore,
+            long cacheTimeNoRevalidationMillis,
+            long cacheTimeMillis, boolean startRevalidationThread, ITimeProvider timeProvider)
+    {
+        this.delegate = delegate;
+        this.userIdAuthenticator = new IAuthenticator()
+            {
+                @Override
+                public Principal tryGetAndAuthenticate(String userId, String passwordOrNull)
+                {
+                    return CachingAuthenticationService.this.delegate.tryGetAndAuthenticateUser(
+                            userId,
+                            passwordOrNull);
+                }
+            };
+        this.emailAuthenticator = new IAuthenticator()
+            {
+                @Override
+                public Principal tryGetAndAuthenticate(String email, String passwordOrNull)
+                {
+                    return CachingAuthenticationService.this.delegate
+                            .tryGetAndAuthenticateUserByEmail(email, passwordOrNull);
+                }
+            };
+        this.userStore = userStore;
+        this.cacheTimeNoRevalidationMillis =
+                Math.min(cacheTimeNoRevalidationMillis, cacheTimeMillis);
+        this.cacheTimeMillis = cacheTimeMillis;
+        this.validationQueue = new LinkedBlockingQueue<ValidationRequest>();
+        this.timeProvider = timeProvider;
+        if (startRevalidationThread)
+        {
+            final Thread t = new Thread(new RevalidationRunnable());
+            t.setName(getClass().getSimpleName() + " - Validator");
+            t.setDaemon(true);
+            t.start();
+        }
+    }
+
+    static IUserStore<UserCacheEntry> createUserStore(
+            final String passwordCacheFileName)
+    {
+        final ILineStore lineStore =
+                new FileBasedLineStore(new File(passwordCacheFileName), "Password cache file");
+        return new LineBasedUserStore<UserCacheEntry>(lineStore,
+                new IUserEntryFactory<UserCacheEntry>()
+                    {
+                        @Override
+                        public UserCacheEntry create(String line)
+                        {
+                            return new UserCacheEntry(line);
+                        }
+                    });
+    }
+
+    // For unit tests.
+    BlockingQueue<ValidationRequest> getValidationQueue()
+    {
+        return validationQueue;
+    }
+
+    @Override
+    public boolean authenticateUser(String userId, String password)
+    {
+        return Principal.isAuthenticated(tryGetAndAuthenticateUser(userId, password,
+                userIdAuthenticator, userStore.tryGetUserById(userId)));
+    }
+
+    private Principal tryGetAndAuthenticateUser(String id,
+            String passwordOrNull, IAuthenticator auth, UserCacheEntry entry)
+    {
+        // Note: getStatus() returns NO_ENTRY on entry == null
+        final boolean requiresAuthentication = StringUtils.isNotEmpty(passwordOrNull);
+        final long now = timeProvider.getTimeInMilliseconds();
+        final CacheEntryStatus state =
+                getStatus(entry, requiresAuthentication, now);
+        switch (state)
+        {
+            case OK:
+            {
+                final boolean authenticated = entry.isPasswordCorrect(passwordOrNull);
+                if (authenticated == false && requiresAuthentication)
+                {
+                    validationQueue.offer(new ValidationRequest(entry, passwordOrNull, false, now));
+                }
+                return toPrincipal(entry, authenticated);
+            }
+            case OK_REVALIDATE:
+            {
+                final boolean authenticated = entry.isPasswordCorrect(passwordOrNull);
+                validationQueue.offer(new ValidationRequest(entry, passwordOrNull, authenticated,
+                        now));
+                return toPrincipal(entry, authenticated);
+            }
+            case NO_ENTRY:
+            {
+                final Principal p = auth.tryGetAndAuthenticate(id, passwordOrNull);
+                if (p == null)
+                {
+                    return null;
+                }
+                final UserCacheEntry user =
+                        new UserCacheEntry(p, passwordOrNull, timeProvider.getTimeInMilliseconds());
+                userStore.addOrUpdateUser(user);
+                return p;
+            }
+            default:
+                throw new Error("Unknown cache entry state " + state);
+        }
+    }
+
+    private static Principal toPrincipal(UserEntry userOrNull, boolean authenticated)
+    {
+        if (userOrNull == null)
+        {
+            return null;
+        }
+        final Principal principal = userOrNull.asPrincipal();
+        principal.setAuthenticated(authenticated);
+        return principal;
+    }
+
+    private CacheEntryStatus getStatus(UserCacheEntry entry, boolean requirePassword, long now)
+    {
+        if (entry == null)
+        {
+            return CacheEntryStatus.NO_ENTRY;
+        }
+        if (requirePassword && entry.hasPassword() == false)
+        {
+            return CacheEntryStatus.NO_ENTRY;
+        }
+        final long cachedAt = entry.getCachedAt();
+        if (cachedAt + cacheTimeNoRevalidationMillis >= now)
+        {
+            return CacheEntryStatus.OK;
+        } else if (cachedAt + cacheTimeMillis >= now)
+        {
+            return CacheEntryStatus.OK_REVALIDATE;
+        } else
+        {
+            // Treat an expired entry like a missing entry.
+            return CacheEntryStatus.NO_ENTRY;
+        }
+    }
+
+    @Override
+    @Deprecated
+    public boolean authenticateUser(String dummyToken, String userId, String password)
+    {
+        return authenticateUser(userId, password);
+    }
+
+    @Override
+    public Principal tryGetAndAuthenticateUser(String userId, String passwordOrNull)
+    {
+        return tryGetAndAuthenticateUser(userId, passwordOrNull, userIdAuthenticator,
+                userStore.tryGetUserById(userId));
+    }
+
+    @Override
+    @Deprecated
+    public Principal tryGetAndAuthenticateUser(String dummyToken, String userId,
+            String passwordOrNull)
+    {
+        return tryGetAndAuthenticateUser(userId, passwordOrNull);
+    }
+
+    @Override
+    public Principal getPrincipal(String userId) throws IllegalArgumentException
+    {
+        final Principal principalOrNull =
+                tryGetAndAuthenticateUser(userId, null, userIdAuthenticator,
+                        userStore.tryGetUserById(userId));
+        if (principalOrNull == null)
+        {
+            throw new IllegalArgumentException("Cannot find user '" + userId + "'.");
+        }
+        return principalOrNull;
+    }
+
+    @Override
+    @Deprecated
+    public Principal getPrincipal(String dummyToken, String userId) throws IllegalArgumentException
+    {
+        return getPrincipal(userId);
+    }
+
+    @Override
+    public Principal tryGetAndAuthenticateUserByEmail(String email, String passwordOrNull)
+    {
+        return tryGetAndAuthenticateUser(email, passwordOrNull, emailAuthenticator,
+                userStore.tryGetUserByEmail(email));
+    }
+
+    @Override
+    @Deprecated
+    public Principal tryGetAndAuthenticateUserByEmail(String dummyToken, String email,
+            String passwordOrNull)
+    {
+        return tryGetAndAuthenticateUserByEmail(email, passwordOrNull);
+    }
+
+    @Override
+    public boolean supportsAuthenticatingByEmail()
+    {
+        return delegate.supportsAuthenticatingByEmail();
+    }
+
+    @Override
+    public boolean supportsListingByUserId()
+    {
+        return delegate.supportsListingByUserId();
+    }
+
+    @Override
+    public List<Principal> listPrincipalsByUserId(String userIdQuery)
+            throws IllegalArgumentException
+    {
+        return delegate.listPrincipalsByUserId(userIdQuery);
+    }
+
+    @Override
+    public boolean supportsListingByEmail()
+    {
+        return delegate.supportsListingByEmail();
+    }
+
+    @Override
+    public List<Principal> listPrincipalsByEmail(String emailQuery) throws IllegalArgumentException
+    {
+        return delegate.listPrincipalsByEmail(emailQuery);
+    }
+
+    @Override
+    public boolean supportsListingByLastName()
+    {
+        return delegate.supportsListingByLastName();
+    }
+
+    @Override
+    public List<Principal> listPrincipalsByLastName(String lastNameQuery)
+            throws IllegalArgumentException
+    {
+        return delegate.listPrincipalsByLastName(lastNameQuery);
+    }
+
+    @Override
+    @Deprecated
+    public String authenticateApplication()
+    {
+        return delegate.authenticateApplication();
+    }
+
+    @Override
+    @Deprecated
+    public List<Principal> listPrincipalsByUserId(String dummyToken, String userIdQuery)
+            throws IllegalArgumentException
+    {
+        return delegate.listPrincipalsByUserId(dummyToken, userIdQuery);
+    }
+
+    @Override
+    @Deprecated
+    public List<Principal> listPrincipalsByEmail(String dummyToken, String emailQuery)
+            throws IllegalArgumentException
+    {
+        return delegate.listPrincipalsByEmail(dummyToken, emailQuery);
+    }
+
+    @Override
+    @Deprecated
+    public List<Principal> listPrincipalsByLastName(String dummyToken, String lastNameQuery)
+            throws IllegalArgumentException
+    {
+        return delegate.listPrincipalsByLastName(dummyToken, lastNameQuery);
+    }
+
+    @Override
+    public boolean isRemote()
+    {
+        return delegate.isRemote();
+    }
+
+    @Override
+    public void check() throws EnvironmentFailureException, ConfigurationFailureException
+    {
+        userStore.check();
+        delegate.check();
+    }
+
+}
diff --git a/authentication/source/java/ch/systemsx/cisd/authentication/file/UserCacheEntry.java b/authentication/source/java/ch/systemsx/cisd/authentication/file/UserCacheEntry.java
new file mode 100644
index 0000000000000000000000000000000000000000..1de21351df6c8c7de7858717247c77b7391e05a2
--- /dev/null
+++ b/authentication/source/java/ch/systemsx/cisd/authentication/file/UserCacheEntry.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2013 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.file;
+
+import org.apache.commons.lang.StringUtils;
+
+import ch.systemsx.cisd.authentication.Principal;
+import ch.systemsx.cisd.common.security.PasswordHasher;
+
+/**
+ * A class representing a user entry in the password cache file.
+ * 
+ * @author Bernd Rinn
+ */
+public class UserCacheEntry extends UserEntry
+{
+    // For unit tests.
+    static final int NUMBER_OF_COLUMNS_IN_PASSWORD_CACHE_FILE = 6;
+
+    private static final int CACHED_AT_IDX = 5;
+
+    private final long cachedAt;
+
+    UserCacheEntry(String passwordFileLine) throws IllegalArgumentException
+    {
+        super(split(passwordFileLine, NUMBER_OF_COLUMNS_IN_PASSWORD_CACHE_FILE));
+        this.cachedAt = Long.parseLong(getElement(CACHED_AT_IDX));
+    }
+
+    UserCacheEntry(Principal p, long cachedAt)
+    {
+        this(p, null, cachedAt);
+    }
+
+    UserCacheEntry(Principal p, String passwordOrNull, long cachedAt)
+    {
+        super(createPasswordFileEntry(p, passwordOrNull, cachedAt));
+        this.cachedAt = cachedAt;
+    }
+
+    private static String[] createPasswordFileEntry(Principal p, String passwordOrNull,
+            long cachedAt)
+    {
+        assert p != null;
+
+        return new String[]
+            {
+                    p.getUserId(),
+                    p.getEmail(),
+                    p.getFirstName(),
+                    p.getLastName(),
+                    (p.isAuthenticated() && StringUtils.isNotEmpty(passwordOrNull)) ? PasswordHasher
+                            .computeSaltedHash(passwordOrNull)
+                            : "",
+                    Long.toString(cachedAt),
+        };
+}
+
+    /**
+     * Returns the time stamp when this cache entry was put into the cache.
+     */
+    long getCachedAt()
+    {
+        return cachedAt;
+    }
+
+    /**
+     * @throw {@link UnsupportedOperationException}
+     */
+    @Override
+    void setEmail(String email) throws UnsupportedOperationException
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * @throw {@link UnsupportedOperationException}
+     */
+    @Override
+    void setFirstName(String firstName) throws UnsupportedOperationException
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * @throw {@link UnsupportedOperationException}
+     */
+    @Override
+    void setLastName(String lastName) throws UnsupportedOperationException
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * @throw {@link UnsupportedOperationException}
+     */
+    @Override
+    void setPassword(String plainPassword) throws UnsupportedOperationException
+    {
+        throw new UnsupportedOperationException();
+    }
+
+}
diff --git a/authentication/sourceTest/java/ch/systemsx/cisd/authentication/file/CachingAuthenticationServiceInvalidLoginTests.java b/authentication/sourceTest/java/ch/systemsx/cisd/authentication/file/CachingAuthenticationServiceInvalidLoginTests.java
new file mode 100644
index 0000000000000000000000000000000000000000..2cebf27827390c83f70869343df0b2764a39721d
--- /dev/null
+++ b/authentication/sourceTest/java/ch/systemsx/cisd/authentication/file/CachingAuthenticationServiceInvalidLoginTests.java
@@ -0,0 +1,292 @@
+/*
+ * Copyright 2013 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.file;
+
+import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertFalse;
+import static org.testng.AssertJUnit.assertNotNull;
+import static org.testng.AssertJUnit.assertNull;
+import static org.testng.AssertJUnit.assertTrue;
+
+import java.io.File;
+import java.util.List;
+import java.util.regex.Pattern;
+
+import org.apache.commons.lang.StringUtils;
+import org.jmock.Expectations;
+import org.jmock.Mockery;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import ch.systemsx.cisd.authentication.IAuthenticationService;
+import ch.systemsx.cisd.authentication.Principal;
+import ch.systemsx.cisd.common.filesystem.FileUtilities;
+import ch.systemsx.cisd.common.utilities.ITimeProvider;
+
+/**
+ * Test cases for the {@link CachingAuthenticationService} with invalid login attempts.
+ * 
+ * @author Bernd Rinn
+ */
+public class CachingAuthenticationServiceInvalidLoginTests
+{
+    private static final File workingDirectory =
+            new File("targets/unit-test-wd/CachingAuthenticationServiceInvalidLoginTests");
+
+    private static final File PASSWD_FILE = new File(workingDirectory, "passwd");
+
+    private static final String PASSWD_FILENAME = PASSWD_FILE.getPath();
+
+    private static final long CACHE_TIME_NO_REVAL_MILLIS = 1000L;
+
+    private static final long CACHE_TIME_MILLIS = 2000L;
+
+    private static final String user = "User";
+
+    private static final String firstName = "First Name";
+
+    private static final String lastName = "Last Name";
+
+    private static final String email = "e@mail";
+
+    private static final String invalidPassword = "passw0rd";
+
+    private static final String validPassword = "s@crit";
+
+    private static final long timeStart = 450L;
+
+    private static final long timeCache = 500L;
+
+    private static final long time2 = 550L;
+
+    private static final long time2Cache = 600L;
+    
+    private static final long time3 = 650L;
+
+    private static final long time4 = 700L;
+
+    private static final long time5 = 800L;
+
+    private Mockery context;
+
+    private IAuthenticationService delegateService;
+
+    private ITimeProvider timeProvider;
+
+    private CachingAuthenticationService service;
+
+    private CachingAuthenticationService.RevalidationRunnable revalidator;
+
+    @BeforeClass
+    public void setUp()
+    {
+        FileUtilities.deleteRecursively(workingDirectory);
+        workingDirectory.mkdirs();
+        context = new Mockery();
+        delegateService = context.mock(IAuthenticationService.class);
+        timeProvider = context.mock(ITimeProvider.class);
+        service =
+                new CachingAuthenticationService(delegateService,
+                        CachingAuthenticationService.createUserStore(PASSWD_FILENAME),
+                        CACHE_TIME_NO_REVAL_MILLIS, CACHE_TIME_MILLIS, false, timeProvider);
+        revalidator = service.new RevalidationRunnable();
+    }
+
+    @AfterMethod
+    public void tearDown()
+    {
+        context.assertIsSatisfied();
+    }
+
+    @AfterClass
+    public void cleanUp()
+    {
+        FileUtilities.deleteRecursively(workingDirectory);
+    }
+
+    @Test
+    public void testAuthenticateUserFirstTimeUserDoesntExist()
+    {
+        PASSWD_FILE.delete();
+        context.checking(new Expectations()
+            {
+                {
+                    one(timeProvider).getTimeInMilliseconds();
+                    will(returnValue(timeStart));
+                    one(delegateService).tryGetAndAuthenticateUser(user, invalidPassword);
+                    will(returnValue(null));
+                }
+            });
+        final Principal p = service.tryGetAndAuthenticateUser(user, invalidPassword);
+        assertNull(p);
+        checkCacheEmpty();
+        assertTrue(service.getValidationQueue().isEmpty());
+        context.assertIsSatisfied();
+    }
+
+    // Note: subsequent test methods depend on testAuthenticateUserFirstTimeUserDoesntExist() not
+    // being called after this method, so we force a dependency.
+    @Test(dependsOnMethods = "testAuthenticateUserFirstTimeUserDoesntExist")
+    public void testAuthenticateUserFirstTimeInvalidPassword()
+    {
+        PASSWD_FILE.delete();
+        context.checking(new Expectations()
+            {
+                {
+                    one(timeProvider).getTimeInMilliseconds();
+                    will(returnValue(timeStart));
+                    one(delegateService).tryGetAndAuthenticateUser(user, invalidPassword);
+                    will(returnValue(new Principal(user, firstName, lastName, email, false)));
+                    one(timeProvider).getTimeInMilliseconds();
+                    will(returnValue(timeCache));
+                }
+            });
+        final Principal p = service.tryGetAndAuthenticateUser(user, invalidPassword);
+        assertNotNull(p);
+        checkPrincipalUser(user, firstName, lastName, email, false, p);
+        checkCache(user, firstName, lastName, email, timeCache, false);
+        assertTrue(service.getValidationQueue().isEmpty());
+        context.assertIsSatisfied();
+    }
+
+    @Test(dependsOnMethods = "testAuthenticateUserFirstTimeInvalidPassword")
+    public void testGetPrincipalAfterCacheWithoutPassword()
+    {
+        context.checking(new Expectations()
+            {
+                {
+                    one(timeProvider).getTimeInMilliseconds();
+                    will(returnValue(time2));
+                }
+            });
+        final Principal p = service.tryGetAndAuthenticateUser(user, null);
+        assertNotNull(p);
+        checkPrincipalUser(user, firstName, lastName, email, false, p);
+        checkCache(user, firstName, lastName, email, timeCache, false);
+        assertTrue(service.getValidationQueue().isEmpty());
+        context.assertIsSatisfied();
+    }
+
+    @Test(dependsOnMethods = "testGetPrincipalAfterCacheWithoutPassword")
+    public void testAuthenticateUserSecondTimeValidPassword()
+    {
+        context.checking(new Expectations()
+            {
+                {
+                    one(timeProvider).getTimeInMilliseconds();
+                    will(returnValue(time3));
+                    // Cannot use the cached entry because it doesn't have a valid password hash.
+                    one(delegateService).tryGetAndAuthenticateUser(user, validPassword);
+                    will(returnValue(new Principal(user, firstName, lastName, email, true)));
+                    one(timeProvider).getTimeInMilliseconds();
+                    will(returnValue(time2Cache));
+                }
+            });
+        final Principal p = service.tryGetAndAuthenticateUser(user, validPassword);
+        assertNotNull(p);
+        checkPrincipalUser(user, firstName, lastName, email, true, p);
+        checkCache(user, firstName, lastName, email, time2Cache, true);
+        assertTrue(service.getValidationQueue().isEmpty());
+        context.assertIsSatisfied();
+    }
+
+    @Test(dependsOnMethods = "testGetPrincipalAfterCacheWithoutPassword")
+    public void testAuthenticateUserThirdTimeInvalidPassword() throws InterruptedException
+    {
+        context.checking(new Expectations()
+            {
+                {
+                    one(timeProvider).getTimeInMilliseconds();
+                    will(returnValue(time4));
+                }
+            });
+        final Principal p = service.tryGetAndAuthenticateUser(user, invalidPassword);
+        assertNotNull(p);
+        checkPrincipalUser(user, firstName, lastName, email, false, p);
+        checkCache(user, firstName, lastName, email, time2Cache, true);
+        assertFalse(service.getValidationQueue().isEmpty());
+        
+        // Run validator with request, but the password is invalid so the result will not be cached.
+        context.checking(new Expectations()
+        {
+            {
+                one(delegateService).tryGetAndAuthenticateUser(user, invalidPassword);
+                will(returnValue(new Principal(user, firstName, lastName, email, false)));
+            }
+        });
+        revalidator.runOnce();
+        checkCache(user, firstName, lastName, email, time2Cache, true);
+        
+        // A valid call is still served from the cache.
+        context.checking(new Expectations()
+        {
+            {
+                one(timeProvider).getTimeInMilliseconds();
+                will(returnValue(time5));
+            }
+        });
+        final Principal p2 = service.tryGetAndAuthenticateUser(user, validPassword);
+        assertNotNull(p2);
+        checkPrincipalUser(user, firstName, lastName, email, true, p2);
+        checkCache(user, firstName, lastName, email, time2Cache, true);
+        
+        context.assertIsSatisfied();
+    }
+
+    @SuppressWarnings("hiding")
+    private void checkPrincipalUser(final String user, final String firstName,
+            final String lastName, final String email, boolean authenticated, Principal p)
+    {
+        assertNotNull(p);
+        assertEquals(user, p.getUserId());
+        assertEquals(firstName, p.getFirstName());
+        assertEquals(lastName, p.getLastName());
+        assertEquals(email, p.getEmail());
+        assertEquals(authenticated, p.isAuthenticated());
+    }
+
+    private void checkCacheEmpty()
+    {
+        assertTrue(PASSWD_FILE.length() == 0);
+    }
+
+    @SuppressWarnings("hiding")
+    private void checkCache(final String user, final String firstName,
+            final String lastName, final String email, final long timeCache, final boolean withHash)
+    {
+        assertTrue(PASSWD_FILE.exists());
+        assertTrue(PASSWD_FILE.length() > 0);
+        final List<String> passwdLines = FileUtilities.loadToStringList(PASSWD_FILE, null);
+        assertEquals(1, passwdLines.size());
+        final String[] split = StringUtils.splitPreserveAllTokens(passwdLines.get(0), ':');
+        assertEquals(UserCacheEntry.NUMBER_OF_COLUMNS_IN_PASSWORD_CACHE_FILE, split.length);
+        assertEquals(user, split[0]);
+        assertEquals(email, split[1]);
+        assertEquals(firstName, split[2]);
+        assertEquals(lastName, split[3]);
+        if (withHash)
+        {
+            assertTrue(split[4], Pattern.matches("[\\w\\/,\\+]+", split[4]));
+        } else
+        {
+            assertEquals("", split[4]);
+        }
+        assertEquals(Long.toString(timeCache), split[5]);
+    }
+}
diff --git a/authentication/sourceTest/java/ch/systemsx/cisd/authentication/file/CachingAuthenticationServiceSuccessTests.java b/authentication/sourceTest/java/ch/systemsx/cisd/authentication/file/CachingAuthenticationServiceSuccessTests.java
new file mode 100644
index 0000000000000000000000000000000000000000..1cbc9842f92c358335945a2da6365a01b4291636
--- /dev/null
+++ b/authentication/sourceTest/java/ch/systemsx/cisd/authentication/file/CachingAuthenticationServiceSuccessTests.java
@@ -0,0 +1,299 @@
+/*
+ * Copyright 2013 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.file;
+
+import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertFalse;
+import static org.testng.AssertJUnit.assertNotNull;
+import static org.testng.AssertJUnit.assertNull;
+import static org.testng.AssertJUnit.assertTrue;
+
+import java.io.File;
+import java.util.List;
+import java.util.regex.Pattern;
+
+import org.apache.commons.lang.StringUtils;
+import org.jmock.Expectations;
+import org.jmock.Mockery;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import ch.systemsx.cisd.authentication.IAuthenticationService;
+import ch.systemsx.cisd.authentication.Principal;
+import ch.systemsx.cisd.common.filesystem.FileUtilities;
+import ch.systemsx.cisd.common.utilities.ITimeProvider;
+
+/**
+ * Test cases for the {@link CachingAuthenticationService} with successful logins.
+ * 
+ * @author Bernd Rinn
+ */
+public class CachingAuthenticationServiceSuccessTests
+{
+    private static final File workingDirectory =
+            new File("targets/unit-test-wd/CachingAuthenticationServiceSuccessTests");
+
+    private static final File PASSWD_FILE = new File(workingDirectory, "passwd");
+
+    private static final String PASSWD_FILENAME = PASSWD_FILE.getPath();
+
+    private static final long CACHE_TIME_NO_REVAL_MILLIS = 1000L;
+
+    private static final long CACHE_TIME_MILLIS = 2000L;
+
+    private static final String user = "User";
+
+    private static final String firstName = "First Name";
+
+    private static final String lastName = "Last Name";
+
+    private static final String email = "e@mail";
+
+    private static final String email2 = "e@mail2";
+
+    private static final String email3 = "e@mail3";
+
+    private static final String password = "passw0rd";
+
+    private static final long timeStart = 450L;
+
+    private static final long timeCache = 500L;
+
+    private static final long timeSecondRequest = 800L;
+
+    private static final long timeThirdRequest = 1200L;
+
+    private static final long timeForthRequest = 1750L; // Triggers re-validation
+
+    private static final long time2Cache = 1800L; // In re-validation
+
+    private static final long timeFifthRequest = 1900L; // That's after re-validation from the
+                                                        // cache.
+
+    private static final long timeSixthRequest = 9950L; // Triggers expiration
+
+    private static final long time3Cache = 10000L; // New cache after expiration
+
+    private Mockery context;
+
+    private IAuthenticationService delegateService;
+
+    private ITimeProvider timeProvider;
+
+    private CachingAuthenticationService service;
+
+    private CachingAuthenticationService.RevalidationRunnable revalidator;
+
+    @BeforeClass
+    public void setUp()
+    {
+        FileUtilities.deleteRecursively(workingDirectory);
+        workingDirectory.mkdirs();
+        context = new Mockery();
+        delegateService = context.mock(IAuthenticationService.class);
+        timeProvider = context.mock(ITimeProvider.class);
+        service =
+                new CachingAuthenticationService(delegateService,
+                        CachingAuthenticationService.createUserStore(PASSWD_FILENAME),
+                        CACHE_TIME_NO_REVAL_MILLIS, CACHE_TIME_MILLIS, false, timeProvider);
+        revalidator = service.new RevalidationRunnable();
+    }
+
+    @AfterMethod
+    public void tearDown()
+    {
+        context.assertIsSatisfied();
+    }
+
+    @AfterClass
+    public void cleanUp()
+    {
+        FileUtilities.deleteRecursively(workingDirectory);
+    }
+
+    @Test
+    public void testAuthenticateUserFirstTimeSuccess()
+    {
+        context.checking(new Expectations()
+            {
+                {
+                    one(timeProvider).getTimeInMilliseconds();
+                    will(returnValue(timeStart));
+                    one(delegateService).tryGetAndAuthenticateUser(user, password);
+                    will(returnValue(new Principal(user, firstName, lastName, email, true)));
+                    one(timeProvider).getTimeInMilliseconds();
+                    will(returnValue(timeCache));
+                }
+            });
+        final Principal p = service.tryGetAndAuthenticateUser(user, password);
+        checkPrincipalHappyCase(user, firstName, lastName, email, p);
+        checkCacheHappyCase(user, firstName, lastName, email, timeCache);
+        assertTrue(service.getValidationQueue().isEmpty());
+        context.assertIsSatisfied();
+    }
+
+    @Test(dependsOnMethods = "testAuthenticateUserFirstTimeSuccess")
+    public void testAuthenticateUserFromCacheStatusOKSuccess()
+    {
+        // Now it has to be answered from the cache.
+        context.checking(new Expectations()
+            {
+                {
+                    one(timeProvider).getTimeInMilliseconds();
+                    will(returnValue(timeSecondRequest));
+                }
+            });
+        final Principal p = service.tryGetAndAuthenticateUser(user, password);
+        checkPrincipalHappyCase(user, firstName, lastName, email, p);
+        checkCacheHappyCase(user, firstName, lastName, email, timeCache);
+        assertTrue(service.getValidationQueue().isEmpty());
+        context.assertIsSatisfied();
+    }
+
+    @Test(dependsOnMethods = "testAuthenticateUserFromCacheStatusOKSuccess")
+    public void testAuthenticateUserFromCacheByEmailStatusOKSuccess()
+    {
+        // Now it has to be answered from the cache.
+        context.checking(new Expectations()
+            {
+                {
+                    one(timeProvider).getTimeInMilliseconds();
+                    will(returnValue(timeThirdRequest));
+                }
+            });
+        final Principal p = service.tryGetAndAuthenticateUserByEmail(email.toUpperCase(), password);
+        checkPrincipalHappyCase(user, firstName, lastName, email, p);
+        checkCacheHappyCase(user, firstName, lastName, email, timeCache);
+        assertTrue(service.getValidationQueue().isEmpty());
+        context.assertIsSatisfied();
+    }
+
+    @Test(dependsOnMethods = "testAuthenticateUserFromCacheByEmailStatusOKSuccess")
+    public void testAuthenticateUserFromCacheStatusOKRevalSuccess() throws InterruptedException
+    {
+        // Now it has to be answered from the cache, but re-validation is required.
+        context.checking(new Expectations()
+            {
+                {
+                    one(timeProvider).getTimeInMilliseconds();
+                    will(returnValue(timeForthRequest));
+                }
+            });
+        final Principal p = service.tryGetAndAuthenticateUser(user, password);
+        checkPrincipalHappyCase(user, firstName, lastName, email, p);
+        checkCacheHappyCase(user, firstName, lastName, email, timeCache);
+        assertFalse(service.getValidationQueue().isEmpty());
+
+        // Perform revalidation
+        context.checking(new Expectations()
+            {
+                {
+                    one(delegateService).tryGetAndAuthenticateUser(user, password);
+                    will(returnValue(new Principal(user, firstName, lastName, email2, true)));
+                    one(timeProvider).getTimeInMilliseconds();
+                    will(returnValue(time2Cache));
+                }
+            });
+        revalidator.runOnce();
+        assertTrue(service.getValidationQueue().isEmpty());
+        checkCacheHappyCase(user, firstName, lastName, email2, time2Cache);
+
+        // Now it has to be answered from the cache again.
+        context.checking(new Expectations()
+            {
+                {
+                    one(timeProvider).getTimeInMilliseconds();
+                    will(returnValue(timeFifthRequest));
+                }
+            });
+        final Principal p2 =
+                service.tryGetAndAuthenticateUserByEmail(email2.toUpperCase(), password);
+        checkPrincipalHappyCase(user, firstName, lastName, email2, p2);
+        checkCacheHappyCase(user, firstName, lastName, email2, time2Cache);
+        assertTrue(service.getValidationQueue().isEmpty());
+
+        // Check that email is gone from the cache.
+        context.checking(new Expectations()
+            {
+                {
+                    one(timeProvider).getTimeInMilliseconds();
+                    will(returnValue(timeFifthRequest));
+                    one(delegateService).tryGetAndAuthenticateUserByEmail(email.toUpperCase(),
+                            password);
+                    will(returnValue(null));
+                }
+            });
+        final Principal p3 =
+                service.tryGetAndAuthenticateUserByEmail(email.toUpperCase(), password);
+        assertNull(p3);
+        context.assertIsSatisfied();
+    }
+
+    @Test(dependsOnMethods = "testAuthenticateUserFromCacheStatusOKRevalSuccess")
+    public void testAuthenticateUserAfterCacheExpiratonSuccess()
+    {
+        context.checking(new Expectations()
+            {
+                {
+                    one(timeProvider).getTimeInMilliseconds();
+                    will(returnValue(timeSixthRequest));
+                    one(delegateService).tryGetAndAuthenticateUser(user, password);
+                    will(returnValue(new Principal(user, firstName, lastName, email3, true)));
+                    one(timeProvider).getTimeInMilliseconds();
+                    will(returnValue(time3Cache));
+                }
+            });
+        final Principal p = service.tryGetAndAuthenticateUser(user, password);
+        checkPrincipalHappyCase(user, firstName, lastName, email3, p);
+        checkCacheHappyCase(user, firstName, lastName, email3, time3Cache);
+        assertTrue(service.getValidationQueue().isEmpty());
+        context.assertIsSatisfied();
+    }
+
+    @SuppressWarnings("hiding")
+    private void checkPrincipalHappyCase(final String user, final String firstName,
+            final String lastName, final String email, Principal p)
+    {
+        assertNotNull(p);
+        assertEquals(user, p.getUserId());
+        assertEquals(firstName, p.getFirstName());
+        assertEquals(lastName, p.getLastName());
+        assertEquals(email, p.getEmail());
+        assertTrue(p.isAuthenticated());
+    }
+
+    @SuppressWarnings("hiding")
+    private void checkCacheHappyCase(final String user, final String firstName,
+            final String lastName,
+            final String email, final long timeCache)
+    {
+        assertTrue(PASSWD_FILE.exists());
+        assertTrue(PASSWD_FILE.length() > 0);
+        final List<String> passwdLines = FileUtilities.loadToStringList(PASSWD_FILE, null);
+        assertEquals(1, passwdLines.size());
+        final String[] split = StringUtils.split(passwdLines.get(0), ':');
+        assertEquals(UserCacheEntry.NUMBER_OF_COLUMNS_IN_PASSWORD_CACHE_FILE, split.length);
+        assertEquals(user, split[0]);
+        assertEquals(email, split[1]);
+        assertEquals(firstName, split[2]);
+        assertEquals(lastName, split[3]);
+        assertTrue(split[4], Pattern.matches("[\\w\\/,\\+]+", split[4]));
+        assertEquals(Long.toString(timeCache), split[5]);
+    }
+}