#930 Create registration captcha manager
- Introduce registration captcha manager, rename login captcha manager accordingly - Integrate reg. captcha manager into /register command Open points: - Refactor common captcha functionality into abstract superclass - If captcha before /register necessary, show appropriate message to player immediately - Unit tests
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
package fr.xephi.authme.command.executable.captcha;
|
||||
|
||||
import fr.xephi.authme.command.PlayerCommand;
|
||||
import fr.xephi.authme.data.CaptchaManager;
|
||||
import fr.xephi.authme.data.LoginCaptchaManager;
|
||||
import fr.xephi.authme.data.RegistrationCaptchaManager;
|
||||
import fr.xephi.authme.data.auth.PlayerCache;
|
||||
import fr.xephi.authme.data.limbo.LimboService;
|
||||
import fr.xephi.authme.message.MessageKey;
|
||||
@@ -17,7 +18,10 @@ public class CaptchaCommand extends PlayerCommand {
|
||||
private PlayerCache playerCache;
|
||||
|
||||
@Inject
|
||||
private CaptchaManager captchaManager;
|
||||
private LoginCaptchaManager loginCaptchaManager;
|
||||
|
||||
@Inject
|
||||
private RegistrationCaptchaManager registrationCaptchaManager;
|
||||
|
||||
@Inject
|
||||
private CommonService commonService;
|
||||
@@ -27,25 +31,40 @@ public class CaptchaCommand extends PlayerCommand {
|
||||
|
||||
@Override
|
||||
public void runCommand(Player player, List<String> arguments) {
|
||||
final String playerName = player.getName().toLowerCase();
|
||||
final String name = player.getName();
|
||||
|
||||
if (playerCache.isAuthenticated(playerName)) {
|
||||
if (playerCache.isAuthenticated(name)) {
|
||||
commonService.send(player, MessageKey.ALREADY_LOGGED_IN_ERROR);
|
||||
} else if (!captchaManager.isCaptchaRequired(playerName)) {
|
||||
commonService.send(player, MessageKey.USAGE_LOGIN);
|
||||
} else if (loginCaptchaManager.isCaptchaRequired(name)) {
|
||||
checkLoginCaptcha(player, arguments.get(0));
|
||||
} else if (registrationCaptchaManager.isCaptchaRequired(name)) {
|
||||
checkRegisterCaptcha(player, arguments.get(0));
|
||||
} else {
|
||||
checkCaptcha(player, arguments.get(0));
|
||||
MessageKey errorMessage = playerCache.isAuthenticated(name)
|
||||
? MessageKey.ALREADY_LOGGED_IN_ERROR : MessageKey.USAGE_LOGIN;
|
||||
commonService.send(player, errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkCaptcha(Player player, String captchaCode) {
|
||||
final boolean isCorrectCode = captchaManager.checkCode(player.getName(), captchaCode);
|
||||
private void checkLoginCaptcha(Player player, String captchaCode) {
|
||||
final boolean isCorrectCode = loginCaptchaManager.checkCode(player.getName(), captchaCode);
|
||||
if (isCorrectCode) {
|
||||
commonService.send(player, MessageKey.CAPTCHA_SUCCESS);
|
||||
commonService.send(player, MessageKey.LOGIN_MESSAGE);
|
||||
limboService.unmuteMessageTask(player);
|
||||
} else {
|
||||
String newCode = captchaManager.generateCode(player.getName());
|
||||
String newCode = loginCaptchaManager.generateCode(player.getName());
|
||||
commonService.send(player, MessageKey.CAPTCHA_WRONG_ERROR, newCode);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkRegisterCaptcha(Player player, String captchaCode) {
|
||||
final boolean isCorrectCode = registrationCaptchaManager.checkCode(player.getName(), captchaCode);
|
||||
if (isCorrectCode) {
|
||||
commonService.send(player, MessageKey.CAPTCHA_SUCCESS);
|
||||
commonService.send(player, MessageKey.REGISTER_MESSAGE);
|
||||
} else {
|
||||
String newCode = registrationCaptchaManager.generateCode(player.getName());
|
||||
commonService.send(player, MessageKey.CAPTCHA_WRONG_ERROR, newCode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package fr.xephi.authme.command.executable.register;
|
||||
|
||||
import fr.xephi.authme.ConsoleLogger;
|
||||
import fr.xephi.authme.command.PlayerCommand;
|
||||
import fr.xephi.authme.data.RegistrationCaptchaManager;
|
||||
import fr.xephi.authme.mail.EmailService;
|
||||
import fr.xephi.authme.message.MessageKey;
|
||||
import fr.xephi.authme.process.Management;
|
||||
@@ -45,8 +46,15 @@ public class RegisterCommand extends PlayerCommand {
|
||||
@Inject
|
||||
private ValidationService validationService;
|
||||
|
||||
@Inject
|
||||
private RegistrationCaptchaManager registrationCaptchaManager;
|
||||
|
||||
@Override
|
||||
public void runCommand(Player player, List<String> arguments) {
|
||||
if (!isCaptchaFulfilled(player)) {
|
||||
return; // isCaptchaFulfilled handles informing the player on failure
|
||||
}
|
||||
|
||||
if (commonService.getProperty(SecuritySettings.PASSWORD_HASH) == HashAlgorithm.TWO_FACTOR) {
|
||||
//for two factor auth we don't need to check the usage
|
||||
management.performRegister(RegistrationMethod.TWO_FACTOR_REGISTRATION,
|
||||
@@ -74,7 +82,16 @@ public class RegisterCommand extends PlayerCommand {
|
||||
|
||||
@Override
|
||||
public MessageKey getArgumentsMismatchMessage() {
|
||||
return MessageKey.USAGE_LOGIN;
|
||||
return MessageKey.USAGE_REGISTER;
|
||||
}
|
||||
|
||||
private boolean isCaptchaFulfilled(Player player) {
|
||||
if (registrationCaptchaManager.isCaptchaRequired(player.getName())) {
|
||||
String code = registrationCaptchaManager.getCaptchaCodeOrGenerateNew(player.getName());
|
||||
commonService.send(player, MessageKey.CAPTCHA_FOR_REGISTRATION_REQUIRED, code);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void handlePasswordRegistration(Player player, List<String> arguments) {
|
||||
|
||||
+11
-10
@@ -12,9 +12,9 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Manager for the handling of captchas.
|
||||
* Manager for the handling of captchas after too many failed login attempts.
|
||||
*/
|
||||
public class CaptchaManager implements SettingsDependent, HasCleanup {
|
||||
public class LoginCaptchaManager implements SettingsDependent, HasCleanup {
|
||||
|
||||
private final TimedCounter<String> playerCounts;
|
||||
private final ConcurrentHashMap<String, String> captchaCodes;
|
||||
@@ -24,7 +24,7 @@ public class CaptchaManager implements SettingsDependent, HasCleanup {
|
||||
private int captchaLength;
|
||||
|
||||
@Inject
|
||||
CaptchaManager(Settings settings) {
|
||||
LoginCaptchaManager(Settings settings) {
|
||||
this.captchaCodes = new ConcurrentHashMap<>();
|
||||
long countTimeout = settings.getProperty(SecuritySettings.CAPTCHA_COUNT_MINUTES_BEFORE_RESET);
|
||||
this.playerCounts = new TimedCounter<>(countTimeout, TimeUnit.MINUTES);
|
||||
@@ -36,7 +36,7 @@ public class CaptchaManager implements SettingsDependent, HasCleanup {
|
||||
*
|
||||
* @param name the player's name
|
||||
*/
|
||||
public void increaseCount(String name) {
|
||||
public void increaseLoginFailureCount(String name) {
|
||||
if (isEnabled) {
|
||||
String playerLower = name.toLowerCase();
|
||||
playerCounts.increment(playerLower);
|
||||
@@ -44,7 +44,7 @@ public class CaptchaManager implements SettingsDependent, HasCleanup {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given player is required to solve a captcha.
|
||||
* Returns whether the given player is required to solve a captcha before he can use /login again.
|
||||
*
|
||||
* @param name the name of the player to verify
|
||||
* @return true if the player has to solve a captcha, false otherwise
|
||||
@@ -84,12 +84,13 @@ public class CaptchaManager implements SettingsDependent, HasCleanup {
|
||||
* @return true if the code matches or if no captcha is required for the player, false otherwise
|
||||
*/
|
||||
public boolean checkCode(String name, String code) {
|
||||
String savedCode = captchaCodes.get(name.toLowerCase());
|
||||
final String nameLowerCase = name.toLowerCase();
|
||||
String savedCode = captchaCodes.get(nameLowerCase);
|
||||
if (savedCode == null) {
|
||||
return true;
|
||||
} else if (savedCode.equalsIgnoreCase(code)) {
|
||||
captchaCodes.remove(name.toLowerCase());
|
||||
playerCounts.remove(name.toLowerCase());
|
||||
captchaCodes.remove(nameLowerCase);
|
||||
playerCounts.remove(nameLowerCase);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -100,7 +101,7 @@ public class CaptchaManager implements SettingsDependent, HasCleanup {
|
||||
*
|
||||
* @param name the player's name
|
||||
*/
|
||||
public void resetCounts(String name) {
|
||||
public void resetLoginFailureCount(String name) {
|
||||
if (isEnabled) {
|
||||
captchaCodes.remove(name.toLowerCase());
|
||||
playerCounts.remove(name.toLowerCase());
|
||||
@@ -109,7 +110,7 @@ public class CaptchaManager implements SettingsDependent, HasCleanup {
|
||||
|
||||
@Override
|
||||
public void reload(Settings settings) {
|
||||
this.isEnabled = settings.getProperty(SecuritySettings.USE_CAPTCHA);
|
||||
this.isEnabled = settings.getProperty(SecuritySettings.ENABLE_LOGIN_FAILURE_CAPTCHA);
|
||||
this.threshold = settings.getProperty(SecuritySettings.MAX_LOGIN_TRIES_BEFORE_CAPTCHA);
|
||||
this.captchaLength = settings.getProperty(SecuritySettings.CAPTCHA_LENGTH);
|
||||
long countTimeout = settings.getProperty(SecuritySettings.CAPTCHA_COUNT_MINUTES_BEFORE_RESET);
|
||||
@@ -0,0 +1,98 @@
|
||||
package fr.xephi.authme.data;
|
||||
|
||||
import fr.xephi.authme.initialization.HasCleanup;
|
||||
import fr.xephi.authme.initialization.SettingsDependent;
|
||||
import fr.xephi.authme.settings.Settings;
|
||||
import fr.xephi.authme.settings.properties.SecuritySettings;
|
||||
import fr.xephi.authme.util.RandomStringUtils;
|
||||
import fr.xephi.authme.util.expiring.ExpiringSet;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Captcha handler for registration.
|
||||
*/
|
||||
public class RegistrationCaptchaManager implements SettingsDependent, HasCleanup {
|
||||
|
||||
private static final int MINUTES_VALID_FOR_REGISTRATION = 30;
|
||||
|
||||
private final Map<String, String> captchaCodes;
|
||||
private final ExpiringSet<String> verifiedNamesForRegistration;
|
||||
|
||||
private boolean isEnabledForRegistration;
|
||||
private int captchaLength;
|
||||
|
||||
@Inject
|
||||
RegistrationCaptchaManager(Settings settings) {
|
||||
this.captchaCodes = new ConcurrentHashMap<>();
|
||||
this.verifiedNamesForRegistration = new ExpiringSet<>(MINUTES_VALID_FOR_REGISTRATION, TimeUnit.MINUTES);
|
||||
reload(settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given player is required to solve a captcha before he can register.
|
||||
*
|
||||
* @param name the name of the player to verify
|
||||
* @return true if the player has to solve a captcha, false otherwise
|
||||
*/
|
||||
public boolean isCaptchaRequired(String name) {
|
||||
return isEnabledForRegistration && !verifiedNamesForRegistration.contains(name.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the stored captcha for the player or generates and saves a new one.
|
||||
*
|
||||
* @param name the player's name
|
||||
* @return the code the player is required to enter
|
||||
*/
|
||||
public String getCaptchaCodeOrGenerateNew(String name) {
|
||||
String code = captchaCodes.get(name.toLowerCase());
|
||||
return code == null ? generateCode(name) : code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a code for the player and returns it.
|
||||
*
|
||||
* @param name the name of the player to generate a code for
|
||||
* @return the generated code
|
||||
*/
|
||||
public String generateCode(String name) {
|
||||
String code = RandomStringUtils.generate(captchaLength);
|
||||
captchaCodes.put(name.toLowerCase(), code);
|
||||
return code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the given code against the existing one and resets the player's auth failure count upon success.
|
||||
*
|
||||
* @param name the name of the player to check
|
||||
* @param code the supplied code
|
||||
* @return true if the code matches or if no captcha is required for the player, false otherwise
|
||||
*/
|
||||
public boolean checkCode(String name, String code) {
|
||||
final String nameLowerCase = name.toLowerCase();
|
||||
String savedCode = captchaCodes.get(nameLowerCase);
|
||||
if (savedCode == null) {
|
||||
return true;
|
||||
} else if (savedCode.equalsIgnoreCase(code)) {
|
||||
captchaCodes.remove(nameLowerCase);
|
||||
verifiedNamesForRegistration.add(nameLowerCase);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reload(Settings settings) {
|
||||
this.isEnabledForRegistration = settings.getProperty(SecuritySettings.ENABLE_CAPTCHA_FOR_REGISTRATION);
|
||||
this.captchaLength = settings.getProperty(SecuritySettings.CAPTCHA_LENGTH);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void performCleanup() {
|
||||
verifiedNamesForRegistration.removeExpiredEntries();
|
||||
}
|
||||
}
|
||||
@@ -134,6 +134,9 @@ public enum MessageKey {
|
||||
/** Captcha code solved correctly! */
|
||||
CAPTCHA_SUCCESS("valid_captcha"),
|
||||
|
||||
/** To register you have to solve a captcha code first, please use the command: /captcha <theCaptcha> */
|
||||
CAPTCHA_FOR_REGISTRATION_REQUIRED("captcha_for_registration", "<theCaptcha>"),
|
||||
|
||||
/** A VIP player has joined the server when it was full! */
|
||||
KICK_FOR_VIP("kick_forvip"),
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ package fr.xephi.authme.process.login;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import fr.xephi.authme.ConsoleLogger;
|
||||
import fr.xephi.authme.data.CaptchaManager;
|
||||
import fr.xephi.authme.data.LoginCaptchaManager;
|
||||
import fr.xephi.authme.data.TempbanManager;
|
||||
import fr.xephi.authme.data.auth.PlayerAuth;
|
||||
import fr.xephi.authme.data.auth.PlayerCache;
|
||||
@@ -61,7 +61,7 @@ public class AsynchronousLogin implements AsynchronousProcess {
|
||||
private PasswordSecurity passwordSecurity;
|
||||
|
||||
@Inject
|
||||
private CaptchaManager captchaManager;
|
||||
private LoginCaptchaManager loginCaptchaManager;
|
||||
|
||||
@Inject
|
||||
private TempbanManager tempbanManager;
|
||||
@@ -163,15 +163,15 @@ public class AsynchronousLogin implements AsynchronousProcess {
|
||||
final String name = player.getName().toLowerCase();
|
||||
|
||||
// If captcha is required send a message to the player and deny to log in
|
||||
if (captchaManager.isCaptchaRequired(name)) {
|
||||
service.send(player, MessageKey.USAGE_CAPTCHA, captchaManager.getCaptchaCodeOrGenerateNew(name));
|
||||
if (loginCaptchaManager.isCaptchaRequired(name)) {
|
||||
service.send(player, MessageKey.USAGE_CAPTCHA, loginCaptchaManager.getCaptchaCodeOrGenerateNew(name));
|
||||
return false;
|
||||
}
|
||||
|
||||
final String ip = PlayerUtils.getPlayerIp(player);
|
||||
|
||||
// Increase the counts here before knowing the result of the login.
|
||||
captchaManager.increaseCount(name);
|
||||
loginCaptchaManager.increaseLoginFailureCount(name);
|
||||
tempbanManager.increaseCount(ip, name);
|
||||
|
||||
if (passwordSecurity.comparePassword(password, auth.getPassword(), player.getName())) {
|
||||
@@ -202,10 +202,10 @@ public class AsynchronousLogin implements AsynchronousProcess {
|
||||
service.send(player, MessageKey.WRONG_PASSWORD);
|
||||
|
||||
// If the authentication fails check if Captcha is required and send a message to the player
|
||||
if (captchaManager.isCaptchaRequired(player.getName())) {
|
||||
if (loginCaptchaManager.isCaptchaRequired(player.getName())) {
|
||||
limboService.muteMessageTask(player);
|
||||
service.send(player, MessageKey.USAGE_CAPTCHA,
|
||||
captchaManager.getCaptchaCodeOrGenerateNew(player.getName()));
|
||||
loginCaptchaManager.getCaptchaCodeOrGenerateNew(player.getName()));
|
||||
} else if (emailService.hasAllInformation() && !Utils.isEmailEmpty(auth.getEmail())) {
|
||||
service.send(player, MessageKey.FORGOT_PASSWORD_MESSAGE);
|
||||
}
|
||||
@@ -232,7 +232,7 @@ public class AsynchronousLogin implements AsynchronousProcess {
|
||||
|
||||
// Successful login, so reset the captcha & temp ban count
|
||||
final String name = player.getName();
|
||||
captchaManager.resetCounts(name);
|
||||
loginCaptchaManager.resetLoginFailureCount(name);
|
||||
tempbanManager.resetCount(ip, name);
|
||||
player.setNoDamageTicks(0);
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ public final class SecuritySettings implements SettingsHolder {
|
||||
newProperty("Security.console.logConsole", true);
|
||||
|
||||
@Comment("Enable captcha when a player uses wrong password too many times")
|
||||
public static final Property<Boolean> USE_CAPTCHA =
|
||||
public static final Property<Boolean> ENABLE_LOGIN_FAILURE_CAPTCHA =
|
||||
newProperty("Security.captcha.useCaptcha", false);
|
||||
|
||||
@Comment("Max allowed tries before a captcha is required")
|
||||
@@ -44,6 +44,10 @@ public final class SecuritySettings implements SettingsHolder {
|
||||
public static final Property<Integer> CAPTCHA_COUNT_MINUTES_BEFORE_RESET =
|
||||
newProperty("Security.captcha.captchaCountReset", 60);
|
||||
|
||||
@Comment("Require captcha before a player may register?")
|
||||
public static final Property<Boolean> ENABLE_CAPTCHA_FOR_REGISTRATION =
|
||||
newProperty("Security.captcha.requireForRegistration", false);
|
||||
|
||||
@Comment("Minimum length of password")
|
||||
public static final Property<Integer> MIN_PASSWORD_LENGTH =
|
||||
newProperty("settings.security.minPasswordLength", 5);
|
||||
|
||||
@@ -91,9 +91,10 @@ change_password_expired: 'You cannot change your password using this command any
|
||||
email_cooldown_error: '&cAn email was already sent recently. You must wait %time before you can send a new one.'
|
||||
|
||||
# Captcha
|
||||
usage_captcha: '&3To login you have to solve a captcha code, please use the command: /captcha <theCaptcha>'
|
||||
usage_captcha: '&3To log in you have to solve a captcha code, please use the command: /captcha <theCaptcha>'
|
||||
wrong_captcha: '&cWrong captcha, please type "/captcha THE_CAPTCHA" into the chat!'
|
||||
valid_captcha: '&2Captcha code solved correctly!'
|
||||
captcha_for_registration: 'To register you have to solve a captcha first, please use the command: /captcha <theCaptcha>'
|
||||
|
||||
# Verification code
|
||||
verification_code_required: '&3This command is sensitive and requires an email verification! Check your inbox and follow the email''s instructions.'
|
||||
@@ -101,7 +102,7 @@ usage_verification_code: '&cUsage: /verification <code>'
|
||||
incorrect_verification_code: '&cIncorrect code, please type "/verification <code>" into the chat, using the code you received by email'
|
||||
verification_code_verified: '&2Your identity has been verified! You can now execute all commands within the current session!'
|
||||
verification_code_already_verified: '&2You can already execute every sensitive command within the current session!'
|
||||
verification_code_expired: '&3Your code has expired! Execute an other sensitive command to get a new code!'
|
||||
verification_code_expired: '&3Your code has expired! Execute another sensitive command to get a new code!'
|
||||
verification_code_email_needed: '&3To verify your identity you need to link an email address with your account!!'
|
||||
|
||||
# Time units
|
||||
|
||||
Reference in New Issue
Block a user