#1141 Move TOTP code during login as separate step: /2fa code

Rough version.
- Introduces a limbo player state on the LimboPlayer, allowing us to add further mandatory actions between successful (password) authentication and the ability to play on the server
This commit is contained in:
ljacqu
2018-03-20 23:06:08 +01:00
parent af6bee59bd
commit 495cfc69a9
18 changed files with 162 additions and 62 deletions
@@ -44,6 +44,7 @@ import fr.xephi.authme.command.executable.totp.AddTotpCommand;
import fr.xephi.authme.command.executable.totp.ConfirmTotpCommand;
import fr.xephi.authme.command.executable.totp.RemoveTotpCommand;
import fr.xephi.authme.command.executable.totp.TotpBaseCommand;
import fr.xephi.authme.command.executable.totp.TotpCodeCommand;
import fr.xephi.authme.command.executable.unregister.UnregisterCommand;
import fr.xephi.authme.command.executable.verification.VerificationCommand;
import fr.xephi.authme.permission.AdminPermission;
@@ -92,7 +93,6 @@ public class CommandInitializer {
.description("Login command")
.detailedDescription("Command to log in using AuthMeReloaded.")
.withArgument("password", "Login password", MANDATORY)
.withArgument("2facode", "TOTP code", OPTIONAL)
.permission(PlayerPermission.LOGIN)
.executableCommand(LoginCommand.class)
.register();
@@ -561,6 +561,15 @@ public class CommandInitializer {
.executableCommand(TotpBaseCommand.class)
.register();
// Register the base totp code
CommandDescription.builder()
.parent(null)
.labels("code", "c")
.description("Command for logging in")
.detailedDescription("Processes the two-factor authentication code during login.")
.executableCommand(TotpCodeCommand.class)
.register();
// Register totp add
CommandDescription.builder()
.parent(totpBase)
@@ -4,6 +4,7 @@ import fr.xephi.authme.command.PlayerCommand;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.data.captcha.LoginCaptchaManager;
import fr.xephi.authme.data.captcha.RegistrationCaptchaManager;
import fr.xephi.authme.data.limbo.LimboMessageType;
import fr.xephi.authme.data.limbo.LimboService;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.message.MessageKey;
@@ -80,6 +81,6 @@ public class CaptchaCommand extends PlayerCommand {
String newCode = registrationCaptchaManager.getCaptchaCodeOrGenerateNew(player.getName());
commonService.send(player, MessageKey.CAPTCHA_WRONG_ERROR, newCode);
}
limboService.resetMessageTask(player, false);
limboService.resetMessageTask(player, LimboMessageType.REGISTER);
}
}
@@ -19,8 +19,7 @@ public class LoginCommand extends PlayerCommand {
@Override
public void runCommand(Player player, List<String> arguments) {
String password = arguments.get(0);
String totpCode = arguments.size() > 1 ? arguments.get(1) : null;
management.performLogin(player, password, totpCode);
management.performLogin(player, password);
}
@Override
@@ -0,0 +1,72 @@
package fr.xephi.authme.command.executable.totp;
import fr.xephi.authme.command.PlayerCommand;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.data.limbo.LimboPlayer;
import fr.xephi.authme.data.limbo.LimboPlayerState;
import fr.xephi.authme.data.limbo.LimboService;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.message.Messages;
import fr.xephi.authme.process.login.AsynchronousLogin;
import fr.xephi.authme.security.totp.TotpService;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
/**
* TOTP code command for processing the 2FA code during the login process.
*/
public class TotpCodeCommand extends PlayerCommand {
@Inject
private LimboService limboService;
@Inject
private PlayerCache playerCache;
@Inject
private Messages messages;
@Inject
private TotpService totpService;
@Inject
private DataSource dataSource;
@Inject
private AsynchronousLogin asynchronousLogin;
@Override
protected void runCommand(Player player, List<String> arguments) {
if (playerCache.isAuthenticated(player.getName())) {
messages.send(player, MessageKey.ALREADY_LOGGED_IN_ERROR);
return;
}
PlayerAuth auth = dataSource.getAuth(player.getName());
if (auth == null) {
messages.send(player, MessageKey.REGISTER_MESSAGE);
return;
}
LimboPlayer limbo = limboService.getLimboPlayer(player.getName());
if (limbo.getState() == LimboPlayerState.TOTP_REQUIRED) {
processCode(player, limbo, auth, arguments.get(0));
} else {
messages.send(player, MessageKey.LOGIN_MESSAGE);
}
}
private void processCode(Player player, LimboPlayer limbo, PlayerAuth auth, String inputCode) {
boolean isCodeValid = totpService.verifyCode(auth, inputCode);
if (isCodeValid) {
limbo.setState(LimboPlayerState.FINISHED);
asynchronousLogin.performLogin(player, auth);
} else {
player.sendMessage("Invalid code!");
}
}
}
@@ -0,0 +1,11 @@
package fr.xephi.authme.data.limbo;
public enum LimboMessageType {
REGISTER,
LOG_IN,
TOTP_CODE
}
@@ -23,6 +23,7 @@ public class LimboPlayer {
private final float flySpeed;
private BukkitTask timeoutTask = null;
private MessageTask messageTask = null;
private LimboPlayerState state = LimboPlayerState.PASSWORD_REQUIRED;
public LimboPlayer(Location loc, boolean operator, Collection<String> groups, boolean fly, float walkSpeed,
float flySpeed) {
@@ -124,4 +125,12 @@ public class LimboPlayer {
setMessageTask(null);
setTimeoutTask(null);
}
public LimboPlayerState getState() {
return state;
}
public void setState(LimboPlayerState state) {
this.state = state;
}
}
@@ -0,0 +1,11 @@
package fr.xephi.authme.data.limbo;
public enum LimboPlayerState {
PASSWORD_REQUIRED,
TOTP_REQUIRED,
FINISHED
}
@@ -45,11 +45,11 @@ class LimboPlayerTaskManager {
*
* @param player the player
* @param limbo the associated limbo player of the player
* @param isRegistered whether the player is registered or not (needed to determine the message in the task)
* @param messageType message type
*/
void registerMessageTask(Player player, LimboPlayer limbo, boolean isRegistered) {
void registerMessageTask(Player player, LimboPlayer limbo, LimboMessageType messageType) {
int interval = settings.getProperty(RegistrationSettings.MESSAGE_INTERVAL);
MessageResult result = getMessageKey(player.getName(), isRegistered);
MessageResult result = getMessageKey(player.getName(), messageType);
if (interval > 0) {
String[] joinMessage = messages.retrieveSingle(player, result.messageKey, result.args).split("\n");
MessageTask messageTask = new MessageTask(player, joinMessage);
@@ -89,12 +89,14 @@ class LimboPlayerTaskManager {
* Returns the appropriate message key according to the registration status and settings.
*
* @param name the player's name
* @param isRegistered whether or not the username is registered
* @param messageType the message to show
* @return the message key to display to the user
*/
private MessageResult getMessageKey(String name, boolean isRegistered) {
if (isRegistered) {
private MessageResult getMessageKey(String name, LimboMessageType messageType) {
if (messageType == LimboMessageType.LOG_IN) {
return new MessageResult(MessageKey.LOGIN_MESSAGE);
} else if (messageType == LimboMessageType.TOTP_CODE) {
return new MessageResult(MessageKey.TWO_FACTOR_CODE_REQUIRED);
} else if (registrationCaptchaManager.isCaptchaRequired(name)) {
final String captchaCode = registrationCaptchaManager.getCaptchaCodeOrGenerateNew(name);
return new MessageResult(MessageKey.CAPTCHA_FOR_REGISTRATION_REQUIRED, captchaCode);
@@ -69,7 +69,8 @@ public class LimboService {
LimboPlayer limboPlayer = helper.merge(existingLimbo, limboFromDisk);
limboPlayer = helper.merge(helper.createLimboPlayer(player, isRegistered, location), limboPlayer);
taskManager.registerMessageTask(player, limboPlayer, isRegistered);
taskManager.registerMessageTask(player, limboPlayer,
isRegistered ? LimboMessageType.LOG_IN : LimboMessageType.REGISTER);
taskManager.registerTimeoutTask(player, limboPlayer);
helper.revokeLimboStates(player);
authGroupHandler.setGroup(player, limboPlayer,
@@ -134,7 +135,7 @@ public class LimboService {
Optional<LimboPlayer> limboPlayer = getLimboOrLogError(player, "reset tasks");
limboPlayer.ifPresent(limbo -> {
taskManager.registerTimeoutTask(player, limbo);
taskManager.registerMessageTask(player, limbo, true);
taskManager.registerMessageTask(player, limbo, LimboMessageType.LOG_IN);
});
authGroupHandler.setGroup(player, limboPlayer.orElse(null), AuthGroupType.REGISTERED_UNAUTHENTICATED);
}
@@ -143,11 +144,11 @@ public class LimboService {
* Resets the message task associated with the player's LimboPlayer.
*
* @param player the player to set a new message task for
* @param isRegistered whether or not the player is registered
* @param messageType the message to show for the limbo player
*/
public void resetMessageTask(Player player, boolean isRegistered) {
public void resetMessageTask(Player player, LimboMessageType messageType) {
getLimboOrLogError(player, "reset message task")
.ifPresent(limbo -> taskManager.registerMessageTask(player, limbo, isRegistered));
.ifPresent(limbo -> taskManager.registerMessageTask(player, limbo, messageType));
}
/**
@@ -197,6 +197,9 @@ public enum MessageKey {
/** Your secret code is %code. You can scan it from here %url */
TWO_FACTOR_CREATE("misc.two_factor_create", "%code", "%url"),
/** Please submit your two-factor authentication code with /2fa code &ltcode&gt;. */
TWO_FACTOR_CODE_REQUIRED("two_factor.code_required"),
/** You are not the owner of this account. Please choose another name! */
NOT_OWNER_ERROR("on_join_validation.not_owner_error"),
@@ -131,6 +131,7 @@ public class MessageUpdater {
.put("captcha", new String[]{"Captcha"})
.put("verification", new String[]{"Verification code"})
.put("time", new String[]{"Time units"})
.put("two_factor", new String[]{"Two-factor authentication"})
.build();
Set<String> addedKeys = new HashSet<>();
@@ -49,8 +49,8 @@ public class Management {
}
public void performLogin(Player player, String password, String totpCode) {
runTask(() -> asynchronousLogin.login(player, password, totpCode));
public void performLogin(Player player, String password) {
runTask(() -> asynchronousLogin.login(player, password));
}
public void forceLogin(Player player) {
@@ -6,6 +6,8 @@ import fr.xephi.authme.data.TempbanManager;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.data.captcha.LoginCaptchaManager;
import fr.xephi.authme.data.limbo.LimboMessageType;
import fr.xephi.authme.data.limbo.LimboPlayerState;
import fr.xephi.authme.data.limbo.LimboService;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.events.AuthMeAsyncPreLoginEvent;
@@ -18,7 +20,6 @@ import fr.xephi.authme.permission.PlayerStatePermission;
import fr.xephi.authme.process.AsynchronousProcess;
import fr.xephi.authme.process.SyncProcessManager;
import fr.xephi.authme.security.PasswordSecurity;
import fr.xephi.authme.security.totp.TotpService;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.service.CommonService;
import fr.xephi.authme.service.SessionService;
@@ -79,9 +80,6 @@ public class AsynchronousLogin implements AsynchronousProcess {
@Inject
private BungeeSender bungeeSender;
@Inject
private TotpService totpService;
AsynchronousLogin() {
}
@@ -90,12 +88,17 @@ public class AsynchronousLogin implements AsynchronousProcess {
*
* @param player the player to log in
* @param password the password to log in with
* @param totpCode the totp code (nullable)
*/
public void login(Player player, String password, String totpCode) {
public void login(Player player, String password) {
PlayerAuth auth = getPlayerAuth(player);
if (auth != null && checkPlayerInfo(player, auth, password, totpCode)) {
performLogin(player, auth);
if (auth != null && checkPlayerInfo(player, auth, password)) {
if (auth.getTotpKey() != null) {
limboService.resetMessageTask(player, LimboMessageType.TOTP_CODE);
limboService.getLimboPlayer(player.getName()).setState(LimboPlayerState.TOTP_REQUIRED);
// TODO #1141: Check if we should check limbo state before processing password
} else {
performLogin(player, auth);
}
}
}
@@ -130,7 +133,7 @@ public class AsynchronousLogin implements AsynchronousProcess {
if (auth == null) {
service.send(player, MessageKey.UNKNOWN_USER);
// Recreate the message task to immediately send the message again as response
limboService.resetMessageTask(player, false);
limboService.resetMessageTask(player, LimboMessageType.REGISTER);
return null;
}
@@ -161,11 +164,10 @@ public class AsynchronousLogin implements AsynchronousProcess {
* @param player the player requesting to log in
* @param auth the PlayerAuth object of the player
* @param password the password supplied by the player
* @param totpCode the input totp code (nullable)
* @return true if the password matches and all other conditions are met (e.g. no captcha required),
* false otherwise
*/
private boolean checkPlayerInfo(Player player, PlayerAuth auth, String password, String totpCode) {
private boolean checkPlayerInfo(Player player, PlayerAuth auth, String password) {
final String name = player.getName().toLowerCase();
// If captcha is required send a message to the player and deny to log in
@@ -180,17 +182,6 @@ public class AsynchronousLogin implements AsynchronousProcess {
loginCaptchaManager.increaseLoginFailureCount(name);
tempbanManager.increaseCount(ip, name);
if (auth.getTotpKey() != null) {
if (totpCode == null) {
player.sendMessage(
"You have two-factor authentication enabled. Please provide it: /login <password> <2faCode>");
return false;
} else if (!totpService.verifyCode(auth, totpCode)) {
player.sendMessage("Invalid code for two-factor authentication. Please try again");
return false;
}
}
if (passwordSecurity.comparePassword(password, auth.getPassword(), player.getName())) {
return true;
} else {
@@ -235,7 +226,7 @@ public class AsynchronousLogin implements AsynchronousProcess {
* @param player the player to log in
* @param auth the associated PlayerAuth object
*/
private void performLogin(Player player, PlayerAuth auth) {
public void performLogin(Player player, PlayerAuth auth) {
if (player.isOnline()) {
final boolean isFirstLogin = (auth.getLastLogin() == null);
@@ -26,7 +26,7 @@ public final class RestrictionSettings implements SettingsHolder {
@Comment("Allowed commands for unauthenticated players")
public static final Property<List<String>> ALLOW_COMMANDS =
newLowercaseListProperty("settings.restrictions.allowCommands",
"/login", "/register", "/l", "/reg", "/email", "/captcha");
"/login", "/register", "/l", "/reg", "/email", "/captcha", "/2fa", "/totp");
@Comment({
"Max number of allowed registrations per IP",