From 0cd06d418f9ccb7aafde62470a85aac18856e984 Mon Sep 17 00:00:00 2001 From: Darto KLoning Date: Fri, 2 Oct 2026 06:22:33 +0700 Subject: [PATCH] fix loom and auth path --- README.md | 11 ++++ build.gradle | 4 ++ gradle.properties | 2 +- .../dartokloning/lobbysystems/AuthConfig.java | 4 +- .../lobbysystems/AuthManager.java | 52 ++++++++++++------- .../lobbysystems/LobbySystemsMod.java | 13 ++++- .../ServerLoginPacketListenerImplMixin.java | 41 ++++++++++++--- src/main/resources/fabric.mod.json | 1 + .../resources/lobby-systems.accesswidener | 4 ++ 9 files changed, 103 insertions(+), 29 deletions(-) create mode 100644 src/main/resources/lobby-systems.accesswidener diff --git a/README.md b/README.md index 679bcd1..00cea01 100644 --- a/README.md +++ b/README.md @@ -477,3 +477,14 @@ Lobby Systems is **proprietary software — All Rights Reserved**. Public access - EasyAuth is intentionally marked incompatible while native Lobby Systems Auth is enabled. For the intended hybrid mode, keep `online-mode=true` in `server.properties` and remove EasyAuth before testing. + +## v1.7.1 +- Fixed hybrid `online-mode=true` login: confirmed non-premium players now receive a deterministic offline GameProfile and advance through the login state machine instead of falling into vanilla `Invalid session`. +- Premium username lookup now prefers the modern `api.minecraftservices.com` public profile endpoint and falls back to the legacy Mojang endpoint. +- Premium/unknown identities still use vanilla authentication; lookup failure never silently grants a local identity. +- Local/cracked players remain password-gated after join. `requireLoginEveryJoin` defaults to `true`; session bypass is intentionally not enabled yet, so changing it currently does not weaken authentication. +- `enforce-secure-profile=true` is intentionally left supported; cracked chat will be validated in server testing before any secure-chat compatibility override is added. + +## v1.7.2 +- Fixed the authentication access widener so the private Minecraft login state type is widened during compilation, not only declared in mod metadata. +- Added Loom `accessWidenerPath` configuration for compile-time access widening. diff --git a/build.gradle b/build.gradle index dbbab67..4f5adcf 100644 --- a/build.gradle +++ b/build.gradle @@ -8,6 +8,10 @@ group = project.maven_group base { archivesName = project.archives_base_name } +loom { + accessWidenerPath = file("src/main/resources/lobby-systems.accesswidener") +} + repositories { mavenCentral() } dependencies { diff --git a/gradle.properties b/gradle.properties index 17d2f23..e570540 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,6 +5,6 @@ minecraft_version=26.2 loader_version=0.19.3 loom_version=1.17-SNAPSHOT fabric_api_version=0.156.0+26.2 -mod_version=1.7.0 +mod_version=1.7.2 maven_group=id.dartokloning archives_base_name=lobby-systems diff --git a/src/main/java/id/dartokloning/lobbysystems/AuthConfig.java b/src/main/java/id/dartokloning/lobbysystems/AuthConfig.java index 0e8ffe1..a2e56f6 100644 --- a/src/main/java/id/dartokloning/lobbysystems/AuthConfig.java +++ b/src/main/java/id/dartokloning/lobbysystems/AuthConfig.java @@ -9,5 +9,7 @@ public final class AuthConfig { public int reminderIntervalSeconds = 8; public int minimumPasswordLength = 6; public int pbkdf2Iterations = 210000; - public String mojangProfileUrl = "https://api.mojang.com/users/profiles/minecraft/{username}"; + public String mojangProfileUrl = "https://api.minecraftservices.com/minecraft/profile/lookup/name/{username}"; + public String legacyMojangProfileUrl = "https://api.mojang.com/users/profiles/minecraft/{username}"; + public boolean requireLoginEveryJoin = true; } diff --git a/src/main/java/id/dartokloning/lobbysystems/AuthManager.java b/src/main/java/id/dartokloning/lobbysystems/AuthManager.java index 5feb0e5..fd42729 100644 --- a/src/main/java/id/dartokloning/lobbysystems/AuthManager.java +++ b/src/main/java/id/dartokloning/lobbysystems/AuthManager.java @@ -62,41 +62,56 @@ public final class AuthManager { public AuthConfig config() { return config; } - /** Called from the login mixin. true = keep vanilla online authentication; false = local/offline path. */ - public boolean shouldUseVanillaAuthentication(String username) { - if (!config.enabled || !config.premiumAutoLogin) return true; + /** Called from the login mixin. */ + public LoginRoute resolveLoginRoute(String username) { + if (!config.enabled || !config.premiumAutoLogin) return LoginRoute.PREMIUM; PremiumResult result = premiumStatus(username); - // Fail closed: if Mojang cannot be checked, retain vanilla authentication rather than granting a local identity. - return result != PremiumResult.NOT_PREMIUM; + return switch (result) { + case PREMIUM -> LoginRoute.PREMIUM; + case NOT_PREMIUM -> LoginRoute.LOCAL; + case UNKNOWN -> LoginRoute.UNKNOWN; + }; } public PremiumResult premiumStatus(String username) { String key = username.toLowerCase(Locale.ROOT); PremiumCache cached = premiumCache.get(key); long now = System.currentTimeMillis(); - if (cached != null && now - cached.checkedAt < 300_000L) return cached.result; + if (cached != null && now - cached.checkedAt < 86_400_000L) return cached.result; + + PremiumResult result = lookupPremium(config.mojangProfileUrl, username); + if (result == PremiumResult.UNKNOWN && config.legacyMojangProfileUrl != null && !config.legacyMojangProfileUrl.isBlank()) { + result = lookupPremium(config.legacyMojangProfileUrl, username); + } + premiumCache.put(key, new PremiumCache(result, now)); + return result; + } + + private PremiumResult lookupPremium(String template, String username) { try { - String url = config.mojangProfileUrl.replace("{username}", java.net.URLEncoder.encode(username, StandardCharsets.UTF_8)); - HttpRequest request = HttpRequest.newBuilder(URI.create(url)).timeout(Duration.ofSeconds(5)).GET().build(); + String url = template.replace("{username}", java.net.URLEncoder.encode(username, StandardCharsets.UTF_8)); + HttpRequest request = HttpRequest.newBuilder(URI.create(url)).timeout(Duration.ofSeconds(8)).header("Accept", "application/json").GET().build(); HttpResponse response = HTTP.send(request, HttpResponse.BodyHandlers.ofString()); - PremiumResult result; - if (response.statusCode() == 200 && response.body() != null && response.body().contains("\"id\"")) result = PremiumResult.PREMIUM; - else if (response.statusCode() == 204 || response.statusCode() == 404) result = PremiumResult.NOT_PREMIUM; - else result = PremiumResult.UNKNOWN; - premiumCache.put(key, new PremiumCache(result, now)); - return result; + if (response.statusCode() == 200 && response.body() != null && response.body().contains("\"id\"")) return PremiumResult.PREMIUM; + if (response.statusCode() == 204 || response.statusCode() == 404) return PremiumResult.NOT_PREMIUM; + LobbySystemsMod.LOGGER.warn("Premium username lookup returned HTTP {} for {} via {}", response.statusCode(), username, URI.create(url).getHost()); + return PremiumResult.UNKNOWN; } catch (Exception e) { - LobbySystemsMod.LOGGER.warn("Premium username lookup failed for {}: {}", username, e.toString()); - premiumCache.put(key, new PremiumCache(PremiumResult.UNKNOWN, now)); + LobbySystemsMod.LOGGER.warn("Premium username lookup failed for {} via {}: {}", username, safeHost(template), e.toString()); return PremiumResult.UNKNOWN; } } + private static String safeHost(String template) { + try { return URI.create(template.replace("{username}", "player")).getHost(); } + catch (Exception ignored) { return "configured endpoint"; } + } + public void onJoin(ServerPlayer player) { if (!config.enabled) return; String name = player.getName().getString(); - PremiumResult premium = premiumStatus(name); - if (premium == PremiumResult.PREMIUM) { + 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.")); return; @@ -215,6 +230,7 @@ public final class AuthManager { 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 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;} } diff --git a/src/main/java/id/dartokloning/lobbysystems/LobbySystemsMod.java b/src/main/java/id/dartokloning/lobbysystems/LobbySystemsMod.java index 18536d8..8d99e52 100644 --- a/src/main/java/id/dartokloning/lobbysystems/LobbySystemsMod.java +++ b/src/main/java/id/dartokloning/lobbysystems/LobbySystemsMod.java @@ -49,13 +49,22 @@ public final class LobbySystemsMod implements ModInitializer { private static final StarterKitManager STARTER_KITS = new StarterKitManager(); private static final AuthManager AUTH = new AuthManager(); private static int saveTicker; + private static final java.util.Set LOCAL_LOGIN_NAMES = java.util.concurrent.ConcurrentHashMap.newKeySet(); /** * Used by the NetherPortalBlock mixin to distinguish Lobby Systems visual * portals from ordinary vanilla Nether portals. */ - public static boolean shouldUseVanillaAuthentication(String username) { - return AUTH.shouldUseVanillaAuthentication(username); + public static AuthManager.LoginRoute resolveLoginRoute(String username) { + return AUTH.resolveLoginRoute(username); + } + + public static void markLocalLogin(String username) { + LOCAL_LOGIN_NAMES.add(username.toLowerCase(java.util.Locale.ROOT)); + } + + public static boolean consumeLocalLogin(String username) { + return LOCAL_LOGIN_NAMES.remove(username.toLowerCase(java.util.Locale.ROOT)); } public static boolean isAuthenticated(ServerPlayer player) { diff --git a/src/main/java/id/dartokloning/lobbysystems/mixin/ServerLoginPacketListenerImplMixin.java b/src/main/java/id/dartokloning/lobbysystems/mixin/ServerLoginPacketListenerImplMixin.java index fa3e389..860ad90 100644 --- a/src/main/java/id/dartokloning/lobbysystems/mixin/ServerLoginPacketListenerImplMixin.java +++ b/src/main/java/id/dartokloning/lobbysystems/mixin/ServerLoginPacketListenerImplMixin.java @@ -1,22 +1,49 @@ package id.dartokloning.lobbysystems.mixin; +import com.mojang.authlib.GameProfile; +import id.dartokloning.lobbysystems.AuthManager; import id.dartokloning.lobbysystems.LobbySystemsMod; +import net.minecraft.core.UUIDUtil; import net.minecraft.network.protocol.login.ServerboundHelloPacket; import net.minecraft.server.MinecraftServer; import net.minecraft.server.network.ServerLoginPacketListenerImpl; +import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -/** Hybrid authentication: preserve vanilla auth for premium names; permit offline profile only for non-premium names. */ +/** + * Hybrid login gate. + * Premium identities continue through vanilla online-mode authentication. + * Confirmed non-premium identities are converted to a deterministic offline + * profile and continue through the normal login state machine without Mojang + * session authentication. Local password authentication happens after join. + */ @Mixin(ServerLoginPacketListenerImpl.class) public abstract class ServerLoginPacketListenerImplMixin { - @Redirect( + @Shadow public GameProfile authenticatedProfile; + @Shadow private ServerLoginPacketListenerImpl.State state; + @Final @Shadow MinecraftServer server; + + @Inject( method = "handleHello(Lnet/minecraft/network/protocol/login/ServerboundHelloPacket;)V", - at = @At(value = "INVOKE", target = "Lnet/minecraft/server/MinecraftServer;usesAuthentication()Z") + at = @At(value = "INVOKE", target = "Lnet/minecraft/server/MinecraftServer;usesAuthentication()Z"), + cancellable = true ) - private boolean lobbySystems$hybridAuthentication(MinecraftServer server, ServerboundHelloPacket packet) { - if (!server.usesAuthentication()) return false; - return LobbySystemsMod.shouldUseVanillaAuthentication(packet.name()); + private void lobbySystems$hybridAuthentication(ServerboundHelloPacket packet, CallbackInfo ci) { + if (!server.usesAuthentication()) return; + + AuthManager.LoginRoute route = LobbySystemsMod.resolveLoginRoute(packet.name()); + if (route != AuthManager.LoginRoute.LOCAL) { + // PREMIUM and UNKNOWN deliberately stay on vanilla authentication. + return; + } + + this.authenticatedProfile = new GameProfile(UUIDUtil.createOfflinePlayerUUID(packet.name()), packet.name()); + this.state = ServerLoginPacketListenerImpl.State.VERIFYING; + LobbySystemsMod.markLocalLogin(packet.name()); + ci.cancel(); } } diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index bed4e6c..081ed28 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -9,6 +9,7 @@ ], "license": "All Rights Reserved", "environment": "server", + "accessWidener": "lobby-systems.accesswidener", "entrypoints": { "main": [ "id.dartokloning.lobbysystems.LobbySystemsMod" diff --git a/src/main/resources/lobby-systems.accesswidener b/src/main/resources/lobby-systems.accesswidener new file mode 100644 index 0000000..ad86688 --- /dev/null +++ b/src/main/resources/lobby-systems.accesswidener @@ -0,0 +1,4 @@ +accessWidener v1 official + +accessible class net/minecraft/server/network/ServerLoginPacketListenerImpl$State +accessible field net/minecraft/server/network/ServerLoginPacketListenerImpl authenticatedProfile Lcom/mojang/authlib/GameProfile;