fix loom and auth path

This commit is contained in:
Darto KLoning
2026-10-02 06:22:33 +07:00
parent 48eb9f1228
commit 0cd06d418f
9 changed files with 103 additions and 29 deletions
+11
View File
@@ -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.
+4
View File
@@ -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 {
+1 -1
View File
@@ -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
@@ -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;
}
@@ -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<String> 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;} }
@@ -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<String> 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) {
@@ -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();
}
}
+1
View File
@@ -9,6 +9,7 @@
],
"license": "All Rights Reserved",
"environment": "server",
"accessWidener": "lobby-systems.accesswidener",
"entrypoints": {
"main": [
"id.dartokloning.lobbysystems.LobbySystemsMod"
@@ -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;