///////////////////////////////////////////////////////////////////////////// // // 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; import java.util.HashMap; import java.util.Iterator; import java.util.Map; /** * Class used by {@link LoginProtection} for handling maps, time offsets etc. * @author Kai Reinhard (k.reinhard@micromata.de) * */ public class LoginProtectionMap { /** * Login offset time after failed login attempts expires after 24h. */ private long loginOffsetExpiresAfterMs = 24 * 60 * 60 * 1000; /** * Login time offset will be number of failed logins multiplied by this value (in ms). */ private long loginTimeOffsetScale = 1000L; private int numberOfFailedLoginsBeforeIncrementing; /** * Number of failed logins per IP address/user id. */ private final Map<String, Integer> loginFailedAttemptsMap = new HashMap<String, Integer>(); /** * Time stamp of last failed login per IP address/user Id in ms since 01/01/1970. * @see System#currentTimeMillis() */ private final Map<String, Long> lastFailedLoginMap = new HashMap<String, Long>(); /** * 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). * @param userId This could be the client's ip address, the login name etc. * @return 0 if no active time offset was found, otherwise the time offset left until the account is opened again for login. */ public long getFailedLoginTimeOffsetIfExists(final String id) { final Long lastFailedLoginInMs = this.lastFailedLoginMap.get(id); if (lastFailedLoginInMs == null) { return 0; } final long offset = getFailedLoginTimeOffset(id, false); final long currentTimeInMs = System.currentTimeMillis(); if (lastFailedLoginInMs + offset < currentTimeInMs) { return 0; } return lastFailedLoginInMs + offset - currentTimeInMs; } /** * Increments the number of login failures. * @param id This could be the client's ip address, the login name etc. * @return Login time offset in ms. */ public long incrementFailedLoginTimeOffset(final String id) { return getFailedLoginTimeOffset(id, true); } /** * @param id This could be the client's ip address, the login name etc. * @param increment If true the login fail counter will be incremented. * @return */ private long getFailedLoginTimeOffset(final String id, final boolean increment) { clearExpiredEntries(); final long currentTimeInMillis = System.currentTimeMillis(); Integer numberOfFailedLogins = this.loginFailedAttemptsMap.get(id); if (numberOfFailedLogins == null) { if (increment == false) { return 0; } numberOfFailedLogins = 0; } else { final Long lastFailedLoginInMs = this.lastFailedLoginMap.get(id); if (lastFailedLoginInMs != null && currentTimeInMillis - lastFailedLoginInMs > loginOffsetExpiresAfterMs) { // Last failed login entry is to old, so we'll ignore and clear it: clearLoginTimeOffset(id); if (increment == false) { return 0; } numberOfFailedLogins = 0; } } if (increment == true) { synchronized (this) { this.loginFailedAttemptsMap.put(id, ++numberOfFailedLogins); this.lastFailedLoginMap.put(id, currentTimeInMillis); } } return (numberOfFailedLogins / numberOfFailedLoginsBeforeIncrementing) * loginTimeOffsetScale; } /** * Call this method after successful authentication. The counter of failed logins will be cleared. * @param id This could be the client's ip address, the login name etc. */ public void clearLoginTimeOffset(final String id) { synchronized (this) { this.loginFailedAttemptsMap.remove(id); this.lastFailedLoginMap.remove(id); } } /** * Clears (removes) all entries for id's (user id's, ip addresses) older than {@link #DEFAULT_LOGIN_OFFSET_EXPIRES_AFTER_MS}. */ public void clearExpiredEntries() { final long currentTimeInMillis = System.currentTimeMillis(); synchronized (this) { final Iterator<String> it = this.lastFailedLoginMap.keySet().iterator(); while (it.hasNext() == true) { final String key = it.next(); final Long lastFailedLoginInMs = this.lastFailedLoginMap.get(key); if (lastFailedLoginInMs != null && currentTimeInMillis - lastFailedLoginInMs > loginOffsetExpiresAfterMs) { // Last failed login entry is to old, so we'll ignore and clear it: this.loginFailedAttemptsMap.remove(key); it.remove(); } } } } /** * Clears all entries of failed logins (counter and time stamps). */ public void clearAll() { synchronized (this) { this.loginFailedAttemptsMap.clear(); this.lastFailedLoginMap.clear(); } } /** * For internal use by test cases. */ int getSizeOfLastFailedLoginMap() { return this.lastFailedLoginMap.size(); } /** * For internal use by test cases. */ int getSizeOfLoginFailedAttemptsMap() { return this.loginFailedAttemptsMap.size(); } /** * For internal use by test cases. */ void setEntry(final String id, final int numberOfFailedLoginAttempts, final long lastFailedAttemptTimestamp) { synchronized (this) { this.loginFailedAttemptsMap.put(id, numberOfFailedLoginAttempts); this.lastFailedLoginMap.put(id, lastFailedAttemptTimestamp); } } /** * @param id This could be the client's ip address, the login name etc. * @return The number of failed login attempts (not expired ones) if exist, otherwise 0. */ public int getNumberOfFailedLoginAttempts(final String id) { final Integer result = this.loginFailedAttemptsMap.get(id); return result != null ? result : 0; } /** * After this number of ms (24h is the default value) after the last failed login an entry for a failed login (for both: by user id and by * ip) is removed. * @return the loginOffsetExpiresAfterMs */ public long getLoginOffsetExpiresAfterMs() { return loginOffsetExpiresAfterMs; } /** * @param loginOffsetExpiresAfterMs the loginOffsetExpiresAfterMs to set * @return this for chaining. * @see #getLoginOffsetExpiresAfterMs() */ public LoginProtectionMap setLoginOffsetExpiresAfterMs(final long loginOffsetExpiresAfterMs) { this.loginOffsetExpiresAfterMs = loginOffsetExpiresAfterMs; return this; } /** * After failed logins the login time offset is increased by this value (default is 1 second). * @return the loginTimeOffsetScale */ public long getLoginTimeOffsetScale() { return loginTimeOffsetScale; } /** * @param loginTimeOffsetScale the loginTimeOffsetScale to set * @return this for chaining. * @see #getLoginTimeOffsetScale() */ public LoginProtectionMap setLoginTimeOffsetScale(final long loginTimeOffsetScale) { this.loginTimeOffsetScale = loginTimeOffsetScale; return this; } /** * This amount contains the number of required failed logins before incrementing the time offset. * @return the numberOfFailedLoginsBeforeIncrementing */ public int getNumberOfFailedLoginsBeforeIncrementing() { return numberOfFailedLoginsBeforeIncrementing; } /** * @param numberOfFailedLoginsBeforeIncrementing the numberOfFailedLoginsBeforeIncrementing to set * @return this for chaining. * @see #getNumberOfFailedLoginsBeforeIncrementing() */ public LoginProtectionMap setNumberOfFailedLoginsBeforeIncrementing(final int numberOfFailedLoginsBeforeIncrementing) { this.numberOfFailedLoginsBeforeIncrementing = numberOfFailedLoginsBeforeIncrementing; return this; } }