|
|
|
@@ -4,8 +4,6 @@ import com.google.gson.Gson;
|
|
|
|
|
import com.google.gson.GsonBuilder;
|
|
|
|
|
import com.google.gson.reflect.TypeToken;
|
|
|
|
|
import net.minecraft.network.chat.Component;
|
|
|
|
|
import net.minecraft.network.protocol.game.ClientboundSetSubtitleTextPacket;
|
|
|
|
|
import net.minecraft.network.protocol.game.ClientboundSetTitleTextPacket;
|
|
|
|
|
import net.minecraft.server.MinecraftServer;
|
|
|
|
|
import net.minecraft.server.level.ServerPlayer;
|
|
|
|
|
|
|
|
|
@@ -22,6 +20,7 @@ import java.nio.file.Files;
|
|
|
|
|
import java.security.MessageDigest;
|
|
|
|
|
import java.security.SecureRandom;
|
|
|
|
|
import java.time.Duration;
|
|
|
|
|
import java.time.Instant;
|
|
|
|
|
import java.util.Base64;
|
|
|
|
|
import java.util.HashMap;
|
|
|
|
|
import java.util.Locale;
|
|
|
|
@@ -33,6 +32,10 @@ public final class AuthManager {
|
|
|
|
|
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
|
|
|
|
private static final SecureRandom RANDOM = new SecureRandom();
|
|
|
|
|
private static final HttpClient HTTP = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(4)).build();
|
|
|
|
|
private static final String DIALOG_RULES = "dartokloning:auth_rules";
|
|
|
|
|
private static final String DIALOG_REGISTER = "dartokloning:auth_register";
|
|
|
|
|
private static final String DIALOG_LOGIN = "dartokloning:auth_login";
|
|
|
|
|
|
|
|
|
|
private final Map<String, Account> accounts = new HashMap<>();
|
|
|
|
|
private final Map<UUID, Session> sessions = new ConcurrentHashMap<>();
|
|
|
|
|
private final Map<String, PremiumCache> premiumCache = new ConcurrentHashMap<>();
|
|
|
|
@@ -46,13 +49,21 @@ public final class AuthManager {
|
|
|
|
|
AuthConfig c = GSON.fromJson(r, AuthConfig.class);
|
|
|
|
|
if (c != null) config = c;
|
|
|
|
|
}
|
|
|
|
|
} else saveConfig();
|
|
|
|
|
}
|
|
|
|
|
saveConfig(); // writes newly-added defaults/fields during upgrades
|
|
|
|
|
|
|
|
|
|
if (Files.exists(ConfigPaths.AUTH_ACCOUNTS)) {
|
|
|
|
|
try (Reader r = Files.newBufferedReader(ConfigPaths.AUTH_ACCOUNTS)) {
|
|
|
|
|
Map<String, Account> loaded = GSON.fromJson(r, new TypeToken<Map<String, Account>>(){}.getType());
|
|
|
|
|
if (loaded != null) { accounts.clear(); accounts.putAll(loaded); }
|
|
|
|
|
if (loaded != null) {
|
|
|
|
|
accounts.clear();
|
|
|
|
|
accounts.putAll(loaded);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else saveAccounts();
|
|
|
|
|
} else {
|
|
|
|
|
saveAccounts();
|
|
|
|
|
}
|
|
|
|
|
migrateAccounts();
|
|
|
|
|
return true;
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
LobbySystemsMod.LOGGER.error("Failed to load Lobby Systems Auth", e);
|
|
|
|
@@ -62,7 +73,56 @@ public final class AuthManager {
|
|
|
|
|
|
|
|
|
|
public AuthConfig config() { return config; }
|
|
|
|
|
|
|
|
|
|
/** Called from the login mixin. */
|
|
|
|
|
/** Re-applies the freshly loaded auth configuration to players already online. */
|
|
|
|
|
public void applyReload(MinecraftServer server) {
|
|
|
|
|
premiumCache.clear();
|
|
|
|
|
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
|
|
|
|
|
if (!config.enabled) {
|
|
|
|
|
Session existing = sessions.get(player.getUUID());
|
|
|
|
|
if (existing != null) {
|
|
|
|
|
existing.authenticated = true;
|
|
|
|
|
existing.stage = Stage.AUTHENTICATED;
|
|
|
|
|
clearDialog(player);
|
|
|
|
|
}
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Session s = sessions.get(player.getUUID());
|
|
|
|
|
if (s == null) continue;
|
|
|
|
|
Account account = account(player.getName().getString());
|
|
|
|
|
|
|
|
|
|
// A rules-version bump is intentionally live: online players must accept
|
|
|
|
|
// the new version before continuing.
|
|
|
|
|
if (needsRules(account)) {
|
|
|
|
|
s.authenticated = false;
|
|
|
|
|
s.stage = Stage.RULES;
|
|
|
|
|
s.joinedAt = System.currentTimeMillis();
|
|
|
|
|
s.attempts = 0;
|
|
|
|
|
showDialog(player, DIALOG_RULES);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// false means premium accounts are satisfied by Mojang authentication.
|
|
|
|
|
if (s.premium && !config.requireLoginEveryJoin) {
|
|
|
|
|
authenticate(player);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// true makes the Lobby Systems password an additional layer for premium
|
|
|
|
|
// accounts, and local accounts always require it on each connection.
|
|
|
|
|
if (s.premium && config.requireLoginEveryJoin && s.authenticated) {
|
|
|
|
|
s.authenticated = false;
|
|
|
|
|
s.joinedAt = System.currentTimeMillis();
|
|
|
|
|
s.attempts = 0;
|
|
|
|
|
}
|
|
|
|
|
if (!s.authenticated) {
|
|
|
|
|
s.stage = isRegistered(player.getName().getString()) ? Stage.LOGIN : Stage.REGISTER;
|
|
|
|
|
showCurrentDialog(player, s);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Called from the login mixin before the player enters play state. */
|
|
|
|
|
public LoginRoute resolveLoginRoute(String username) {
|
|
|
|
|
if (!config.enabled || !config.premiumAutoLogin) return LoginRoute.PREMIUM;
|
|
|
|
|
PremiumResult result = premiumStatus(username);
|
|
|
|
@@ -74,7 +134,7 @@ public final class AuthManager {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public PremiumResult premiumStatus(String username) {
|
|
|
|
|
String key = username.toLowerCase(Locale.ROOT);
|
|
|
|
|
String key = key(username);
|
|
|
|
|
PremiumCache cached = premiumCache.get(key);
|
|
|
|
|
long now = System.currentTimeMillis();
|
|
|
|
|
if (cached != null && now - cached.checkedAt < 86_400_000L) return cached.result;
|
|
|
|
@@ -111,20 +171,41 @@ public final class AuthManager {
|
|
|
|
|
if (!config.enabled) return;
|
|
|
|
|
String name = player.getName().getString();
|
|
|
|
|
boolean local = LobbySystemsMod.consumeLocalLogin(name);
|
|
|
|
|
if (!local) {
|
|
|
|
|
sessions.put(player.getUUID(), new Session(true, true, 0, System.currentTimeMillis(), player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot()));
|
|
|
|
|
player.sendSystemMessage(Component.literal("§aLobby Systems: Premium account verified. Auto-login successful."));
|
|
|
|
|
boolean premium = !local;
|
|
|
|
|
Session s = new Session(false, premium, 0, System.currentTimeMillis(), player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot());
|
|
|
|
|
sessions.put(player.getUUID(), s);
|
|
|
|
|
|
|
|
|
|
Account account = account(name);
|
|
|
|
|
if (premium) {
|
|
|
|
|
ensurePremiumMetadata(player, account);
|
|
|
|
|
account = account(name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (needsRules(account)) {
|
|
|
|
|
s.stage = Stage.RULES;
|
|
|
|
|
showDialog(player, DIALOG_RULES);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
Session s = new Session(false, false, 0, System.currentTimeMillis(), player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot());
|
|
|
|
|
sessions.put(player.getUUID(), s);
|
|
|
|
|
sendPrompt(player, true);
|
|
|
|
|
|
|
|
|
|
continueAfterRules(player);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void onDisconnect(UUID uuid) { sessions.remove(uuid); }
|
|
|
|
|
public boolean isAuthenticated(ServerPlayer player) { Session s = sessions.get(player.getUUID()); return !config.enabled || (s != null && s.authenticated); }
|
|
|
|
|
public boolean isPremium(ServerPlayer player) { Session s = sessions.get(player.getUUID()); return s != null && s.premium; }
|
|
|
|
|
public boolean isRegistered(String username) { synchronized (accounts) { return accounts.containsKey(username.toLowerCase(Locale.ROOT)); } }
|
|
|
|
|
|
|
|
|
|
public boolean isAuthenticated(ServerPlayer player) {
|
|
|
|
|
Session s = sessions.get(player.getUUID());
|
|
|
|
|
return !config.enabled || (s != null && s.authenticated);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public boolean isPremium(ServerPlayer player) {
|
|
|
|
|
Session s = sessions.get(player.getUUID());
|
|
|
|
|
return s != null && s.premium;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public boolean isRegistered(String username) {
|
|
|
|
|
Account a = account(username);
|
|
|
|
|
return a != null && a.hasPassword();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void tick(MinecraftServer server) {
|
|
|
|
|
if (!config.enabled) return;
|
|
|
|
@@ -136,84 +217,198 @@ public final class AuthManager {
|
|
|
|
|
player.connection.disconnect(Component.literal("Authentication timed out. Please reconnect and try again."));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
// Server-authoritative freeze. This also prevents walking into Lobby portals before authentication.
|
|
|
|
|
// Keep the player at the safe Lobby authentication position. World safety is
|
|
|
|
|
// intentionally handled by the configured Lobby dimension itself.
|
|
|
|
|
if (player.distanceToSqr(s.x, s.y, s.z) > 0.01) {
|
|
|
|
|
player.connection.teleport(s.x, s.y, s.z, s.yaw, s.pitch);
|
|
|
|
|
}
|
|
|
|
|
if (now - s.lastReminder >= config.reminderIntervalSeconds * 1000L) {
|
|
|
|
|
s.lastReminder = now;
|
|
|
|
|
sendPrompt(player, false);
|
|
|
|
|
showCurrentDialog(player, s);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public int register(ServerPlayer player, String password, String confirm) {
|
|
|
|
|
if (!config.enabled) return fail(player, "§cAuthentication is disabled.");
|
|
|
|
|
/** Called by the rules dialog's command-template action. */
|
|
|
|
|
public int acceptRules(ServerPlayer player, boolean accepted) {
|
|
|
|
|
Session s = sessions.get(player.getUUID());
|
|
|
|
|
if (s != null && s.premium) return fail(player, "§ePremium accounts do not need local registration.");
|
|
|
|
|
if (isAuthenticated(player)) return fail(player, "§eYou are already authenticated.");
|
|
|
|
|
String name = player.getName().getString();
|
|
|
|
|
PremiumResult premium = premiumStatus(name);
|
|
|
|
|
if (premium != PremiumResult.NOT_PREMIUM) return fail(player, premium == PremiumResult.PREMIUM ? "§cThis username belongs to a premium Minecraft account." : "§cPremium username verification is temporarily unavailable. Try again later.");
|
|
|
|
|
if (!password.equals(confirm)) return fail(player, "§cPasswords do not match.");
|
|
|
|
|
if (password.length() < config.minimumPasswordLength) return fail(player, "§cPassword must be at least " + config.minimumPasswordLength + " characters.");
|
|
|
|
|
if (s == null || s.authenticated || s.stage != Stage.RULES) return 0;
|
|
|
|
|
if (!accepted) {
|
|
|
|
|
player.sendSystemMessage(Component.literal("§cYou must accept the server rules to continue."));
|
|
|
|
|
showDialog(player, DIALOG_RULES);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
synchronized (accounts) {
|
|
|
|
|
String key = name.toLowerCase(Locale.ROOT);
|
|
|
|
|
if (accounts.containsKey(key)) return fail(player, "§cThis local account is already registered. Use /login <password>.");
|
|
|
|
|
accounts.put(key, hash(password));
|
|
|
|
|
String name = player.getName().getString();
|
|
|
|
|
Account a = accounts.computeIfAbsent(key(name), ignored -> Account.metadataOnly(s.premium));
|
|
|
|
|
a.accountType = s.premium ? "PREMIUM" : "LOCAL";
|
|
|
|
|
if (s.premium) a.officialUuid = player.getUUID().toString();
|
|
|
|
|
a.rulesVersionAccepted = config.rulesVersion;
|
|
|
|
|
a.rulesAcceptedAt = Instant.now().toString();
|
|
|
|
|
saveAccounts();
|
|
|
|
|
}
|
|
|
|
|
authenticate(player, false);
|
|
|
|
|
continueAfterRules(player);
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public int register(ServerPlayer player, String password, String confirm) {
|
|
|
|
|
if (!config.enabled) return failAndReopen(player, "§cAuthentication is disabled.", DIALOG_REGISTER);
|
|
|
|
|
Session s = sessions.get(player.getUUID());
|
|
|
|
|
if (s == null || s.authenticated) return failAndReopen(player, "§eYou are already authenticated.", DIALOG_REGISTER);
|
|
|
|
|
if (s.stage == Stage.RULES) return failAndReopen(player, "§cPlease accept the server rules first.", DIALOG_RULES);
|
|
|
|
|
if (s.premium && !config.requireLoginEveryJoin) return fail(player, "§ePremium accounts use Mojang authentication and do not need a local password.");
|
|
|
|
|
|
|
|
|
|
String name = player.getName().getString();
|
|
|
|
|
if (!s.premium) {
|
|
|
|
|
PremiumResult premium = premiumStatus(name);
|
|
|
|
|
if (premium != PremiumResult.NOT_PREMIUM) {
|
|
|
|
|
return failAndReopen(player,
|
|
|
|
|
premium == PremiumResult.PREMIUM ? "§cThis username belongs to a premium Minecraft account." : "§cPremium username verification is temporarily unavailable. Try again later.",
|
|
|
|
|
DIALOG_REGISTER);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!password.equals(confirm)) return failAndReopen(player, "§cPasswords do not match.", DIALOG_REGISTER);
|
|
|
|
|
if (password.length() < config.minimumPasswordLength) return failAndReopen(player, "§cPassword must be at least " + config.minimumPasswordLength + " characters.", DIALOG_REGISTER);
|
|
|
|
|
|
|
|
|
|
synchronized (accounts) {
|
|
|
|
|
String k = key(name);
|
|
|
|
|
Account existing = accounts.get(k);
|
|
|
|
|
if (existing != null && existing.hasPassword()) return failAndReopen(player, "§cThis account is already registered. Please sign in.", DIALOG_LOGIN);
|
|
|
|
|
Account hashed = hash(password);
|
|
|
|
|
if (existing != null) {
|
|
|
|
|
hashed.accountType = existing.accountType;
|
|
|
|
|
hashed.officialUuid = existing.officialUuid;
|
|
|
|
|
hashed.rulesVersionAccepted = existing.rulesVersionAccepted;
|
|
|
|
|
hashed.rulesAcceptedAt = existing.rulesAcceptedAt;
|
|
|
|
|
} else {
|
|
|
|
|
hashed.accountType = s.premium ? "PREMIUM" : "LOCAL";
|
|
|
|
|
hashed.officialUuid = s.premium ? player.getUUID().toString() : null;
|
|
|
|
|
}
|
|
|
|
|
accounts.put(k, hashed);
|
|
|
|
|
saveAccounts();
|
|
|
|
|
}
|
|
|
|
|
authenticate(player);
|
|
|
|
|
player.sendSystemMessage(Component.literal("§aRegistration successful. You are now logged in."));
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public int login(ServerPlayer player, String password) {
|
|
|
|
|
Session s = sessions.get(player.getUUID());
|
|
|
|
|
if (s != null && s.premium) return fail(player, "§ePremium account is already authenticated.");
|
|
|
|
|
if (isAuthenticated(player)) return fail(player, "§eYou are already authenticated.");
|
|
|
|
|
Account account;
|
|
|
|
|
synchronized (accounts) { account = accounts.get(player.getName().getString().toLowerCase(Locale.ROOT)); }
|
|
|
|
|
if (account == null) return fail(player, "§cAccount not registered. Use /register <password> <password>.");
|
|
|
|
|
if (s == null || s.authenticated) return fail(player, "§eYou are already authenticated.");
|
|
|
|
|
if (s.stage == Stage.RULES) return failAndReopen(player, "§cPlease accept the server rules first.", DIALOG_RULES);
|
|
|
|
|
|
|
|
|
|
Account account = account(player.getName().getString());
|
|
|
|
|
if (account == null || !account.hasPassword()) return failAndReopen(player, "§cAccount not registered. Please create a password first.", DIALOG_REGISTER);
|
|
|
|
|
if (!verify(password, account)) {
|
|
|
|
|
if (s != null && ++s.attempts >= config.maxLoginAttempts) {
|
|
|
|
|
if (++s.attempts >= config.maxLoginAttempts) {
|
|
|
|
|
player.connection.disconnect(Component.literal("Too many failed login attempts."));
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
return fail(player, "§cIncorrect password.");
|
|
|
|
|
int remaining = Math.max(0, config.maxLoginAttempts - s.attempts);
|
|
|
|
|
return failAndReopen(player, "§cIncorrect password. §7" + remaining + " attempt(s) remaining.", DIALOG_LOGIN);
|
|
|
|
|
}
|
|
|
|
|
authenticate(player, false);
|
|
|
|
|
authenticate(player);
|
|
|
|
|
player.sendSystemMessage(Component.literal("§aLogin successful. Welcome back!"));
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void authenticate(ServerPlayer player, boolean premium) {
|
|
|
|
|
Session old = sessions.get(player.getUUID());
|
|
|
|
|
sessions.put(player.getUUID(), new Session(true, premium, 0, old == null ? System.currentTimeMillis() : old.joinedAt,
|
|
|
|
|
player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot()));
|
|
|
|
|
player.connection.send(new ClientboundSetTitleTextPacket(Component.literal("§aAUTHENTICATED")));
|
|
|
|
|
player.connection.send(new ClientboundSetSubtitleTextPacket(Component.literal("§fWelcome, " + player.getName().getString() + "!")));
|
|
|
|
|
private void continueAfterRules(ServerPlayer player) {
|
|
|
|
|
Session s = sessions.get(player.getUUID());
|
|
|
|
|
if (s == null) return;
|
|
|
|
|
|
|
|
|
|
if (s.premium && !config.requireLoginEveryJoin) {
|
|
|
|
|
authenticate(player);
|
|
|
|
|
player.sendSystemMessage(Component.literal("§aLobby Systems: Premium account verified. Auto-login successful."));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isRegistered(player.getName().getString())) {
|
|
|
|
|
s.stage = Stage.LOGIN;
|
|
|
|
|
showDialog(player, DIALOG_LOGIN);
|
|
|
|
|
} else {
|
|
|
|
|
s.stage = Stage.REGISTER;
|
|
|
|
|
showDialog(player, DIALOG_REGISTER);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void sendPrompt(ServerPlayer player, boolean chat) {
|
|
|
|
|
boolean registered = isRegistered(player.getName().getString());
|
|
|
|
|
String title = registered ? "§eLOGIN REQUIRED" : "§bREGISTER REQUIRED";
|
|
|
|
|
String command = registered ? "/login <password>" : "/register <password> <password>";
|
|
|
|
|
player.connection.send(new ClientboundSetTitleTextPacket(Component.literal(title)));
|
|
|
|
|
player.connection.send(new ClientboundSetSubtitleTextPacket(Component.literal("§f" + command)));
|
|
|
|
|
if (chat) player.sendSystemMessage(Component.literal("§6[Lobby Systems Auth] §f" + (registered ? "Please login: §e" : "Please register: §b") + command));
|
|
|
|
|
private void authenticate(ServerPlayer player) {
|
|
|
|
|
Session s = sessions.get(player.getUUID());
|
|
|
|
|
if (s == null) return;
|
|
|
|
|
s.authenticated = true;
|
|
|
|
|
s.stage = Stage.AUTHENTICATED;
|
|
|
|
|
s.attempts = 0;
|
|
|
|
|
clearDialog(player);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static int fail(ServerPlayer p, String msg) { p.sendSystemMessage(Component.literal(msg)); return 0; }
|
|
|
|
|
private boolean needsRules(Account account) {
|
|
|
|
|
return config.rulesEnabled && (account == null || account.rulesVersionAccepted < config.rulesVersion);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ensurePremiumMetadata(ServerPlayer player, Account existing) {
|
|
|
|
|
synchronized (accounts) {
|
|
|
|
|
String k = key(player.getName().getString());
|
|
|
|
|
Account a = existing;
|
|
|
|
|
if (a == null) {
|
|
|
|
|
a = Account.metadataOnly(true);
|
|
|
|
|
accounts.put(k, a);
|
|
|
|
|
}
|
|
|
|
|
a.accountType = "PREMIUM";
|
|
|
|
|
a.officialUuid = player.getUUID().toString();
|
|
|
|
|
saveAccounts();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void showCurrentDialog(ServerPlayer player, Session s) {
|
|
|
|
|
switch (s.stage) {
|
|
|
|
|
case RULES -> showDialog(player, DIALOG_RULES);
|
|
|
|
|
case REGISTER -> showDialog(player, DIALOG_REGISTER);
|
|
|
|
|
case LOGIN -> showDialog(player, DIALOG_LOGIN);
|
|
|
|
|
default -> { }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void showDialog(ServerPlayer player, String id) {
|
|
|
|
|
try {
|
|
|
|
|
String command = "dialog show " + player.getGameProfile().name() + " " + id;
|
|
|
|
|
player.level().getServer().getCommands().getDispatcher().execute(command, player.level().getServer().createCommandSourceStack());
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
LobbySystemsMod.LOGGER.error("Failed to show auth dialog {} to {}", id, player.getName().getString(), e);
|
|
|
|
|
player.sendSystemMessage(Component.literal("§cAuthentication UI could not be opened. Please reconnect or contact staff."));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void clearDialog(ServerPlayer player) {
|
|
|
|
|
try {
|
|
|
|
|
String command = "dialog clear " + player.getGameProfile().name();
|
|
|
|
|
player.level().getServer().getCommands().getDispatcher().execute(command, player.level().getServer().createCommandSourceStack());
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
LobbySystemsMod.LOGGER.debug("Failed to clear auth dialog for {}: {}", player.getName().getString(), e.toString());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private int failAndReopen(ServerPlayer player, String message, String dialog) {
|
|
|
|
|
player.sendSystemMessage(Component.literal(message));
|
|
|
|
|
showDialog(player, dialog);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static int fail(ServerPlayer p, String msg) {
|
|
|
|
|
p.sendSystemMessage(Component.literal(msg));
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Account hash(String password) {
|
|
|
|
|
try {
|
|
|
|
|
byte[] salt = new byte[16]; RANDOM.nextBytes(salt);
|
|
|
|
|
byte[] salt = new byte[16];
|
|
|
|
|
RANDOM.nextBytes(salt);
|
|
|
|
|
PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, config.pbkdf2Iterations, 256);
|
|
|
|
|
byte[] digest = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(spec).getEncoded();
|
|
|
|
|
spec.clearPassword();
|
|
|
|
|
return new Account(Base64.getEncoder().encodeToString(salt), Base64.getEncoder().encodeToString(digest), config.pbkdf2Iterations);
|
|
|
|
|
} catch (Exception e) { throw new IllegalStateException("Unable to hash password", e); }
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
throw new IllegalStateException("Unable to hash password", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private boolean verify(String password, Account a) {
|
|
|
|
@@ -223,15 +418,98 @@ public final class AuthManager {
|
|
|
|
|
byte[] actual = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(spec).getEncoded();
|
|
|
|
|
spec.clearPassword();
|
|
|
|
|
return MessageDigest.isEqual(actual, Base64.getDecoder().decode(a.hash));
|
|
|
|
|
} catch (Exception e) { return false; }
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void saveConfig() throws Exception { try (Writer w = Files.newBufferedWriter(ConfigPaths.AUTH_CONFIG)) { GSON.toJson(config, w); } }
|
|
|
|
|
private void saveAccounts() { try { Files.createDirectories(ConfigPaths.AUTH_DIRECTORY); try (Writer w = Files.newBufferedWriter(ConfigPaths.AUTH_ACCOUNTS)) { GSON.toJson(accounts, w); } } catch (Exception e) { LobbySystemsMod.LOGGER.error("Failed saving auth accounts", e); } }
|
|
|
|
|
private Account account(String username) {
|
|
|
|
|
synchronized (accounts) {
|
|
|
|
|
return accounts.get(key(username));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void migrateAccounts() {
|
|
|
|
|
boolean changed = false;
|
|
|
|
|
synchronized (accounts) {
|
|
|
|
|
for (Account a : accounts.values()) {
|
|
|
|
|
if (a.accountType == null || a.accountType.isBlank()) {
|
|
|
|
|
a.accountType = "LOCAL";
|
|
|
|
|
changed = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (changed) saveAccounts();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static String key(String username) { return username.toLowerCase(Locale.ROOT); }
|
|
|
|
|
|
|
|
|
|
private void saveConfig() throws Exception {
|
|
|
|
|
try (Writer w = Files.newBufferedWriter(ConfigPaths.AUTH_CONFIG)) { GSON.toJson(config, w); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void saveAccounts() {
|
|
|
|
|
try {
|
|
|
|
|
Files.createDirectories(ConfigPaths.AUTH_DIRECTORY);
|
|
|
|
|
try (Writer w = Files.newBufferedWriter(ConfigPaths.AUTH_ACCOUNTS)) { GSON.toJson(accounts, w); }
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
LobbySystemsMod.LOGGER.error("Failed saving auth accounts", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public enum PremiumResult { PREMIUM, NOT_PREMIUM, UNKNOWN }
|
|
|
|
|
public enum LoginRoute { PREMIUM, LOCAL, UNKNOWN }
|
|
|
|
|
private enum Stage { RULES, REGISTER, LOGIN, AUTHENTICATED }
|
|
|
|
|
private record PremiumCache(PremiumResult result, long checkedAt) {}
|
|
|
|
|
private static final class Account { String salt; String hash; int iterations; Account(String salt,String hash,int iterations){this.salt=salt;this.hash=hash;this.iterations=iterations;} }
|
|
|
|
|
private static final class Session { boolean authenticated; boolean premium; int attempts; long joinedAt; long lastReminder; double x,y,z; float yaw,pitch; Session(boolean a,boolean p,int at,long j,double x,double y,double z,float yaw,float pitch){authenticated=a;premium=p;attempts=at;joinedAt=j;lastReminder=0;this.x=x;this.y=y;this.z=z;this.yaw=yaw;this.pitch=pitch;} }
|
|
|
|
|
|
|
|
|
|
private static final class Account {
|
|
|
|
|
String salt;
|
|
|
|
|
String hash;
|
|
|
|
|
int iterations;
|
|
|
|
|
String accountType;
|
|
|
|
|
String officialUuid;
|
|
|
|
|
int rulesVersionAccepted;
|
|
|
|
|
String rulesAcceptedAt;
|
|
|
|
|
|
|
|
|
|
Account(String salt, String hash, int iterations) {
|
|
|
|
|
this.salt = salt;
|
|
|
|
|
this.hash = hash;
|
|
|
|
|
this.iterations = iterations;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static Account metadataOnly(boolean premium) {
|
|
|
|
|
Account a = new Account(null, null, 0);
|
|
|
|
|
a.accountType = premium ? "PREMIUM" : "LOCAL";
|
|
|
|
|
return a;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
boolean hasPassword() {
|
|
|
|
|
return salt != null && !salt.isBlank() && hash != null && !hash.isBlank() && iterations > 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static final class Session {
|
|
|
|
|
boolean authenticated;
|
|
|
|
|
final boolean premium;
|
|
|
|
|
int attempts;
|
|
|
|
|
long joinedAt;
|
|
|
|
|
long lastReminder;
|
|
|
|
|
final double x, y, z;
|
|
|
|
|
final float yaw, pitch;
|
|
|
|
|
Stage stage = Stage.RULES;
|
|
|
|
|
|
|
|
|
|
Session(boolean authenticated, boolean premium, int attempts, long joinedAt,
|
|
|
|
|
double x, double y, double z, float yaw, float pitch) {
|
|
|
|
|
this.authenticated = authenticated;
|
|
|
|
|
this.premium = premium;
|
|
|
|
|
this.attempts = attempts;
|
|
|
|
|
this.joinedAt = joinedAt;
|
|
|
|
|
this.lastReminder = 0;
|
|
|
|
|
this.x = x;
|
|
|
|
|
this.y = y;
|
|
|
|
|
this.z = z;
|
|
|
|
|
this.yaw = yaw;
|
|
|
|
|
this.pitch = pitch;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|