/////////////////////////////////////////////////////////////////////////////
//
// Project ProjectForge Community Edition
// www.projectforge.org
//
// Copyright (C) 2001-2014 Kai Reinhard (k.reinhard@micromata.de)
//
// ProjectForge is dual-licensed.
//
// This community edition is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as published
// by the Free Software Foundation; version 3 of the License.
//
// This community edition is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
// Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, see http://www.gnu.org/licenses/.
//
/////////////////////////////////////////////////////////////////////////////
package org.projectforge.user;
/**
* Class for avoiding brute force attacks by time offsets during login after failed login attempts. Usage:<br/>
*
* <pre>
* public boolean login(String clientIp, String username, String password)
* {
* long offset = LoginProtection.instance().getFailedLoginTimeOffsetIfExists(username, clientIp);
* if (offset > 0) {
* final String seconds = String.valueOf(offset / 1000);
* final int numberOfFailedAttempts = loginProtection.getNumberOfFailedLoginAttempts(username, clientIpAddress);
* // setResponsePage(MessagePage.class, "Your account is locked for " + seconds +
* // " seconds due to " + numberOfFailedAttempts + " failed login attempts. Please try again later.");
* return false;
* }
* boolean success = checkLogin(username, password); // Check the login however you want.
* if (success == true) {
* LoginProtection.instance().clearLoginTimeOffset(userId, clientIp);
* return true;
* } else {
* LoginProtection.instance().incrementFailedLoginTimeOffset(userId, clientIp);
* return false;
* }
* }
* </pre>
*
* Time offsets for ip addresses should be much smaller (for avoiding penalties for normal usage by a lot of users behind the same NAT
* system).
* @author Kai Reinhard (k.reinhard@micromata.de)
*
*/
public class LoginProtection
{
private static final org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(LoginProtection.class);
/**
* After this given number of failed logins (for one specific user id) the account penalty counter will be incremented.
*/
private static final int DEFAULT_NUMBER_OF_FAILED_LOGINS_BEFORE_INCREMENTING_FOR_USER_ID = 1;
/**
* After this given number of failed logins (for one specific ip address) the account penalty counter will be incremented.
*/
private static final int DEFAULT_NUMBER_OF_FAILED_LOGINS_BEFORE_INCREMENTING_FOR_IP = 1000;
private static final LoginProtection instance = new LoginProtection();
public static LoginProtection instance()
{
return instance;
}
/**
* Singleton.
*/
private LoginProtection()
{
mapByUserId = new LoginProtectionMap();
mapByUserId.setNumberOfFailedLoginsBeforeIncrementing(DEFAULT_NUMBER_OF_FAILED_LOGINS_BEFORE_INCREMENTING_FOR_USER_ID);
mapByIpAddress = new LoginProtectionMap();
mapByIpAddress.setNumberOfFailedLoginsBeforeIncrementing(DEFAULT_NUMBER_OF_FAILED_LOGINS_BEFORE_INCREMENTING_FOR_IP);
}
private final LoginProtectionMap mapByUserId;
private final LoginProtectionMap mapByIpAddress;
/**
* Call this before checking the login credentials. If a long > 0 is returned please don't proceed the login-procedure. Please display a
* user message that the login was denied due previous failed login attempts. The user should try it later again (after x seconds). <br/>
* If a login offset exists for both (for user id and client's ip address) the larger value will be returned.
* @param userId May-be null.
* @param clientIpAddress May-be null.
* @return the time offset for login if exist, otherwise 0.
*/
public long getFailedLoginTimeOffsetIfExists(final String userId, final String clientIpAddress)
{
long userIdOffset = 0;
long ipAddressOffset = 0;
if (userId != null) {
userIdOffset = mapByUserId.getFailedLoginTimeOffsetIfExists(userId);
}
if (clientIpAddress != null) {
ipAddressOffset = mapByIpAddress.getFailedLoginTimeOffsetIfExists(clientIpAddress);
}
return (userIdOffset > ipAddressOffset) ? userIdOffset : ipAddressOffset;
}
/**
* Returns the number of failed login attempts. If failed login attempts exist for both (user id and ip) the larger value will be
* returned.
* @param userId May-be null.
* @param clientIpAddress May-be null.
* @return The number of failed login attempts (not expired ones) if exist, otherwise 0.
*/
public int getNumberOfFailedLoginAttempts(final String userId, final String clientIpAddress)
{
int failedLoginsForUserId = 0;
int failedLoginsForIpAddress = 0;
if (userId != null) {
failedLoginsForUserId = mapByUserId.getNumberOfFailedLoginAttempts(userId);
}
if (clientIpAddress != null) {
failedLoginsForIpAddress = mapByIpAddress.getNumberOfFailedLoginAttempts(clientIpAddress);
}
return (failedLoginsForUserId > failedLoginsForIpAddress) ? failedLoginsForUserId : failedLoginsForIpAddress;
}
/**
* Call this method after successful authentication. The counter of failed logins will be cleared.
* @param userId May-be null.
* @param clientIpAddress May-be null.
*/
public void clearLoginTimeOffset(final String userId, final String clientIpAddress)
{
if (userId != null) {
mapByUserId.clearLoginTimeOffset(userId);
}
if (clientIpAddress != null) {
mapByIpAddress.clearLoginTimeOffset(clientIpAddress);
}
}
/**
* Clears all entries of failed logins (counter and time stamps).
*/
public void clearAll()
{
mapByUserId.clearAll();
mapByIpAddress.clearAll();
}
/**
* Increments the number of login failures.
* @param userId May-be null.
* @param clientIpAddress May-be null.
* @return Login time offset in ms. If time offsets are given for both, the user id and the ip address, the larger one will be returned.
*/
public long incrementFailedLoginTimeOffset(final String userId, final String clientIpAddress)
{
long timeOffsetForUserId = 0;
long timeOffsetForIpAddress = 0;
if (userId != null) {
timeOffsetForUserId = mapByUserId.incrementFailedLoginTimeOffset(userId);
if (timeOffsetForUserId > 0) {
log.warn("Time-offset (penalty) for user '" + userId + "' increased: " + (timeOffsetForUserId / 1000) + " seconds.");
}
}
if (clientIpAddress != null) {
timeOffsetForIpAddress = mapByIpAddress.incrementFailedLoginTimeOffset(clientIpAddress);
if (timeOffsetForIpAddress > 0) {
log.warn("Time-offset (penalty) for ip address '"
+ clientIpAddress
+ "' increased: "
+ (timeOffsetForIpAddress / 1000)
+ " seconds.");
}
}
return (timeOffsetForUserId > timeOffsetForIpAddress) ? timeOffsetForUserId : timeOffsetForIpAddress;
}
/**
* @return the mapByIpAddress
*/
public LoginProtectionMap getMapByIpAddress()
{
return mapByIpAddress;
}
/**
* @return the mapByUserId
*/
public LoginProtectionMap getMapByUserId()
{
return mapByUserId;
}
}