#1141 Add TOTP key field to database and PlayerAuth

- Add new field for storing TOTP key
- Implement data source methods for manipulation of its value
This commit is contained in:
ljacqu 2018-03-05 19:50:58 +01:00
parent 930f5609bf
commit 9954c82cb6
10 changed files with 126 additions and 2 deletions

View File

@ -26,6 +26,7 @@ public class PlayerAuth {
/** The player's name in the correct casing, e.g. "Xephi". */
private String realName;
private HashedPassword password;
private String totpKey;
private String email;
private String lastIp;
private int groupId;
@ -160,6 +161,10 @@ public class PlayerAuth {
this.registrationDate = registrationDate;
}
public String getTotpKey() {
return totpKey;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof PlayerAuth)) {
@ -195,6 +200,7 @@ public class PlayerAuth {
private String name;
private String realName;
private HashedPassword password;
private String totpKey;
private String lastIp;
private String email;
private int groupId = -1;
@ -219,6 +225,7 @@ public class PlayerAuth {
auth.nickname = checkNotNull(name).toLowerCase();
auth.realName = firstNonNull(realName, "Player");
auth.password = firstNonNull(password, new HashedPassword(""));
auth.totpKey = totpKey;
auth.email = DB_EMAIL_DEFAULT.equals(email) ? null : email;
auth.lastIp = lastIp; // Don't check against default value 127.0.0.1 as it may be a legit value
auth.groupId = groupId;
@ -258,6 +265,11 @@ public class PlayerAuth {
return password(new HashedPassword(hash, salt));
}
public Builder totpKey(String totpKey) {
this.totpKey = totpKey;
return this;
}
public Builder lastIp(String lastIp) {
this.lastIp = lastIp;
return this;

View File

@ -268,6 +268,15 @@ public class CacheDataSource implements DataSource {
return source.getRecentlyLoggedInPlayers();
}
@Override
public boolean setTotpKey(String user, String totpKey) {
boolean result = source.setTotpKey(user, totpKey);
if (result) {
cachedAuths.refresh(user);
}
return result;
}
@Override
public void invalidateCache(String playerName) {
cachedAuths.invalidate(playerName);

View File

@ -14,6 +14,7 @@ public final class Columns {
public final String REAL_NAME;
public final String PASSWORD;
public final String SALT;
public final String TOTP_KEY;
public final String LAST_IP;
public final String LAST_LOGIN;
public final String GROUP;
@ -35,6 +36,7 @@ public final class Columns {
REAL_NAME = settings.getProperty(DatabaseSettings.MYSQL_COL_REALNAME);
PASSWORD = settings.getProperty(DatabaseSettings.MYSQL_COL_PASSWORD);
SALT = settings.getProperty(DatabaseSettings.MYSQL_COL_SALT);
TOTP_KEY = settings.getProperty(DatabaseSettings.MYSQL_COL_TOTP_KEY);
LAST_IP = settings.getProperty(DatabaseSettings.MYSQL_COL_LAST_IP);
LAST_LOGIN = settings.getProperty(DatabaseSettings.MYSQL_COL_LASTLOGIN);
GROUP = settings.getProperty(DatabaseSettings.MYSQL_COL_GROUP);

View File

@ -232,6 +232,25 @@ public interface DataSource extends Reloadable {
*/
List<PlayerAuth> getRecentlyLoggedInPlayers();
/**
* Sets the given TOTP key to the player's account.
*
* @param user the name of the player to modify
* @param totpKey the totp key to set
* @return True upon success, false upon failure
*/
boolean setTotpKey(String user, String totpKey);
/**
* Removes the TOTP key if present of the given player's account.
*
* @param user the name of the player to modify
* @return True upon success, false upon failure
*/
default boolean removeTotpKey(String user) {
return setTotpKey(user, null);
}
/**
* Reload the data source.
*/

View File

@ -398,6 +398,11 @@ public class FlatFile implements DataSource {
throw new UnsupportedOperationException("Flat file no longer supported");
}
@Override
public boolean setTotpKey(String user, String totpKey) {
throw new UnsupportedOperationException("Flat file no longer supported");
}
/**
* Creates a PlayerAuth object from the read data.
*

View File

@ -248,6 +248,11 @@ public class MySQL implements DataSource {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN "
+ col.HAS_SESSION + " SMALLINT NOT NULL DEFAULT '0' AFTER " + col.IS_LOGGED);
}
if (isColumnMissing(md, col.TOTP_KEY)) {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.TOTP_KEY + " VARCHAR(16);");
}
}
ConsoleLogger.info("MySQL setup finished");
}
@ -728,6 +733,20 @@ public class MySQL implements DataSource {
return players;
}
@Override
public boolean setTotpKey(String user, String totpKey) {
String sql = "UPDATE " + tableName + " SET " + col.TOTP_KEY + " = ? WHERE " + col.NAME + " = ?";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, totpKey);
pst.setString(2, user.toLowerCase());
pst.executeUpdate();
return true;
} catch (SQLException e) {
logSqlException(e);
}
return false;
}
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);
@ -735,6 +754,7 @@ public class MySQL implements DataSource {
.name(row.getString(col.NAME))
.realName(row.getString(col.REAL_NAME))
.password(row.getString(col.PASSWORD), salt)
.totpKey(row.getString(col.TOTP_KEY))
.lastLogin(getNullableLong(row, col.LAST_LOGIN))
.lastIp(row.getString(col.LAST_IP))
.email(row.getString(col.EMAIL))

View File

@ -171,6 +171,11 @@ public class SQLite implements DataSource {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.HAS_SESSION + " INT NOT NULL DEFAULT '0';");
}
if (isColumnMissing(md, col.TOTP_KEY)) {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.TOTP_KEY + " VARCHAR(16);");
}
}
ConsoleLogger.info("SQLite Setup finished");
}
@ -654,6 +659,21 @@ public class SQLite implements DataSource {
return players;
}
@Override
public boolean setTotpKey(String user, String totpKey) {
String sql = "UPDATE " + tableName + " SET " + col.TOTP_KEY + " = ? WHERE " + col.NAME + " = ?";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, totpKey);
pst.setString(2, user.toLowerCase());
pst.executeUpdate();
return true;
} catch (SQLException e) {
logSqlException(e);
}
return false;
}
private PlayerAuth buildAuthFromResultSet(ResultSet row) throws SQLException {
String salt = !col.SALT.isEmpty() ? row.getString(col.SALT) : null;
@ -662,6 +682,7 @@ public class SQLite implements DataSource {
.email(row.getString(col.EMAIL))
.realName(row.getString(col.REAL_NAME))
.password(row.getString(col.PASSWORD), salt)
.totpKey(row.getString(col.TOTP_KEY))
.lastLogin(getNullableLong(row, col.LAST_LOGIN))
.lastIp(row.getString(col.LAST_IP))
.registrationDate(row.getLong(col.REGISTRATION_DATE))

View File

@ -79,6 +79,10 @@ public final class DatabaseSettings implements SettingsHolder {
public static final Property<String> MYSQL_COL_HASSESSION =
newProperty("DataSource.mySQLColumnHasSession", "hasSession");
@Comment("Column for storing a player's TOTP key (for two-factor authentication)")
public static final Property<String> MYSQL_COL_TOTP_KEY =
newProperty("DataSource.mySQLtotpKey", "totp");
@Comment("Column for storing the player's last IP")
public static final Property<String> MYSQL_COL_LAST_IP =
newProperty("DataSource.mySQLColumnIp", "ip");

View File

@ -103,12 +103,14 @@ public abstract class AbstractDataSourceIntegrationTest {
assertThat(bobbyAuth, hasRegistrationInfo("127.0.4.22", 1436778723L));
assertThat(bobbyAuth.getLastLogin(), equalTo(1449136800L));
assertThat(bobbyAuth.getPassword(), equalToHash("$SHA$11aa0706173d7272$dbba966"));
assertThat(bobbyAuth.getTotpKey(), equalTo("JBSWY3DPEHPK3PXP"));
assertThat(userAuth, hasAuthBasicData("user", "user", "user@example.org", "34.56.78.90"));
assertThat(userAuth, hasAuthLocation(124.1, 76.3, -127.8, "nether", 0.23f, 4.88f));
assertThat(userAuth, hasRegistrationInfo(null, 0));
assertThat(userAuth.getLastLogin(), equalTo(1453242857L));
assertThat(userAuth.getPassword(), equalToHash("b28c32f624a4eb161d6adc9acb5bfc5b", "f750ba32"));
assertThat(userAuth.getTotpKey(), nullValue());
}
@Test
@ -494,4 +496,33 @@ public abstract class AbstractDataSourceIntegrationTest {
contains("user24", "user20", "user22", "user29", "user28",
"user16", "user18", "user12", "user14", "user11"));
}
@Test
public void shouldSetTotpKey() {
// given
DataSource dataSource = getDataSource();
String newTotpKey = "My new TOTP key";
// when
dataSource.setTotpKey("BObBy", newTotpKey);
dataSource.setTotpKey("does-not-exist", "bogus");
// then
assertThat(dataSource.getAuth("bobby").getTotpKey(), equalTo(newTotpKey));
}
@Test
public void shouldRemoveTotpKey() {
// given
DataSource dataSource = getDataSource();
// when
dataSource.removeTotpKey("BoBBy");
dataSource.removeTotpKey("user");
dataSource.removeTotpKey("does-not-exist");
// then
assertThat(dataSource.getAuth("bobby").getTotpKey(), nullValue());
assertThat(dataSource.getAuth("user").getTotpKey(), nullValue());
}
}

View File

@ -4,6 +4,7 @@ CREATE TABLE authme (
id INTEGER AUTO_INCREMENT,
username VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
totp VARCHAR(16),
ip VARCHAR(40),
lastlogin BIGINT,
regdate BIGINT NOT NULL,
@ -22,7 +23,7 @@ CREATE TABLE authme (
CONSTRAINT table_const_prim PRIMARY KEY (id)
);
INSERT INTO authme (id, username, password, ip, lastlogin, x, y, z, world, yaw, pitch, email, isLogged, realname, salt, regdate, regip)
VALUES (1,'bobby','$SHA$11aa0706173d7272$dbba966','123.45.67.89',1449136800,1.05,2.1,4.2,'world',-0.44,2.77,'your@email.com',0,'Bobby',NULL,1436778723,'127.0.4.22');
INSERT INTO authme (id, username, password, ip, lastlogin, x, y, z, world, yaw, pitch, email, isLogged, realname, salt, regdate, regip, totp)
VALUES (1,'bobby','$SHA$11aa0706173d7272$dbba966','123.45.67.89',1449136800,1.05,2.1,4.2,'world',-0.44,2.77,'your@email.com',0,'Bobby',NULL,1436778723,'127.0.4.22','JBSWY3DPEHPK3PXP');
INSERT INTO authme (id, username, password, ip, lastlogin, x, y, z, world, yaw, pitch, email, isLogged, realname, salt, regdate)
VALUES (NULL,'user','b28c32f624a4eb161d6adc9acb5bfc5b','34.56.78.90',1453242857,124.1,76.3,-127.8,'nether',0.23,4.88,'user@example.org',0,'user','f750ba32',0);