AccountLockoutManager.java
/*
* Copyright 2018 Global Crop Diversity Trust
*
* 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 org.genesys.blocks.security.lockout;
import java.io.Serializable;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.annotation.Resource;
import org.genesys.blocks.security.model.BasicUser;
import org.genesys.blocks.security.service.BasicUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.LockedException;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
/**
* {#link {@link AccountLockoutManager} keeps track of successive failed login
* attempts and locks the user account if there are more than {
* {@link #lockAfterXFailures} successive failures.
*
* @author Matija Obreza, matija.obreza@croptrust.org
*
*/
@Component
@Slf4j
public class AccountLockoutManager {
/** The login attempts. */
@Resource(name = "accountLockoutMap")
private Map<String, AttemptStatistics> loginAttempts;
/** The user service. */
@Autowired
private BasicUserService<?, ?> userService;
/** The lock after X failures. */
private int lockAfterXFailures = 5;
/** The lockout time. */
// Things older than 60minutes=60*60*1000
private int lockoutTime = 60 * 60 * 1000;
/**
* Set account lockout time.
*
* @param lockoutTime the new lockout time
*/
public void setLockoutTime(final int lockoutTime) {
this.lockoutTime = lockoutTime;
}
/**
* Set number of successive failed login attempts that result in account
* lockout.
*
* @param lockAfterXFailures the new lock after X failures
*/
public void setLockAfterXFailures(final int lockAfterXFailures) {
log.info("Will lock user accounts after " + lockAfterXFailures + " successive failed attempts.");
this.lockAfterXFailures = lockAfterXFailures;
}
/**
* Reset failed attempt statistics on successful login.
*
* @param userName the user name
*/
synchronized public void handleSuccessfulLogin(final String userName) {
purge();
if (userName == null) {
return;
}
final AttemptStatistics stats = loginAttempts.get(userName);
if (stats != null) {
loginAttempts.remove(userName);
log.info("Successful login. Removed failed login statistics for {}: {}", userName, stats);
}
}
/**
* Update failed attempt statistics on failed login.
*
* @param userName the user name
*/
synchronized public void handleFailedLogin(final String userName) {
purge();
if (userName == null) {
return;
}
AttemptStatistics stats = loginAttempts.get(userName);
if (stats == null) {
try {
final BasicUser<?> user = userService.getUserByEmail(userName);
if (user != null) {
stats = new AttemptStatistics();
stats.id = user.getId();
loginAttempts.put(userName, stats);
}
} catch (final Throwable e) {
log.warn("Could not load user data for {}: {}", userName, e.getMessage());
}
}
if (stats != null) {
stats.count++;
stats.lastAttempt = Instant.now();
loginAttempts.put(userName, stats);
log.info("Updated failed login statistics for username=" + userName + " " + stats);
if (stats.count >= lockAfterXFailures) {
log.warn("Too many failed login attempts. Locking account for username=" + userName);
try {
userService.setAccountLockLocal(stats.id, true);
} catch (final Throwable e) {
log.warn("Could not lock account {}: {}", userName, e.getMessage());
}
throw new LockedException("Too many failed login attempts.");
}
}
}
/**
* Removes expired statistics.
*/
synchronized void purge() {
if (loginAttempts.isEmpty()) {
return;
}
log.debug("Purging expired entries");
final List<String> userNames = new ArrayList<>(loginAttempts.keySet());
final long now = Instant.now().toEpochMilli();
for (final String userName : userNames) {
final AttemptStatistics stats = loginAttempts.get(userName);
if (stats == null) {
loginAttempts.remove(userName);
continue;
}
if ((now - stats.lastAttempt.toEpochMilli()) >= lockoutTime) {
loginAttempts.remove(userName);
log.info("Removed expired failed login statistics for {}: {}", userName, stats);
}
}
log.debug("Number of failed login attempts in memory: {}", loginAttempts.size());
}
/**
* The Class AttemptStatistics.
*/
public static class AttemptStatistics implements Serializable {
/** The Constant serialVersionUID. */
private static final long serialVersionUID = -5966606439944355735L;
/** The id. */
long id;
/** The count. */
int count = 0;
/** The last attempt. */
Instant lastAttempt = Instant.now();
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "count=" + count + " lastAttempt=" + lastAttempt;
}
}
}