diff --git a/src/main/java/fr/xephi/authme/cache/auth/EmailRecoveryData.java b/src/main/java/fr/xephi/authme/cache/auth/EmailRecoveryData.java deleted file mode 100644 index 3cc02577..00000000 --- a/src/main/java/fr/xephi/authme/cache/auth/EmailRecoveryData.java +++ /dev/null @@ -1,41 +0,0 @@ -package fr.xephi.authme.cache.auth; - -/** - * Stored data for email recovery. - */ -public class EmailRecoveryData { - - private final String email; - private final String recoveryCode; - - /** - * Constructor. - * - * @param email the email address - * @param recoveryCode the recovery code, or null if not available - * @param codeExpiration expiration timestamp of the recovery code - */ - public EmailRecoveryData(String email, String recoveryCode, Long codeExpiration) { - this.email = email; - - if (codeExpiration == null || System.currentTimeMillis() > codeExpiration) { - this.recoveryCode = null; - } else { - this.recoveryCode = recoveryCode; - } - } - - /** - * @return the email address - */ - public String getEmail() { - return email; - } - - /** - * @return the recovery code, if available and not expired - */ - public String getRecoveryCode() { - return recoveryCode; - } -} diff --git a/src/main/java/fr/xephi/authme/command/executable/email/RecoverEmailCommand.java b/src/main/java/fr/xephi/authme/command/executable/email/RecoverEmailCommand.java index cf9c3d4f..6f7923d7 100644 --- a/src/main/java/fr/xephi/authme/command/executable/email/RecoverEmailCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/email/RecoverEmailCommand.java @@ -1,7 +1,7 @@ package fr.xephi.authme.command.executable.email; import fr.xephi.authme.ConsoleLogger; -import fr.xephi.authme.cache.auth.EmailRecoveryData; +import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.PlayerCommand; @@ -11,15 +11,13 @@ import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.security.PasswordSecurity; import fr.xephi.authme.security.RandomString; import fr.xephi.authme.security.crypts.HashedPassword; -import fr.xephi.authme.settings.properties.EmailSettings; +import fr.xephi.authme.service.RecoveryCodeManager; import org.bukkit.entity.Player; import javax.inject.Inject; import java.util.List; -import static fr.xephi.authme.settings.properties.SecuritySettings.RECOVERY_CODE_HOURS_VALID; -import static fr.xephi.authme.settings.properties.SecuritySettings.RECOVERY_CODE_LENGTH; -import static fr.xephi.authme.util.Utils.MILLIS_PER_HOUR; +import static fr.xephi.authme.settings.properties.EmailSettings.RECOVERY_PASSWORD_LENGTH; /** * Command for password recovery by email. @@ -41,6 +39,9 @@ public class RecoverEmailCommand extends PlayerCommand { @Inject private SendMailSSL sendMailSsl; + @Inject + private RecoveryCodeManager recoveryCodeManager; + @Override public void runCommand(Player player, List arguments) { final String playerMail = arguments.get(0); @@ -56,48 +57,54 @@ public class RecoverEmailCommand extends PlayerCommand { return; } - EmailRecoveryData recoveryData = dataSource.getEmailRecoveryData(playerName); - if (recoveryData == null) { + PlayerAuth auth = dataSource.getAuth(playerName); // TODO: Create method to get email only + if (auth == null) { commandService.send(player, MessageKey.REGISTER_EMAIL_MESSAGE); return; } - final String email = recoveryData.getEmail(); + final String email = auth.getEmail(); if (email == null || !email.equalsIgnoreCase(playerMail) || "your@email.com".equalsIgnoreCase(email)) { commandService.send(player, MessageKey.INVALID_EMAIL); return; } - if (arguments.size() == 1) { - // Process /email recover addr@example.com - createAndSendRecoveryCode(playerName, recoveryData); + if (recoveryCodeManager.isRecoveryCodeNeeded()) { + // Process /email recovery addr@example.com + if (arguments.size() == 1) { + createAndSendRecoveryCode(playerName, email); + } else { + // Process /email recovery addr@example.com 12394 + processRecoveryCode(player, arguments.get(1), email); + } } else { - // Process /email recover addr@example.com 12394 - processRecoveryCode(player, arguments.get(1), recoveryData); + generateAndSendNewPassword(player, email); } } - private void createAndSendRecoveryCode(String name, EmailRecoveryData recoveryData) { - String recoveryCode = RandomString.generateHex(commandService.getProperty(RECOVERY_CODE_LENGTH)); - long expiration = System.currentTimeMillis() - + commandService.getProperty(RECOVERY_CODE_HOURS_VALID) * MILLIS_PER_HOUR; - - dataSource.setRecoveryCode(name, recoveryCode, expiration); - sendMailSsl.sendRecoveryCode(name, recoveryData.getEmail(), recoveryCode); + private void createAndSendRecoveryCode(String name, String email) { + String recoveryCode = recoveryCodeManager.generateCode(name); + sendMailSsl.sendRecoveryCode(name, email, recoveryCode); } - private void processRecoveryCode(Player player, String code, EmailRecoveryData recoveryData) { - if (!code.equals(recoveryData.getRecoveryCode())) { + private void processRecoveryCode(Player player, String code, String email) { + final String name = player.getName(); + if (!recoveryCodeManager.isCodeValid(name, code)) { player.sendMessage("The recovery code is not correct! Use /email recovery [email] to generate a new one"); return; } - final String name = player.getName(); - String thePass = RandomString.generate(commandService.getProperty(EmailSettings.RECOVERY_PASSWORD_LENGTH)); + generateAndSendNewPassword(player, email); + recoveryCodeManager.removeCode(name); + } + + private void generateAndSendNewPassword(Player player, String email) { + String name = player.getName(); + String thePass = RandomString.generate(commandService.getProperty(RECOVERY_PASSWORD_LENGTH)); HashedPassword hashNew = passwordSecurity.computeHash(thePass, name); + dataSource.updatePassword(name, hashNew); - dataSource.removeRecoveryCode(name); - sendMailSsl.sendPasswordMail(name, recoveryData.getEmail(), thePass); + sendMailSsl.sendPasswordMail(name, email, thePass); commandService.send(player, MessageKey.RECOVERY_EMAIL_SENT_MESSAGE); } } diff --git a/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java b/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java index a4f642d0..bab51daf 100644 --- a/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java +++ b/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java @@ -8,9 +8,7 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.ThreadFactoryBuilder; - import fr.xephi.authme.ConsoleLogger; -import fr.xephi.authme.cache.auth.EmailRecoveryData; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.security.crypts.HashedPassword; @@ -236,23 +234,6 @@ public class CacheDataSource implements DataSource { return source.getAllAuths(); } - @Override - public void setRecoveryCode(String name, String code, long expiration) { - source.setRecoveryCode(name, code, expiration); - cachedAuths.refresh(name); - } - - @Override - public EmailRecoveryData getEmailRecoveryData(String name) { - return source.getEmailRecoveryData(name); - } - - @Override - public void removeRecoveryCode(String name) { - source.removeRecoveryCode(name); - cachedAuths.refresh(name); - } - @Override public List getLoggedPlayers() { return new ArrayList<>(PlayerCache.getInstance().getCache().values()); diff --git a/src/main/java/fr/xephi/authme/datasource/Columns.java b/src/main/java/fr/xephi/authme/datasource/Columns.java index d56aae72..b6d732cd 100644 --- a/src/main/java/fr/xephi/authme/datasource/Columns.java +++ b/src/main/java/fr/xephi/authme/datasource/Columns.java @@ -22,8 +22,6 @@ public final class Columns { public final String EMAIL; public final String ID; public final String IS_LOGGED; - public final String RECOVERY_CODE; - public final String RECOVERY_EXPIRATION; public Columns(Settings settings) { NAME = settings.getProperty(DatabaseSettings.MYSQL_COL_NAME); @@ -40,8 +38,6 @@ public final class Columns { EMAIL = settings.getProperty(DatabaseSettings.MYSQL_COL_EMAIL); ID = settings.getProperty(DatabaseSettings.MYSQL_COL_ID); IS_LOGGED = settings.getProperty(DatabaseSettings.MYSQL_COL_ISLOGGED); - RECOVERY_CODE = settings.getProperty(DatabaseSettings.MYSQL_COL_RECOVERY_CODE); - RECOVERY_EXPIRATION = settings.getProperty(DatabaseSettings.MYSQL_COL_RECOVERY_EXPIRATION); } } diff --git a/src/main/java/fr/xephi/authme/datasource/DataSource.java b/src/main/java/fr/xephi/authme/datasource/DataSource.java index b069cf4b..5b25fde4 100644 --- a/src/main/java/fr/xephi/authme/datasource/DataSource.java +++ b/src/main/java/fr/xephi/authme/datasource/DataSource.java @@ -1,6 +1,5 @@ package fr.xephi.authme.datasource; -import fr.xephi.authme.cache.auth.EmailRecoveryData; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.initialization.Reloadable; import fr.xephi.authme.security.crypts.HashedPassword; @@ -195,30 +194,6 @@ public interface DataSource extends Reloadable { */ List getAllAuths(); - /** - * Set the password recovery code for a user. - * - * @param name The name of the user - * @param code The recovery code - * @param expiration Recovery code expiration (milliseconds timestamp) - */ - void setRecoveryCode(String name, String code, long expiration); - - /** - * Get the information necessary for performing a password recovery by email. - * - * @param name The name of the user - * @return The data of the player, or null if player doesn't exist - */ - EmailRecoveryData getEmailRecoveryData(String name); - - /** - * Remove the recovery code of a given user. - * - * @param name The name of the user - */ - void removeRecoveryCode(String name); - /** * Reload the data source. */ diff --git a/src/main/java/fr/xephi/authme/datasource/FlatFile.java b/src/main/java/fr/xephi/authme/datasource/FlatFile.java index f2490a84..f3f18014 100644 --- a/src/main/java/fr/xephi/authme/datasource/FlatFile.java +++ b/src/main/java/fr/xephi/authme/datasource/FlatFile.java @@ -1,7 +1,6 @@ package fr.xephi.authme.datasource; import fr.xephi.authme.ConsoleLogger; -import fr.xephi.authme.cache.auth.EmailRecoveryData; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.security.crypts.HashedPassword; @@ -469,21 +468,6 @@ public class FlatFile implements DataSource { throw new UnsupportedOperationException("Flat file no longer supported"); } - @Override - public void setRecoveryCode(String name, String code, long expiration) { - throw new UnsupportedOperationException("Flat file no longer supported"); - } - - @Override - public EmailRecoveryData getEmailRecoveryData(String name) { - throw new UnsupportedOperationException("Flat file no longer supported"); - } - - @Override - public void removeRecoveryCode(String name) { - throw new UnsupportedOperationException("Flat file no longer supported"); - } - private static PlayerAuth buildAuthFromArray(String[] args) { // Format allows 2, 3, 4, 7, 8, 9 fields. Anything else is unknown if (args.length >= 2 && args.length <= 9 && args.length != 5 && args.length != 6) { diff --git a/src/main/java/fr/xephi/authme/datasource/MySQL.java b/src/main/java/fr/xephi/authme/datasource/MySQL.java index ff771bb2..dc08d7b4 100644 --- a/src/main/java/fr/xephi/authme/datasource/MySQL.java +++ b/src/main/java/fr/xephi/authme/datasource/MySQL.java @@ -4,7 +4,6 @@ import com.google.common.annotations.VisibleForTesting; import com.zaxxer.hikari.HikariDataSource; import com.zaxxer.hikari.pool.HikariPool.PoolInitializationException; import fr.xephi.authme.ConsoleLogger; -import fr.xephi.authme.cache.auth.EmailRecoveryData; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.security.HashAlgorithm; import fr.xephi.authme.security.crypts.HashedPassword; @@ -209,14 +208,6 @@ public class MySQL implements DataSource { st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.IS_LOGGED + " SMALLINT NOT NULL DEFAULT '0' AFTER " + col.EMAIL); } - - if (isColumnMissing(md, col.RECOVERY_CODE)) { - st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.RECOVERY_CODE + " VARCHAR(20);"); - } - - if (isColumnMissing(md, col.RECOVERY_EXPIRATION)) { - st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.RECOVERY_EXPIRATION + " BIGINT;"); - } } ConsoleLogger.info("MySQL setup finished"); } @@ -865,55 +856,6 @@ public class MySQL implements DataSource { return auths; } - @Override - public void setRecoveryCode(String name, String code, long expiration) { - String sql = "UPDATE " + tableName - + " SET " + col.RECOVERY_CODE + " = ?, " - + col.RECOVERY_EXPIRATION + " = ?" - + " WHERE " + col.NAME + " = ?;"; - try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { - pst.setString(1, code); - pst.setLong(2, expiration); - pst.setString(3, name.toLowerCase()); - pst.executeUpdate(); - } catch (SQLException e) { - logSqlException(e); - } - } - - @Override - public EmailRecoveryData getEmailRecoveryData(String name) { - String sql = "SELECT " + col.EMAIL + ", " + col.RECOVERY_CODE + ", " + col.RECOVERY_EXPIRATION - + " FROM " + tableName - + " WHERE " + col.NAME + " = ?;"; - try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { - pst.setString(1, name.toLowerCase()); - try (ResultSet rs = pst.executeQuery()) { - if (rs.next()) { - return new EmailRecoveryData( - rs.getString(col.EMAIL), rs.getString(col.RECOVERY_CODE), rs.getLong(col.RECOVERY_EXPIRATION)); - } - } - } catch (SQLException e) { - logSqlException(e); - } - return null; - } - - @Override - public void removeRecoveryCode(String name) { - String sql = "UPDATE " + tableName - + " SET " + col.RECOVERY_CODE + " = NULL, " - + col.RECOVERY_EXPIRATION + " = NULL" - + " WHERE " + col.NAME + " = ?;"; - try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { - pst.setString(1, name.toLowerCase()); - pst.executeUpdate(); - } catch (SQLException e) { - logSqlException(e); - } - } - private PlayerAuth buildAuthFromResultSet(ResultSet row) throws SQLException { String salt = col.SALT.isEmpty() ? null : row.getString(col.SALT); int group = col.GROUP.isEmpty() ? -1 : row.getInt(col.GROUP); diff --git a/src/main/java/fr/xephi/authme/datasource/SQLite.java b/src/main/java/fr/xephi/authme/datasource/SQLite.java index f6ae8f90..8aae94c5 100644 --- a/src/main/java/fr/xephi/authme/datasource/SQLite.java +++ b/src/main/java/fr/xephi/authme/datasource/SQLite.java @@ -2,7 +2,6 @@ package fr.xephi.authme.datasource; import com.google.common.annotations.VisibleForTesting; import fr.xephi.authme.ConsoleLogger; -import fr.xephi.authme.cache.auth.EmailRecoveryData; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.security.crypts.HashedPassword; import fr.xephi.authme.settings.Settings; @@ -128,14 +127,6 @@ public class SQLite implements DataSource { if (isColumnMissing(md, col.IS_LOGGED)) { st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.IS_LOGGED + " INT DEFAULT '0';"); } - - if (isColumnMissing(md, col.RECOVERY_CODE)) { - st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.RECOVERY_CODE + " VARCHAR(20);"); - } - - if (isColumnMissing(md, col.RECOVERY_EXPIRATION)) { - st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.RECOVERY_EXPIRATION + " BIGINT;"); - } } ConsoleLogger.info("SQLite Setup finished"); } @@ -595,55 +586,6 @@ public class SQLite implements DataSource { return auths; } - @Override - public void setRecoveryCode(String name, String code, long expiration) { - String sql = "UPDATE " + tableName - + " SET " + col.RECOVERY_CODE + " = ?, " - + col.RECOVERY_EXPIRATION + " = ?" - + " WHERE " + col.NAME + " = ?;"; - try (PreparedStatement pst = con.prepareStatement(sql)) { - pst.setString(1, code); - pst.setLong(2, expiration); - pst.setString(3, name.toLowerCase()); - pst.executeUpdate(); - } catch (SQLException e) { - logSqlException(e); - } - } - - @Override - public EmailRecoveryData getEmailRecoveryData(String name) { - String sql = "SELECT " + col.EMAIL + ", " + col.RECOVERY_CODE + ", " + col.RECOVERY_EXPIRATION - + " FROM " + tableName - + " WHERE " + col.NAME + " = ?;"; - try (PreparedStatement pst = con.prepareStatement(sql)) { - pst.setString(1, name.toLowerCase()); - try (ResultSet rs = pst.executeQuery()) { - if (rs.next()) { - return new EmailRecoveryData( - rs.getString(col.EMAIL), rs.getString(col.RECOVERY_CODE), rs.getLong(col.RECOVERY_EXPIRATION)); - } - } - } catch (SQLException e) { - logSqlException(e); - } - return null; - } - - @Override - public void removeRecoveryCode(String name) { - String sql = "UPDATE " + tableName - + " SET " + col.RECOVERY_CODE + " = NULL, " - + col.RECOVERY_EXPIRATION + " = NULL" - + " WHERE " + col.NAME + " = ?;"; - try (PreparedStatement pst = con.prepareStatement(sql)) { - pst.setString(1, name.toLowerCase()); - pst.executeUpdate(); - } catch (SQLException e) { - logSqlException(e); - } - } - private PlayerAuth buildAuthFromResultSet(ResultSet row) throws SQLException { String salt = !col.SALT.isEmpty() ? row.getString(col.SALT) : null; diff --git a/src/main/java/fr/xephi/authme/service/RecoveryCodeManager.java b/src/main/java/fr/xephi/authme/service/RecoveryCodeManager.java new file mode 100644 index 00000000..bca8dd51 --- /dev/null +++ b/src/main/java/fr/xephi/authme/service/RecoveryCodeManager.java @@ -0,0 +1,73 @@ +package fr.xephi.authme.service; + +import fr.xephi.authme.initialization.SettingsDependent; +import fr.xephi.authme.security.RandomString; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.SecuritySettings; + +import javax.inject.Inject; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import static fr.xephi.authme.settings.properties.SecuritySettings.RECOVERY_CODE_HOURS_VALID; +import static fr.xephi.authme.util.Utils.MILLIS_PER_HOUR; + +/** + * Manager for recovery codes. + */ +public class RecoveryCodeManager implements SettingsDependent { + + private Map recoveryCodes = new ConcurrentHashMap<>(); + + private int recoveryCodeLength; + private long recoveryCodeExpirationMillis; + + @Inject + RecoveryCodeManager(Settings settings) { + reload(settings); + } + + public boolean isRecoveryCodeNeeded() { + return recoveryCodeExpirationMillis > 0; + } + + public String generateCode(String player) { + String code = RandomString.generateHex(recoveryCodeLength); + recoveryCodes.put(player, new TimedEntry(code, System.currentTimeMillis() + recoveryCodeExpirationMillis)); + return code; + } + + public boolean isCodeValid(String player, String code) { + TimedEntry entry = recoveryCodes.get(player); + if (entry != null) { + return code != null && code.equals(entry.getCode()); + } + return false; + } + + public void removeCode(String player) { + recoveryCodes.remove(player); + } + + @Override + public void reload(Settings settings) { + recoveryCodeLength = settings.getProperty(SecuritySettings.RECOVERY_CODE_LENGTH); + recoveryCodeExpirationMillis = settings.getProperty(RECOVERY_CODE_HOURS_VALID) * MILLIS_PER_HOUR; + } + + private static final class TimedEntry { + + private final String code; + private final long expiration; + + TimedEntry(String code, long expiration) { + this.code = code; + this.expiration = expiration; + } + + public String getCode() { + return System.currentTimeMillis() < expiration ? code : null; + } + } + +} diff --git a/src/main/java/fr/xephi/authme/settings/properties/DatabaseSettings.java b/src/main/java/fr/xephi/authme/settings/properties/DatabaseSettings.java index 9b76c09c..2dc769ec 100644 --- a/src/main/java/fr/xephi/authme/settings/properties/DatabaseSettings.java +++ b/src/main/java/fr/xephi/authme/settings/properties/DatabaseSettings.java @@ -98,14 +98,6 @@ public class DatabaseSettings implements SettingsHolder { public static final Property MYSQL_COL_GROUP = newProperty("ExternalBoardOptions.mySQLColumnGroup", ""); - @Comment("Column for storing recovery code (when password lost)") - public static final Property MYSQL_COL_RECOVERY_CODE = - newProperty("DataSource.mySQLrecoveryCode", "recoverycode"); - - @Comment("Column for storing recovery code expiration") - public static final Property MYSQL_COL_RECOVERY_EXPIRATION = - newProperty("DataSource.mySQLrecoveryExpiration", "recoveryexpiration"); - private DatabaseSettings() { } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 18721925..6cf97f9f 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -40,10 +40,6 @@ DataSource: mySQLlastlocWorld: world # Column for RealName mySQLRealName: realname - # Column for storing recovery code (when password lost) - mySQLrecoveryCode: recoverycode - # Column for storing recovery code expiration - mySQLrecoveryExpiration: recoveryexpiration settings: # The name shown in the help messages. helpHeader: AuthMeReloaded diff --git a/src/test/java/fr/xephi/authme/command/executable/email/RecoverEmailCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/email/RecoverEmailCommandTest.java index 732e0777..6185d0b2 100644 --- a/src/test/java/fr/xephi/authme/command/executable/email/RecoverEmailCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/email/RecoverEmailCommandTest.java @@ -1,7 +1,7 @@ package fr.xephi.authme.command.executable.email; import fr.xephi.authme.TestHelper; -import fr.xephi.authme.cache.auth.EmailRecoveryData; +import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.command.CommandService; import fr.xephi.authme.datasource.DataSource; @@ -9,6 +9,7 @@ import fr.xephi.authme.mail.SendMailSSL; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.security.PasswordSecurity; import fr.xephi.authme.security.crypts.HashedPassword; +import fr.xephi.authme.service.RecoveryCodeManager; import fr.xephi.authme.settings.properties.EmailSettings; import fr.xephi.authme.settings.properties.SecuritySettings; import org.bukkit.entity.Player; @@ -24,11 +25,7 @@ import java.util.Arrays; import java.util.Collections; import static fr.xephi.authme.AuthMeMatchers.stringWithLength; -import static fr.xephi.authme.util.Utils.MILLIS_PER_HOUR; -import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.greaterThan; -import static org.hamcrest.Matchers.lessThan; import static org.junit.Assert.assertThat; import static org.mockito.BDDMockito.given; import static org.mockito.Matchers.any; @@ -66,6 +63,9 @@ public class RecoverEmailCommandTest { @Mock private SendMailSSL sendMailSsl; + + @Mock + private RecoveryCodeManager recoveryCodeManager; @BeforeClass public static void initLogger() { @@ -112,14 +112,14 @@ public class RecoverEmailCommandTest { given(sender.getName()).willReturn(name); given(sendMailSsl.hasAllInformation()).willReturn(true); given(playerCache.isAuthenticated(name)).willReturn(false); - given(dataSource.getEmailRecoveryData(name)).willReturn(null); + given(dataSource.getAuth(name)).willReturn(null); // when command.executeCommand(sender, Collections.singletonList("someone@example.com")); // then verify(sendMailSsl).hasAllInformation(); - verify(dataSource).getEmailRecoveryData(name); + verify(dataSource).getAuth(name); verifyNoMoreInteractions(dataSource); verify(commandService).send(sender, MessageKey.REGISTER_EMAIL_MESSAGE); } @@ -132,14 +132,14 @@ public class RecoverEmailCommandTest { given(sender.getName()).willReturn(name); given(sendMailSsl.hasAllInformation()).willReturn(true); given(playerCache.isAuthenticated(name)).willReturn(false); - given(dataSource.getEmailRecoveryData(name)).willReturn(newEmailRecoveryData(DEFAULT_EMAIL)); + given(dataSource.getAuth(name)).willReturn(newAuthWithEmail(DEFAULT_EMAIL)); // when command.executeCommand(sender, Collections.singletonList(DEFAULT_EMAIL)); // then verify(sendMailSsl).hasAllInformation(); - verify(dataSource).getEmailRecoveryData(name); + verify(dataSource).getAuth(name); verifyNoMoreInteractions(dataSource); verify(commandService).send(sender, MessageKey.INVALID_EMAIL); } @@ -152,14 +152,14 @@ public class RecoverEmailCommandTest { given(sender.getName()).willReturn(name); given(sendMailSsl.hasAllInformation()).willReturn(true); given(playerCache.isAuthenticated(name)).willReturn(false); - given(dataSource.getEmailRecoveryData(name)).willReturn(newEmailRecoveryData("raptor@example.org")); + given(dataSource.getAuth(name)).willReturn(newAuthWithEmail("raptor@example.org")); // when command.executeCommand(sender, Collections.singletonList("wrong-email@example.com")); // then verify(sendMailSsl).hasAllInformation(); - verify(dataSource).getEmailRecoveryData(name); + verify(dataSource).getAuth(name); verifyNoMoreInteractions(dataSource); verify(commandService).send(sender, MessageKey.INVALID_EMAIL); } @@ -173,26 +173,23 @@ public class RecoverEmailCommandTest { given(sendMailSsl.hasAllInformation()).willReturn(true); given(playerCache.isAuthenticated(name)).willReturn(false); String email = "v@example.com"; - given(dataSource.getEmailRecoveryData(name)).willReturn(newEmailRecoveryData(email)); + given(dataSource.getAuth(name)).willReturn(newAuthWithEmail(email)); int codeLength = 7; given(commandService.getProperty(SecuritySettings.RECOVERY_CODE_LENGTH)).willReturn(codeLength); int hoursValid = 12; given(commandService.getProperty(SecuritySettings.RECOVERY_CODE_HOURS_VALID)).willReturn(hoursValid); + String code = "a94f37"; + given(recoveryCodeManager.isRecoveryCodeNeeded()).willReturn(true); + given(recoveryCodeManager.generateCode(name)).willReturn(code); // when command.executeCommand(sender, Collections.singletonList(email.toUpperCase())); // then verify(sendMailSsl).hasAllInformation(); - verify(dataSource).getEmailRecoveryData(name); - ArgumentCaptor codeCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor expirationCaptor = ArgumentCaptor.forClass(Long.class); - verify(dataSource).setRecoveryCode(eq(name), codeCaptor.capture(), expirationCaptor.capture()); - assertThat(codeCaptor.getValue(), stringWithLength(codeLength)); - // Check expiration with a tolerance - assertThat(expirationCaptor.getValue() - System.currentTimeMillis(), - allOf(lessThan(12L * MILLIS_PER_HOUR), greaterThan((long) (11.9 * MILLIS_PER_HOUR)))); - verify(sendMailSsl).sendRecoveryCode(name, email, codeCaptor.getValue()); + verify(dataSource).getAuth(name); + verify(recoveryCodeManager).generateCode(name); + verify(sendMailSsl).sendRecoveryCode(name, email, code); } @Test @@ -204,19 +201,18 @@ public class RecoverEmailCommandTest { given(sendMailSsl.hasAllInformation()).willReturn(true); given(playerCache.isAuthenticated(name)).willReturn(false); String email = "vulture@example.com"; - String code = "A6EF3AC8"; - EmailRecoveryData recoveryData = newEmailRecoveryData(email, code); - given(dataSource.getEmailRecoveryData(name)).willReturn(recoveryData); + PlayerAuth auth = newAuthWithEmail(email); + given(dataSource.getAuth(name)).willReturn(auth); given(commandService.getProperty(EmailSettings.RECOVERY_PASSWORD_LENGTH)).willReturn(20); - given(passwordSecurity.computeHash(anyString(), eq(name))) - .willAnswer(invocation -> new HashedPassword((String) invocation.getArguments()[0])); + given(recoveryCodeManager.isRecoveryCodeNeeded()).willReturn(true); + given(recoveryCodeManager.isCodeValid(name, "bogus")).willReturn(false); // when command.executeCommand(sender, Arrays.asList(email, "bogus")); // then verify(sendMailSsl).hasAllInformation(); - verify(dataSource, only()).getEmailRecoveryData(name); + verify(dataSource, only()).getAuth(name); verify(sender).sendMessage(argThat(containsString("The recovery code is not correct"))); verifyNoMoreInteractions(sendMailSsl); } @@ -231,35 +227,35 @@ public class RecoverEmailCommandTest { given(playerCache.isAuthenticated(name)).willReturn(false); String email = "vulture@example.com"; String code = "A6EF3AC8"; - EmailRecoveryData recoveryData = newEmailRecoveryData(email, code); - given(dataSource.getEmailRecoveryData(name)).willReturn(recoveryData); + PlayerAuth auth = newAuthWithEmail(email); + given(dataSource.getAuth(name)).willReturn(auth); given(commandService.getProperty(EmailSettings.RECOVERY_PASSWORD_LENGTH)).willReturn(20); given(passwordSecurity.computeHash(anyString(), eq(name))) .willAnswer(invocation -> new HashedPassword((String) invocation.getArguments()[0])); + given(recoveryCodeManager.isRecoveryCodeNeeded()).willReturn(true); + given(recoveryCodeManager.isCodeValid(name, code)).willReturn(true); // when command.executeCommand(sender, Arrays.asList(email, code)); // then verify(sendMailSsl).hasAllInformation(); - verify(dataSource).getEmailRecoveryData(name); + verify(dataSource).getAuth(name); ArgumentCaptor passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordSecurity).computeHash(passwordCaptor.capture(), eq(name)); String generatedPassword = passwordCaptor.getValue(); assertThat(generatedPassword, stringWithLength(20)); verify(dataSource).updatePassword(eq(name), any(HashedPassword.class)); - verify(dataSource).removeRecoveryCode(name); + verify(recoveryCodeManager).removeCode(name); verify(sendMailSsl).sendPasswordMail(name, email, generatedPassword); verify(commandService).send(sender, MessageKey.RECOVERY_EMAIL_SENT_MESSAGE); } - private static EmailRecoveryData newEmailRecoveryData(String email) { - return new EmailRecoveryData(email, null, 0L); + private static PlayerAuth newAuthWithEmail(String email) { + return PlayerAuth.builder() + .name("name") + .email(email) + .build(); } - - private static EmailRecoveryData newEmailRecoveryData(String email, String code) { - return new EmailRecoveryData(email, code, System.currentTimeMillis() + 10_000); - } - } diff --git a/src/test/java/fr/xephi/authme/datasource/AbstractDataSourceIntegrationTest.java b/src/test/java/fr/xephi/authme/datasource/AbstractDataSourceIntegrationTest.java index 59c6ce22..1223130c 100644 --- a/src/test/java/fr/xephi/authme/datasource/AbstractDataSourceIntegrationTest.java +++ b/src/test/java/fr/xephi/authme/datasource/AbstractDataSourceIntegrationTest.java @@ -1,6 +1,5 @@ package fr.xephi.authme.datasource; -import fr.xephi.authme.cache.auth.EmailRecoveryData; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.security.crypts.HashedPassword; import org.junit.Test; @@ -382,63 +381,4 @@ public abstract class AbstractDataSourceIntegrationTest { // then assertThat(dataSource.getAllAuths(), empty()); } - - @Test - public void shouldSetRecoveryCode() { - // given - DataSource dataSource = getDataSource(); - String name = "Bobby"; - String code = "A123BC"; - - // when - dataSource.setRecoveryCode(name, code, System.currentTimeMillis() + 100_000L); - - // then - assertThat(dataSource.getEmailRecoveryData(name).getRecoveryCode(), equalTo(code)); - } - - @Test - public void shouldRemoveRecoveryCode() { - // given - String name = "User"; - DataSource dataSource = getDataSource(); - dataSource.setRecoveryCode(name, "code", System.currentTimeMillis() + 20_000L); - - // when - dataSource.removeRecoveryCode(name); - - // then - EmailRecoveryData recoveryData = dataSource.getEmailRecoveryData(name); - assertThat(recoveryData.getRecoveryCode(), nullValue()); - assertThat(recoveryData.getEmail(), equalTo("user@example.org")); - assertThat(dataSource.getEmailRecoveryData("bobby").getRecoveryCode(), nullValue()); - } - - @Test - public void shouldNotReturnRecoveryCodeIfExpired() { - // given - String name = "user"; - DataSource dataSource = getDataSource(); - dataSource.setRecoveryCode(name, "123456", System.currentTimeMillis() - 2_000L); - - // when - EmailRecoveryData recoveryData = dataSource.getEmailRecoveryData(name); - - // then - assertThat(recoveryData.getEmail(), equalTo("user@example.org")); - assertThat(recoveryData.getRecoveryCode(), nullValue()); - } - - @Test - public void shouldReturnNullForNoAvailableUser() { - // given - DataSource dataSource = getDataSource(); - - // when - EmailRecoveryData result = dataSource.getEmailRecoveryData("does-not-exist"); - - // then - assertThat(result, nullValue()); - } - }