[17.3k Lines] Started on the base of two factor authentication

This commit is contained in:
Rainnny7 2021-02-20 19:30:39 -05:00
parent 0c664d2ef1
commit d360fabcc8
11 changed files with 294 additions and 2 deletions

View File

@ -3,4 +3,5 @@ dependencies {
implementation("com.zaxxer:HikariCP:3.4.5")
compileOnly("com.destroystokyo:paperspigot:1.12.2")
implementation("com.github.cryptomorin:XSeries:7.8.0")
implementation("com.warrenstrange:googleauth:1.4.0")
}

View File

@ -117,6 +117,7 @@ public class AccountRepository extends MySQLRepository {
}
Bukkit.getPluginManager().callEvent(new AccountPreLoadEvent(uuid, name, ipAddress));
int finalAccountId = accountId;
query+= AccountManager.MINI_ACCOUNTS.parallelStream().map(miniAccount -> miniAccount.getQuery(finalAccountId, uuid, name, ipAddress, encryptedIpAddress)).collect(Collectors.joining());
if (!query.trim().isEmpty()) {
log.info("Executing mini account tasks (" + AccountManager.MINI_ACCOUNTS.size() + ") for " + name);

View File

@ -2,6 +2,7 @@ package zone.themcgamer.core.account;
import lombok.Getter;
import org.bukkit.plugin.java.JavaPlugin;
import zone.themcgamer.core.account.Account;
import zone.themcgamer.core.module.Module;
import java.sql.ResultSet;

View File

@ -8,6 +8,7 @@ import net.md_5.bungee.api.chat.TextComponent;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.plugin.java.JavaPlugin;
import zone.themcgamer.core.account.Account;
@ -65,8 +66,10 @@ public class ChatManager extends Module {
registerCommand(new EmotesCommand(this));
}
@EventHandler
@EventHandler(priority = EventPriority.HIGHEST)
private void onChat(AsyncPlayerChatEvent event) {
if (event.isCancelled())
return;
Player player = event.getPlayer();
String message = event.getMessage();

View File

@ -38,6 +38,10 @@ public abstract class Module implements Listener {
public void onEnable() {} // Called when the module is enabled
public void onDisable() {} // Called when the module is disabled
public String getName() {
return info.name();
}
public void registerCommand(Object command) {
CommandManager commandManager = getModule(CommandManager.class);
if (commandManager == null)
@ -50,7 +54,7 @@ public abstract class Module implements Listener {
* @param message the message to log
*/
public void log(String message) {
System.out.println(info.name() + " » " + message);
Bukkit.getLogger().info("§b" + info.name() + " §8» §7" + message);
}
/**

View File

@ -20,6 +20,7 @@ import zone.themcgamer.core.plugin.command.PluginsCommand;
import zone.themcgamer.core.server.ServerManager;
import zone.themcgamer.core.task.TaskManager;
import zone.themcgamer.core.traveler.ServerTraveler;
import zone.themcgamer.core.twoFactor.TwoFactorAuthentication;
import zone.themcgamer.core.update.ServerUpdater;
import zone.themcgamer.data.jedis.JedisController;
import zone.themcgamer.data.jedis.data.server.MinecraftServer;
@ -168,6 +169,7 @@ public abstract class MGZPlugin extends JavaPlugin {
commandManager.registerCommand(this, new PluginsCommand());
new CooldownHandler(this);
AccountManager.addMiniAccount(new TwoFactorAuthentication(this, mySQLController));
nametagManager = new NametagManager(this);
accountManager = new AccountManager(this, mySQLController, nametagManager);

View File

@ -0,0 +1,207 @@
package zone.themcgamer.core.twoFactor;
import com.cryptomorin.xseries.XSound;
import com.warrenstrange.googleauth.GoogleAuthenticator;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.FoodLevelChangeEvent;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.player.*;
import org.bukkit.plugin.java.JavaPlugin;
import zone.themcgamer.common.EnumUtils;
import zone.themcgamer.core.account.Account;
import zone.themcgamer.core.account.AccountManager;
import zone.themcgamer.core.account.MiniAccount;
import zone.themcgamer.core.common.Style;
import zone.themcgamer.core.module.ModuleInfo;
import zone.themcgamer.data.Rank;
import zone.themcgamer.data.mysql.MySQLController;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
/**
* @author Braydon
*/
@ModuleInfo(name = "Two Factor")
public class TwoFactorAuthentication extends MiniAccount<TwoFactorClient> {
private final TwoFactorRepository repository;
private final GoogleAuthenticator googleAuthenticator = new GoogleAuthenticator();
private final List<UUID> authenticating = new ArrayList<>();
public TwoFactorAuthentication(JavaPlugin plugin, MySQLController mySQLController) {
super(plugin);
repository = new TwoFactorRepository(mySQLController.getDataSource());
}
@Override
public TwoFactorClient getAccount(int accountId, UUID uuid, String name, String ip, String encryptedIp) {
return new TwoFactorClient();
}
@Override
public String getQuery(int accountId, UUID uuid, String name, String ip, String encryptedIp) {
return "SELECT dbKey, value FROM `twoFactor` WHERE `accountId` = '" + accountId + "';";
}
@Override
public void loadAccount(int accountId, UUID uuid, String name, String ip, String encryptedIp, ResultSet resultSet) throws SQLException {
Optional<TwoFactorClient> optionalTwoFactorClient = lookup(uuid);
if (optionalTwoFactorClient.isEmpty())
return;
TwoFactorClient twoFactorClient = optionalTwoFactorClient.get();
while (resultSet.next()) {
TwoFactorDBKey databaseKey = EnumUtils.fromString(TwoFactorDBKey.class, resultSet.getString("dbKey"));
if (databaseKey == null)
continue;
if (databaseKey == TwoFactorDBKey.SECRET_KEY)
twoFactorClient.setSecretKey(resultSet.getString("value"));
else twoFactorClient.setLastAuthentication(resultSet.getLong("value"));
}
}
@EventHandler(priority = EventPriority.LOWEST)
private void onLogin(PlayerLoginEvent event) {
UUID uuid = event.getPlayer().getUniqueId();
Optional<Account> optionalAccount = AccountManager.fromCache(uuid);
if (optionalAccount.isEmpty())
return;
Account account = optionalAccount.get();
if (!account.hasRank(Rank.HELPER))
return;
Optional<TwoFactorClient> optionalTwoFactorClient = lookup(uuid);
if (optionalTwoFactorClient.isEmpty())
return;
// If the player hasn't setup 2fa, the player hasn't authenticated in 24 hours, or their ip has changed, make them authenticate
if (optionalTwoFactorClient.get().requiresAuthentication() || !account.getLastEncryptedIpAddress().equals(account.getEncryptedIpAddress()))
authenticating.add(uuid);
}
@EventHandler(priority = EventPriority.HIGHEST)
private void onJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
Optional<Account> optionalAccount = AccountManager.fromCache(player.getUniqueId());
if (optionalAccount.isEmpty())
return;
Account account = optionalAccount.get();
if (!account.hasRank(Rank.HELPER))
return;
if (!authenticating.contains(player.getUniqueId())) {
player.sendMessage(Style.main(getName(), "§aAuthenticated!"));
return;
}
Optional<TwoFactorClient> optionalTwoFactorClient = lookup(player.getUniqueId());
if (optionalTwoFactorClient.isEmpty())
return;
TwoFactorClient twoFactorClient = optionalTwoFactorClient.get();
Bukkit.getScheduler().scheduleSyncDelayedTask(getPlugin(), () -> {
if (twoFactorClient.getSecretKey() == null) {
String secretKey = googleAuthenticator.createCredentials().getKey();
twoFactorClient.setSecretKey(secretKey);
// TODO: 2/20/21 generate a qr map and give it to the player so they can scan it on their twoFactor app
player.sendMessage(Style.main(getName(), "Hey §b" + player.getName() + "§7, you have not setup your two factor authentication yet!"));
player.sendMessage(Style.main(getName(), "To begin, open your authentication app of choice and insert the secret key"));
player.sendMessage(Style.main(getName(), "§f" + secretKey));
player.sendMessage(Style.main(getName(), "Once done, type the 6 digit code provided by your authentication app into the chat"));
return;
}
player.sendMessage(Style.main(getName(), "§cYou need to re-authenticate!"));
player.sendMessage(Style.main(getName(), "Type the 6 digit code provided by your authentication app into the chat"));
}, 1L);
}
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
private void onChat(AsyncPlayerChatEvent event) {
Player player = event.getPlayer();
if (!authenticating.contains(player.getUniqueId()))
return;
Optional<Account> optionalAccount = AccountManager.fromCache(player.getUniqueId());
if (optionalAccount.isEmpty())
return;
Optional<TwoFactorClient> optionalTwoFactorClient = lookup(player.getUniqueId());
if (optionalTwoFactorClient.isEmpty())
return;
event.setCancelled(true);
String message = event.getMessage().replaceAll(" ", "");
int code;
try {
code = Integer.parseInt(message);
} catch (NumberFormatException ex) {
player.sendMessage(Style.main(getName(), "§cInvalid authentication code!"));
return;
}
TwoFactorClient twoFactorClient = optionalTwoFactorClient.get();
String secretKey = twoFactorClient.getSecretKey();
if (!googleAuthenticator.authorize(secretKey, code))
player.sendMessage(Style.main(getName(), "§cInvalid authentication code!"));
else {
twoFactorClient.setLastAuthentication(System.currentTimeMillis());
repository.authenticate(optionalAccount.get().getId(), secretKey, twoFactorClient.getLastAuthentication());
authenticating.remove(player.getUniqueId());
player.playSound(player.getEyeLocation(), XSound.ENTITY_PLAYER_LEVELUP.parseSound(), 0.9f, 1f);
player.sendMessage(Style.main(getName(), "§aAuthenticated!"));
}
}
@EventHandler(priority = EventPriority.LOWEST)
private void onCommand(PlayerCommandPreprocessEvent event) {
cancelEvent(event);
}
@EventHandler(priority = EventPriority.LOWEST)
private void onMove(PlayerMoveEvent event) {
Location from = event.getFrom();
Location to = event.getTo();
if (from.getBlockX() != to.getBlockX() || from.getBlockZ() != to.getBlockZ())
cancelEvent(event);
}
@EventHandler(priority = EventPriority.LOWEST)
private void onInventoryClick(InventoryClickEvent event) {
cancelEvent((Player) event.getWhoClicked(), event);
}
@EventHandler(priority = EventPriority.LOWEST)
private void onFoodLevelChange(FoodLevelChangeEvent event) {
cancelEvent(event);
}
@EventHandler(priority = EventPriority.LOWEST)
private void onDamage(EntityDamageEvent event) {
Entity entity = event.getEntity();
if (entity instanceof Player)
cancelEvent((Player) entity, event);
}
@EventHandler(priority = EventPriority.LOWEST)
private void onHeldItemChange(PlayerItemHeldEvent event) {
cancelEvent(event);
}
@EventHandler
private void onQuit(PlayerQuitEvent event) {
authenticating.remove(event.getPlayer().getUniqueId());
}
private void cancelEvent(Cancellable cancellable) {
if (!(cancellable instanceof PlayerEvent))
return;
cancelEvent(((PlayerEvent) cancellable).getPlayer(), cancellable);
}
private void cancelEvent(Player player, Cancellable cancellable) {
if (authenticating.contains(player.getUniqueId()))
cancellable.setCancelled(true);
}
}

View File

@ -0,0 +1,20 @@
package zone.themcgamer.core.twoFactor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.concurrent.TimeUnit;
/**
* @author Braydon
*/
@NoArgsConstructor @Setter @Getter
public class TwoFactorClient {
private String secretKey;
private long lastAuthentication;
public boolean requiresAuthentication() {
return secretKey == null || (System.currentTimeMillis() - lastAuthentication) >= TimeUnit.DAYS.toMillis(1L);
}
}

View File

@ -0,0 +1,8 @@
package zone.themcgamer.core.twoFactor;
/**
* @author Braydon
*/
public enum TwoFactorDBKey {
SECRET_KEY, LAST_AUTHENTICATION
}

View File

@ -0,0 +1,39 @@
package zone.themcgamer.core.twoFactor;
import com.zaxxer.hikari.HikariDataSource;
import zone.themcgamer.data.mysql.data.column.Column;
import zone.themcgamer.data.mysql.data.column.impl.IntegerColumn;
import zone.themcgamer.data.mysql.data.column.impl.LongColumn;
import zone.themcgamer.data.mysql.data.column.impl.StringColumn;
import zone.themcgamer.data.mysql.repository.MySQLRepository;
import java.util.concurrent.CompletableFuture;
/**
* @author Braydon
*/
public class TwoFactorRepository extends MySQLRepository {
private static final String INSERT_TWO_FACTOR = "INSERT INTO `twoFactor` " +
"(`accountId`, `dbKey`, `value`) VALUES " +
"(?, ?, ?) ON DUPLICATE KEY UPDATE `value`=?";
public TwoFactorRepository(HikariDataSource dataSource) {
super(dataSource);
}
public void authenticate(int accountId, String secretKey, long lastAuthentication) {
StringColumn secretKeyColumn = new StringColumn("value", secretKey);
LongColumn lastAuthenticationColumn = new LongColumn("value", lastAuthentication);
CompletableFuture.runAsync(() -> {
for (TwoFactorDBKey dbKey : TwoFactorDBKey.values()) {
Column<?> valueColumn = dbKey == TwoFactorDBKey.SECRET_KEY ? secretKeyColumn : lastAuthenticationColumn;
executeInsert(INSERT_TWO_FACTOR, new Column[] {
new IntegerColumn("accountId", accountId),
new StringColumn("dbKey", dbKey.name()),
valueColumn,
valueColumn
});
}
});
}
}

View File

@ -45,6 +45,12 @@ public class MySQLController {
new LongColumn("lastLogin", false)
}, new String[] { "id" }),
new Table("twoFactor", new Column[] {
new IntegerColumn("accountId", false, false),
new StringColumn("dbKey", 50, false),
new StringColumn("value", 50, false)
}, new String[] { "accountId", "dbKey" }),
new Table("punishments", new Column[] {
new IntegerColumn("id", true, false),
new StringColumn("targetIp", 255, false),