#949 Add configurable timeout for captcha count

This commit is contained in:
ljacqu 2017-02-19 11:50:06 +01:00
parent 510826d268
commit 39395836b4
3 changed files with 32 additions and 46 deletions

View File

@ -1,19 +1,22 @@
package fr.xephi.authme.data; package fr.xephi.authme.data;
import fr.xephi.authme.initialization.HasCleanup;
import fr.xephi.authme.initialization.SettingsDependent; import fr.xephi.authme.initialization.SettingsDependent;
import fr.xephi.authme.util.RandomStringUtils;
import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.SecuritySettings; import fr.xephi.authme.settings.properties.SecuritySettings;
import fr.xephi.authme.util.RandomStringUtils;
import fr.xephi.authme.util.TimedCounter;
import javax.inject.Inject; import javax.inject.Inject;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/** /**
* Manager for the handling of captchas. * Manager for the handling of captchas.
*/ */
public class CaptchaManager implements SettingsDependent { public class CaptchaManager implements SettingsDependent, HasCleanup {
private final ConcurrentHashMap<String, Integer> playerCounts; private final TimedCounter<String> playerCounts;
private final ConcurrentHashMap<String, String> captchaCodes; private final ConcurrentHashMap<String, String> captchaCodes;
private boolean isEnabled; private boolean isEnabled;
@ -22,8 +25,9 @@ public class CaptchaManager implements SettingsDependent {
@Inject @Inject
CaptchaManager(Settings settings) { CaptchaManager(Settings settings) {
this.playerCounts = new ConcurrentHashMap<>();
this.captchaCodes = new ConcurrentHashMap<>(); this.captchaCodes = new ConcurrentHashMap<>();
long countTimeout = settings.getProperty(SecuritySettings.CAPTCHA_COUNT_MINUTES_BEFORE_RESET);
this.playerCounts = new TimedCounter<>(countTimeout, TimeUnit.MINUTES);
reload(settings); reload(settings);
} }
@ -35,12 +39,7 @@ public class CaptchaManager implements SettingsDependent {
public void increaseCount(String name) { public void increaseCount(String name) {
if (isEnabled) { if (isEnabled) {
String playerLower = name.toLowerCase(); String playerLower = name.toLowerCase();
Integer currentCount = playerCounts.get(playerLower); playerCounts.increment(playerLower);
if (currentCount == null) {
playerCounts.put(playerLower, 1);
} else {
playerCounts.put(playerLower, currentCount + 1);
}
} }
} }
@ -51,21 +50,7 @@ public class CaptchaManager implements SettingsDependent {
* @return true if the player has to solve a captcha, false otherwise * @return true if the player has to solve a captcha, false otherwise
*/ */
public boolean isCaptchaRequired(String name) { public boolean isCaptchaRequired(String name) {
if (isEnabled) { return isEnabled && playerCounts.get(name.toLowerCase()) >= threshold;
Integer count = playerCounts.get(name.toLowerCase());
return count != null && count >= threshold;
}
return false;
}
/**
* Returns the stored captcha code for the player.
*
* @param name the player's name
* @return the code the player is required to enter, or null if none registered
*/
public String getCaptchaCode(String name) {
return captchaCodes.get(name.toLowerCase());
} }
/** /**
@ -75,7 +60,7 @@ public class CaptchaManager implements SettingsDependent {
* @return the code the player is required to enter * @return the code the player is required to enter
*/ */
public String getCaptchaCodeOrGenerateNew(String name) { public String getCaptchaCodeOrGenerateNew(String name) {
String code = getCaptchaCode(name); String code = captchaCodes.get(name.toLowerCase());
return code == null ? generateCode(name) : code; return code == null ? generateCode(name) : code;
} }
@ -127,6 +112,13 @@ public class CaptchaManager implements SettingsDependent {
this.isEnabled = settings.getProperty(SecuritySettings.USE_CAPTCHA); this.isEnabled = settings.getProperty(SecuritySettings.USE_CAPTCHA);
this.threshold = settings.getProperty(SecuritySettings.MAX_LOGIN_TRIES_BEFORE_CAPTCHA); this.threshold = settings.getProperty(SecuritySettings.MAX_LOGIN_TRIES_BEFORE_CAPTCHA);
this.captchaLength = settings.getProperty(SecuritySettings.CAPTCHA_LENGTH); this.captchaLength = settings.getProperty(SecuritySettings.CAPTCHA_LENGTH);
long countTimeout = settings.getProperty(SecuritySettings.CAPTCHA_COUNT_MINUTES_BEFORE_RESET);
playerCounts.setExpiration(countTimeout, TimeUnit.MINUTES);
}
@Override
public void performCleanup() {
playerCounts.removeExpiredEntries();
} }
} }

View File

@ -40,6 +40,10 @@ public class SecuritySettings implements SettingsHolder {
public static final Property<Integer> CAPTCHA_LENGTH = public static final Property<Integer> CAPTCHA_LENGTH =
newProperty("Security.captcha.captchaLength", 5); newProperty("Security.captcha.captchaLength", 5);
@Comment("Minutes after which login attempts count is reset for a player")
public static final Property<Integer> CAPTCHA_COUNT_MINUTES_BEFORE_RESET =
newProperty("Security.captcha.captchaCountReset", 60);
@Comment("Minimum length of password") @Comment("Minimum length of password")
public static final Property<Integer> MIN_PASSWORD_LENGTH = public static final Property<Integer> MIN_PASSWORD_LENGTH =
newProperty("settings.security.minPasswordLength", 5); newProperty("settings.security.minPasswordLength", 5);

View File

@ -3,12 +3,10 @@ package fr.xephi.authme.data;
import fr.xephi.authme.ReflectionTestUtils; import fr.xephi.authme.ReflectionTestUtils;
import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.SecuritySettings; import fr.xephi.authme.settings.properties.SecuritySettings;
import fr.xephi.authme.util.TimedCounter;
import org.junit.Test; import org.junit.Test;
import java.util.Map;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
@ -57,11 +55,6 @@ public class CaptchaManagerTest {
assertThat(manager.checkCode(player, "bogus"), equalTo(true)); assertThat(manager.checkCode(player, "bogus"), equalTo(true));
} }
/**
* Tests {@link CaptchaManager#getCaptchaCode} and {@link CaptchaManager#getCaptchaCodeOrGenerateNew}.
* The former method should never change the code (and so return {@code null} for no code) while the latter should
* generate a new code if no code is yet present. If a code is saved, it should never generate a new one.
*/
@Test @Test
public void shouldHaveSameCodeAfterGeneration() { public void shouldHaveSameCodeAfterGeneration() {
// given // given
@ -70,18 +63,14 @@ public class CaptchaManagerTest {
CaptchaManager manager = new CaptchaManager(settings); CaptchaManager manager = new CaptchaManager(settings);
// when // when
String code1 = manager.getCaptchaCode(player); String code1 = manager.getCaptchaCodeOrGenerateNew(player);
String code2 = manager.getCaptchaCodeOrGenerateNew(player); String code2 = manager.getCaptchaCodeOrGenerateNew(player);
String code3 = manager.getCaptchaCode(player); String code3 = manager.getCaptchaCodeOrGenerateNew(player);
String code4 = manager.getCaptchaCodeOrGenerateNew(player);
String code5 = manager.getCaptchaCode(player);
// then // then
assertThat(code1, nullValue()); assertThat(code1.length(), equalTo(5));
assertThat(code2.length(), equalTo(5)); assertThat(code2, equalTo(code1));
assertThat(code3, equalTo(code2)); assertThat(code3, equalTo(code1));
assertThat(code4, equalTo(code2));
assertThat(code5, equalTo(code2));
} }
@Test @Test
@ -104,7 +93,7 @@ public class CaptchaManagerTest {
// then 2 // then 2
assertThat(manager.isCaptchaRequired(player), equalTo(false)); assertThat(manager.isCaptchaRequired(player), equalTo(false));
assertHasCount(manager, player, null); assertHasCount(manager, player, 0);
} }
@Test @Test
@ -120,7 +109,7 @@ public class CaptchaManagerTest {
// then // then
assertThat(manager.isCaptchaRequired(player), equalTo(false)); assertThat(manager.isCaptchaRequired(player), equalTo(false));
assertHasCount(manager, player, null); assertHasCount(manager, player, 0);
} }
@Test @Test
@ -149,11 +138,12 @@ public class CaptchaManagerTest {
given(settings.getProperty(SecuritySettings.USE_CAPTCHA)).willReturn(true); given(settings.getProperty(SecuritySettings.USE_CAPTCHA)).willReturn(true);
given(settings.getProperty(SecuritySettings.MAX_LOGIN_TRIES_BEFORE_CAPTCHA)).willReturn(maxTries); given(settings.getProperty(SecuritySettings.MAX_LOGIN_TRIES_BEFORE_CAPTCHA)).willReturn(maxTries);
given(settings.getProperty(SecuritySettings.CAPTCHA_LENGTH)).willReturn(captchaLength); given(settings.getProperty(SecuritySettings.CAPTCHA_LENGTH)).willReturn(captchaLength);
given(settings.getProperty(SecuritySettings.CAPTCHA_COUNT_MINUTES_BEFORE_RESET)).willReturn(30);
return settings; return settings;
} }
private static void assertHasCount(CaptchaManager manager, String player, Integer count) { private static void assertHasCount(CaptchaManager manager, String player, Integer count) {
Map<String, Integer> playerCounts = ReflectionTestUtils TimedCounter<String> playerCounts = ReflectionTestUtils
.getFieldValue(CaptchaManager.class, manager, "playerCounts"); .getFieldValue(CaptchaManager.class, manager, "playerCounts");
assertThat(playerCounts.get(player.toLowerCase()), equalTo(count)); assertThat(playerCounts.get(player.toLowerCase()), equalTo(count));
} }