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

View File

@ -268,6 +268,15 @@ public class CacheDataSource implements DataSource {
return source.getRecentlyLoggedInPlayers(); 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 @Override
public void invalidateCache(String playerName) { public void invalidateCache(String playerName) {
cachedAuths.invalidate(playerName); cachedAuths.invalidate(playerName);

View File

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

View File

@ -232,6 +232,25 @@ public interface DataSource extends Reloadable {
*/ */
List<PlayerAuth> getRecentlyLoggedInPlayers(); 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. * Reload the data source.
*/ */

View File

@ -398,6 +398,11 @@ public class FlatFile implements DataSource {
throw new UnsupportedOperationException("Flat file no longer supported"); 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. * 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 " st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN "
+ col.HAS_SESSION + " SMALLINT NOT NULL DEFAULT '0' AFTER " + col.IS_LOGGED); + 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"); ConsoleLogger.info("MySQL setup finished");
} }
@ -728,6 +733,20 @@ public class MySQL implements DataSource {
return players; 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 { private PlayerAuth buildAuthFromResultSet(ResultSet row) throws SQLException {
String salt = col.SALT.isEmpty() ? null : row.getString(col.SALT); String salt = col.SALT.isEmpty() ? null : row.getString(col.SALT);
int group = col.GROUP.isEmpty() ? -1 : row.getInt(col.GROUP); int group = col.GROUP.isEmpty() ? -1 : row.getInt(col.GROUP);
@ -735,6 +754,7 @@ public class MySQL implements DataSource {
.name(row.getString(col.NAME)) .name(row.getString(col.NAME))
.realName(row.getString(col.REAL_NAME)) .realName(row.getString(col.REAL_NAME))
.password(row.getString(col.PASSWORD), salt) .password(row.getString(col.PASSWORD), salt)
.totpKey(row.getString(col.TOTP_KEY))
.lastLogin(getNullableLong(row, col.LAST_LOGIN)) .lastLogin(getNullableLong(row, col.LAST_LOGIN))
.lastIp(row.getString(col.LAST_IP)) .lastIp(row.getString(col.LAST_IP))
.email(row.getString(col.EMAIL)) .email(row.getString(col.EMAIL))

View File

@ -171,6 +171,11 @@ public class SQLite implements DataSource {
st.executeUpdate("ALTER TABLE " + tableName st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.HAS_SESSION + " INT NOT NULL DEFAULT '0';"); + " 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"); ConsoleLogger.info("SQLite Setup finished");
} }
@ -654,6 +659,21 @@ public class SQLite implements DataSource {
return players; 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 { private PlayerAuth buildAuthFromResultSet(ResultSet row) throws SQLException {
String salt = !col.SALT.isEmpty() ? row.getString(col.SALT) : null; String salt = !col.SALT.isEmpty() ? row.getString(col.SALT) : null;
@ -662,6 +682,7 @@ public class SQLite implements DataSource {
.email(row.getString(col.EMAIL)) .email(row.getString(col.EMAIL))
.realName(row.getString(col.REAL_NAME)) .realName(row.getString(col.REAL_NAME))
.password(row.getString(col.PASSWORD), salt) .password(row.getString(col.PASSWORD), salt)
.totpKey(row.getString(col.TOTP_KEY))
.lastLogin(getNullableLong(row, col.LAST_LOGIN)) .lastLogin(getNullableLong(row, col.LAST_LOGIN))
.lastIp(row.getString(col.LAST_IP)) .lastIp(row.getString(col.LAST_IP))
.registrationDate(row.getLong(col.REGISTRATION_DATE)) .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 = public static final Property<String> MYSQL_COL_HASSESSION =
newProperty("DataSource.mySQLColumnHasSession", "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") @Comment("Column for storing the player's last IP")
public static final Property<String> MYSQL_COL_LAST_IP = public static final Property<String> MYSQL_COL_LAST_IP =
newProperty("DataSource.mySQLColumnIp", "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, hasRegistrationInfo("127.0.4.22", 1436778723L));
assertThat(bobbyAuth.getLastLogin(), equalTo(1449136800L)); assertThat(bobbyAuth.getLastLogin(), equalTo(1449136800L));
assertThat(bobbyAuth.getPassword(), equalToHash("$SHA$11aa0706173d7272$dbba966")); 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, 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, hasAuthLocation(124.1, 76.3, -127.8, "nether", 0.23f, 4.88f));
assertThat(userAuth, hasRegistrationInfo(null, 0)); assertThat(userAuth, hasRegistrationInfo(null, 0));
assertThat(userAuth.getLastLogin(), equalTo(1453242857L)); assertThat(userAuth.getLastLogin(), equalTo(1453242857L));
assertThat(userAuth.getPassword(), equalToHash("b28c32f624a4eb161d6adc9acb5bfc5b", "f750ba32")); assertThat(userAuth.getPassword(), equalToHash("b28c32f624a4eb161d6adc9acb5bfc5b", "f750ba32"));
assertThat(userAuth.getTotpKey(), nullValue());
} }
@Test @Test
@ -494,4 +496,33 @@ public abstract class AbstractDataSourceIntegrationTest {
contains("user24", "user20", "user22", "user29", "user28", contains("user24", "user20", "user22", "user29", "user28",
"user16", "user18", "user12", "user14", "user11")); "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, id INTEGER AUTO_INCREMENT,
username VARCHAR(255) NOT NULL UNIQUE, username VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL, password VARCHAR(255) NOT NULL,
totp VARCHAR(16),
ip VARCHAR(40), ip VARCHAR(40),
lastlogin BIGINT, lastlogin BIGINT,
regdate BIGINT NOT NULL, regdate BIGINT NOT NULL,
@ -22,7 +23,7 @@ CREATE TABLE authme (
CONSTRAINT table_const_prim PRIMARY KEY (id) 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) 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'); 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) 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); 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);