#1219 Make 2FA tokens one-use only (#1848)

- Make TotpAuthenticator keep track of the used codes and reject any known ones
This commit is contained in:
ljacqu 2019-07-06 18:26:43 +02:00 committed by Gabriele C
parent e2c2e8bead
commit 210a4f61cb
4 changed files with 56 additions and 13 deletions

View File

@ -59,7 +59,7 @@ public class GenerateTotpService implements HasCleanup {
*/ */
public boolean isTotpCodeCorrectForGeneratedTotpKey(Player player, String totpCode) { public boolean isTotpCodeCorrectForGeneratedTotpKey(Player player, String totpCode) {
TotpGenerationResult totpDetails = totpKeys.get(player.getName().toLowerCase()); TotpGenerationResult totpDetails = totpKeys.get(player.getName().toLowerCase());
return totpDetails != null && totpAuthenticator.checkCode(totpDetails.getTotpKey(), totpCode); return totpDetails != null && totpAuthenticator.checkCode(player.getName(), totpDetails.getTotpKey(), totpCode);
} }
@Override @Override

View File

@ -1,23 +1,31 @@
package fr.xephi.authme.security.totp; package fr.xephi.authme.security.totp;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Table;
import com.google.common.primitives.Ints; import com.google.common.primitives.Ints;
import com.warrenstrange.googleauth.GoogleAuthenticator; import com.warrenstrange.googleauth.GoogleAuthenticator;
import com.warrenstrange.googleauth.GoogleAuthenticatorKey; import com.warrenstrange.googleauth.GoogleAuthenticatorKey;
import com.warrenstrange.googleauth.GoogleAuthenticatorQRGenerator; import com.warrenstrange.googleauth.GoogleAuthenticatorQRGenerator;
import com.warrenstrange.googleauth.IGoogleAuthenticator; import com.warrenstrange.googleauth.IGoogleAuthenticator;
import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.initialization.HasCleanup;
import fr.xephi.authme.service.BukkitService; import fr.xephi.authme.service.BukkitService;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import javax.inject.Inject; import javax.inject.Inject;
import static fr.xephi.authme.util.Utils.MILLIS_PER_MINUTE;
/** /**
* Provides TOTP functions (wrapping a third-party TOTP implementation). * Provides TOTP functions (wrapping a third-party TOTP implementation).
*/ */
public class TotpAuthenticator { public class TotpAuthenticator implements HasCleanup {
private static final int CODE_RETENTION_MINUTES = 5;
private final IGoogleAuthenticator authenticator; private final IGoogleAuthenticator authenticator;
private final BukkitService bukkitService; private final BukkitService bukkitService;
private final Table<String, Integer, Long> usedCodes = HashBasedTable.create();
@Inject @Inject
TotpAuthenticator(BukkitService bukkitService) { TotpAuthenticator(BukkitService bukkitService) {
@ -33,19 +41,26 @@ public class TotpAuthenticator {
} }
public boolean checkCode(PlayerAuth auth, String totpCode) { public boolean checkCode(PlayerAuth auth, String totpCode) {
return checkCode(auth.getTotpKey(), totpCode); return checkCode(auth.getNickname(), auth.getTotpKey(), totpCode);
} }
/** /**
* Returns whether the given input code matches for the provided TOTP key. * Returns whether the given input code matches for the provided TOTP key.
* *
* @param playerName the player name
* @param totpKey the key to check with * @param totpKey the key to check with
* @param inputCode the input code to verify * @param inputCode the input code to verify
* @return true if code is valid, false otherwise * @return true if code is valid, false otherwise
*/ */
public boolean checkCode(String totpKey, String inputCode) { public boolean checkCode(String playerName, String totpKey, String inputCode) {
String nameLower = playerName.toLowerCase();
Integer totpCode = Ints.tryParse(inputCode); Integer totpCode = Ints.tryParse(inputCode);
return totpCode != null && authenticator.authorize(totpKey, totpCode); if (totpCode != null && !usedCodes.contains(nameLower, totpCode)
&& authenticator.authorize(totpKey, totpCode)) {
usedCodes.put(nameLower, totpCode, System.currentTimeMillis());
return true;
}
return false;
} }
public TotpGenerationResult generateTotpKey(Player player) { public TotpGenerationResult generateTotpKey(Player player) {
@ -55,6 +70,12 @@ public class TotpAuthenticator {
return new TotpGenerationResult(credentials.getKey(), qrCodeUrl); return new TotpGenerationResult(credentials.getKey(), qrCodeUrl);
} }
@Override
public void performCleanup() {
long threshold = System.currentTimeMillis() - CODE_RETENTION_MINUTES * MILLIS_PER_MINUTE;
usedCodes.values().removeIf(value -> value < threshold);
}
public static final class TotpGenerationResult { public static final class TotpGenerationResult {
private final String totpKey; private final String totpKey;
private final String authenticatorQrCodeUrl; private final String authenticatorQrCodeUrl;

View File

@ -70,7 +70,7 @@ public class GenerateTotpServiceTest {
given(totpAuthenticator.generateTotpKey(player)).willReturn(givenGenerationResult); given(totpAuthenticator.generateTotpKey(player)).willReturn(givenGenerationResult);
generateTotpService.generateTotpKey(player); generateTotpService.generateTotpKey(player);
String validCode = "928374"; String validCode = "928374";
given(totpAuthenticator.checkCode(generatedKey, validCode)).willReturn(true); given(totpAuthenticator.checkCode("Aria", generatedKey, validCode)).willReturn(true);
// when // when
boolean invalidCodeResult = generateTotpService.isTotpCodeCorrectForGeneratedTotpKey(player, "000000"); boolean invalidCodeResult = generateTotpService.isTotpCodeCorrectForGeneratedTotpKey(player, "000000");
@ -81,8 +81,8 @@ public class GenerateTotpServiceTest {
assertThat(invalidCodeResult, equalTo(false)); assertThat(invalidCodeResult, equalTo(false));
assertThat(validCodeResult, equalTo(true)); assertThat(validCodeResult, equalTo(true));
assertThat(unknownPlayerResult, equalTo(false)); assertThat(unknownPlayerResult, equalTo(false));
verify(totpAuthenticator).checkCode(generatedKey, "000000"); verify(totpAuthenticator).checkCode("Aria", generatedKey, "000000");
verify(totpAuthenticator).checkCode(generatedKey, validCode); verify(totpAuthenticator).checkCode("Aria", generatedKey, validCode);
} }
@Test @Test

View File

@ -1,9 +1,12 @@
package fr.xephi.authme.security.totp; package fr.xephi.authme.security.totp;
import com.google.common.collect.Table;
import com.warrenstrange.googleauth.IGoogleAuthenticator; import com.warrenstrange.googleauth.IGoogleAuthenticator;
import fr.xephi.authme.ReflectionTestUtils;
import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.security.totp.TotpAuthenticator.TotpGenerationResult; import fr.xephi.authme.security.totp.TotpAuthenticator.TotpGenerationResult;
import fr.xephi.authme.service.BukkitService; import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.util.Utils;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@ -63,24 +66,26 @@ public class TotpAuthenticatorTest {
} }
@Test @Test
public void shouldCheckCode() { public void shouldCheckCodeAndDeclareItValidOnlyOnce() {
// given // given
String secret = "the_secret"; String secret = "the_secret";
int code = 21398; int code = 21398;
given(googleAuthenticator.authorize(secret, code)).willReturn(true); given(googleAuthenticator.authorize(secret, code)).willReturn(true);
// when // when
boolean result = totpAuthenticator.checkCode(secret, Integer.toString(code)); boolean result1 = totpAuthenticator.checkCode("pl", secret, Integer.toString(code));
boolean result2 = totpAuthenticator.checkCode("pl", secret, Integer.toString(code));
// then // then
assertThat(result, equalTo(true)); assertThat(result1, equalTo(true));
assertThat(result2, equalTo(false));
verify(googleAuthenticator).authorize(secret, code); verify(googleAuthenticator).authorize(secret, code);
} }
@Test @Test
public void shouldHandleInvalidNumberInput() { public void shouldHandleInvalidNumberInput() {
// given / when // given / when
boolean result = totpAuthenticator.checkCode("Some_Secret", "123ZZ"); boolean result = totpAuthenticator.checkCode("foo", "Some_Secret", "123ZZ");
// then // then
assertThat(result, equalTo(false)); assertThat(result, equalTo(false));
@ -96,7 +101,7 @@ public class TotpAuthenticatorTest {
.totpKey(totpKey) .totpKey(totpKey)
.build(); .build();
String inputCode = "408435"; String inputCode = "408435";
given(totpAuthenticator.checkCode(totpKey, inputCode)).willReturn(true); given(totpAuthenticator.checkCode("Maya", totpKey, inputCode)).willReturn(true);
// when // when
boolean result = totpAuthenticator.checkCode(auth, inputCode); boolean result = totpAuthenticator.checkCode(auth, inputCode);
@ -106,6 +111,23 @@ public class TotpAuthenticatorTest {
verify(googleAuthenticator).authorize(totpKey, 408435); verify(googleAuthenticator).authorize(totpKey, 408435);
} }
@Test
public void shouldRemoveOldEntries() {
// given
Table<String, Integer, Long> usedCodes = ReflectionTestUtils.getFieldValue(
TotpAuthenticator.class, totpAuthenticator, "usedCodes");
usedCodes.put("bobby", 414213, System.currentTimeMillis());
usedCodes.put("charlie", 732050, System.currentTimeMillis() - 6 * Utils.MILLIS_PER_MINUTE);
usedCodes.put("bobby", 236067, System.currentTimeMillis() - 9 * Utils.MILLIS_PER_MINUTE);
// when
totpAuthenticator.performCleanup();
// then
assertThat(usedCodes.size(), equalTo(1));
assertThat(usedCodes.contains("bobby", 414213), equalTo(true));
}
private final class TotpAuthenticatorTestImpl extends TotpAuthenticator { private final class TotpAuthenticatorTestImpl extends TotpAuthenticator {
TotpAuthenticatorTestImpl(BukkitService bukkitService) { TotpAuthenticatorTestImpl(BukkitService bukkitService) {