UserTempLockoutCache.java

package org.entrystore.rest.auth;

import org.entrystore.Entry;
import org.entrystore.PrincipalManager;
import org.entrystore.User;
import org.entrystore.config.Config;
import org.entrystore.repository.RepositoryManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.URI;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.stream.Collectors;

import static org.entrystore.repository.config.Settings.AUTH_TEMP_LOCKOUT_ADMIN;
import static org.entrystore.repository.config.Settings.AUTH_TEMP_LOCKOUT_DURATION;
import static org.entrystore.repository.config.Settings.AUTH_TEMP_LOCKOUT_MAX_ATTEMPTS;

public class UserTempLockoutCache {

	private static final Logger log = LoggerFactory.getLogger(UserTempLockoutCache.class);

	private final int configAllowedFailedLoginAttempts;

	private final Duration configUserLockoutDuration;

	private final boolean configIncludeAdmin;

	private final PrincipalManager pm;

	private final ConcurrentMap<String, UserTemporaryLockout> userTempLockoutMap = new ConcurrentHashMap<>();

	public UserTempLockoutCache(RepositoryManager rm, PrincipalManager pm) {
		Config config = rm.getConfiguration();
		this.pm = pm;
		this.configAllowedFailedLoginAttempts = config.getInt(AUTH_TEMP_LOCKOUT_MAX_ATTEMPTS, 5);
		this.configUserLockoutDuration = config.getDuration(AUTH_TEMP_LOCKOUT_DURATION, Duration.ofMinutes(5));
		this.configIncludeAdmin = config.getBoolean(AUTH_TEMP_LOCKOUT_ADMIN, true);
	}

	public void succeedLogin(String userName) {
		userTempLockoutMap.remove(userName);
	}

	public void failLogin(String userName) {
		if (configAllowedFailedLoginAttempts < 0 || configUserLockoutDuration.isZero()) {
			return;
		}

		Entry userEntry = pm.getPrincipalEntry(userName);
		if (userEntry == null) {
			log.warn("Login attempt failed, user does not exist: {}", userName);
			return;
		}
		URI userUri = userEntry.getResourceURI();
		User user = pm.getUser(userUri);

		log.info("User [{}] failed login attempt due to providing wrong password", userName);

		if (pm.isUserAdminOrAdminGroup(userUri) && !configIncludeAdmin) {
			log.warn("Login attempt of user [{}] is not counted towards temporary lockout because of configuration for admin users", userName);
			return;
		}

		UserTemporaryLockout lockoutEntry = userTempLockoutMap.getOrDefault(userName, new UserTemporaryLockout(user, 1, null));

		if (lockoutEntry.disableUntil() != null && LocalDateTime.now().isAfter(lockoutEntry.disableUntil())) {
			new UserTemporaryLockout(user, 1, null);
		}

		if (lockoutEntry.failedLogins() < this.configAllowedFailedLoginAttempts) {
			lockoutEntry = new UserTemporaryLockout(user, lockoutEntry.failedLogins() + 1, null);
		} else {
			LocalDateTime lockedOutUntil = LocalDateTime.now().plus(configUserLockoutDuration);
			lockoutEntry = new UserTemporaryLockout(user, lockoutEntry.failedLogins() + 1, lockedOutUntil);
			log.warn("User [{}] failed too many login attempts and will be locked out until {}", userName, lockedOutUntil);
		}
		userTempLockoutMap.put(userName, lockoutEntry);
		pm.setAuthenticatedUserURI(null);
	}

	public boolean userIsLockedOut(String userName) {
		return getLockedOutUser(userName) != null;
	}

	public UserTemporaryLockout getLockedOutUser(String userName) {
		UserTemporaryLockout lockedOutUser = userTempLockoutMap.get(userName);
		if (lockedOutUser == null || lockedOutUser.disableUntil() == null) {
			return null;
		} else if (LocalDateTime.now().isAfter(lockedOutUser.disableUntil())) {
			log.info("User [{}] stopped being locked out", userName);
			userTempLockoutMap.remove(userName);
			return null;
		} else {
			return lockedOutUser;
		}
	}

	public List<UserTemporaryLockout> getLockedOutUsers() {
		List<UserTemporaryLockout> lockedOutUsers = userTempLockoutMap.entrySet().stream()
				.filter(failedLoginCount -> userIsLockedOut(failedLoginCount.getValue().user().getName()))
				.map(Map.Entry::getValue)
				.collect(Collectors.toList());
		return lockedOutUsers;
	}

	public record UserTemporaryLockout(User user, int failedLogins, LocalDateTime disableUntil) {}

}