fix loom and auth path
This commit is contained in:
@@ -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.
|
- 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.
|
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.
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ group = project.maven_group
|
|||||||
|
|
||||||
base { archivesName = project.archives_base_name }
|
base { archivesName = project.archives_base_name }
|
||||||
|
|
||||||
|
loom {
|
||||||
|
accessWidenerPath = file("src/main/resources/lobby-systems.accesswidener")
|
||||||
|
}
|
||||||
|
|
||||||
repositories { mavenCentral() }
|
repositories { mavenCentral() }
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|||||||
+1
-1
@@ -5,6 +5,6 @@ minecraft_version=26.2
|
|||||||
loader_version=0.19.3
|
loader_version=0.19.3
|
||||||
loom_version=1.17-SNAPSHOT
|
loom_version=1.17-SNAPSHOT
|
||||||
fabric_api_version=0.156.0+26.2
|
fabric_api_version=0.156.0+26.2
|
||||||
mod_version=1.7.0
|
mod_version=1.7.2
|
||||||
maven_group=id.dartokloning
|
maven_group=id.dartokloning
|
||||||
archives_base_name=lobby-systems
|
archives_base_name=lobby-systems
|
||||||
|
|||||||
@@ -9,5 +9,7 @@ public final class AuthConfig {
|
|||||||
public int reminderIntervalSeconds = 8;
|
public int reminderIntervalSeconds = 8;
|
||||||
public int minimumPasswordLength = 6;
|
public int minimumPasswordLength = 6;
|
||||||
public int pbkdf2Iterations = 210000;
|
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; }
|
public AuthConfig config() { return config; }
|
||||||
|
|
||||||
/** Called from the login mixin. true = keep vanilla online authentication; false = local/offline path. */
|
/** Called from the login mixin. */
|
||||||
public boolean shouldUseVanillaAuthentication(String username) {
|
public LoginRoute resolveLoginRoute(String username) {
|
||||||
if (!config.enabled || !config.premiumAutoLogin) return true;
|
if (!config.enabled || !config.premiumAutoLogin) return LoginRoute.PREMIUM;
|
||||||
PremiumResult result = premiumStatus(username);
|
PremiumResult result = premiumStatus(username);
|
||||||
// Fail closed: if Mojang cannot be checked, retain vanilla authentication rather than granting a local identity.
|
return switch (result) {
|
||||||
return result != PremiumResult.NOT_PREMIUM;
|
case PREMIUM -> LoginRoute.PREMIUM;
|
||||||
|
case NOT_PREMIUM -> LoginRoute.LOCAL;
|
||||||
|
case UNKNOWN -> LoginRoute.UNKNOWN;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public PremiumResult premiumStatus(String username) {
|
public PremiumResult premiumStatus(String username) {
|
||||||
String key = username.toLowerCase(Locale.ROOT);
|
String key = username.toLowerCase(Locale.ROOT);
|
||||||
PremiumCache cached = premiumCache.get(key);
|
PremiumCache cached = premiumCache.get(key);
|
||||||
long now = System.currentTimeMillis();
|
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 {
|
try {
|
||||||
String url = config.mojangProfileUrl.replace("{username}", java.net.URLEncoder.encode(username, StandardCharsets.UTF_8));
|
String url = template.replace("{username}", java.net.URLEncoder.encode(username, StandardCharsets.UTF_8));
|
||||||
HttpRequest request = HttpRequest.newBuilder(URI.create(url)).timeout(Duration.ofSeconds(5)).GET().build();
|
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());
|
HttpResponse<String> response = HTTP.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
PremiumResult result;
|
if (response.statusCode() == 200 && response.body() != null && response.body().contains("\"id\"")) return PremiumResult.PREMIUM;
|
||||||
if (response.statusCode() == 200 && response.body() != null && response.body().contains("\"id\"")) result = PremiumResult.PREMIUM;
|
if (response.statusCode() == 204 || response.statusCode() == 404) return PremiumResult.NOT_PREMIUM;
|
||||||
else if (response.statusCode() == 204 || response.statusCode() == 404) result = PremiumResult.NOT_PREMIUM;
|
LobbySystemsMod.LOGGER.warn("Premium username lookup returned HTTP {} for {} via {}", response.statusCode(), username, URI.create(url).getHost());
|
||||||
else result = PremiumResult.UNKNOWN;
|
return PremiumResult.UNKNOWN;
|
||||||
premiumCache.put(key, new PremiumCache(result, now));
|
|
||||||
return result;
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LobbySystemsMod.LOGGER.warn("Premium username lookup failed for {}: {}", username, e.toString());
|
LobbySystemsMod.LOGGER.warn("Premium username lookup failed for {} via {}: {}", username, safeHost(template), e.toString());
|
||||||
premiumCache.put(key, new PremiumCache(PremiumResult.UNKNOWN, now));
|
|
||||||
return PremiumResult.UNKNOWN;
|
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) {
|
public void onJoin(ServerPlayer player) {
|
||||||
if (!config.enabled) return;
|
if (!config.enabled) return;
|
||||||
String name = player.getName().getString();
|
String name = player.getName().getString();
|
||||||
PremiumResult premium = premiumStatus(name);
|
boolean local = LobbySystemsMod.consumeLocalLogin(name);
|
||||||
if (premium == PremiumResult.PREMIUM) {
|
if (!local) {
|
||||||
sessions.put(player.getUUID(), new Session(true, true, 0, System.currentTimeMillis(), player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot()));
|
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."));
|
player.sendSystemMessage(Component.literal("§aLobby Systems: Premium account verified. Auto-login successful."));
|
||||||
return;
|
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); } }
|
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 PremiumResult { PREMIUM, NOT_PREMIUM, UNKNOWN }
|
||||||
|
public enum LoginRoute { PREMIUM, LOCAL, UNKNOWN }
|
||||||
private record PremiumCache(PremiumResult result, long checkedAt) {}
|
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 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 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 StarterKitManager STARTER_KITS = new StarterKitManager();
|
||||||
private static final AuthManager AUTH = new AuthManager();
|
private static final AuthManager AUTH = new AuthManager();
|
||||||
private static int saveTicker;
|
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
|
* Used by the NetherPortalBlock mixin to distinguish Lobby Systems visual
|
||||||
* portals from ordinary vanilla Nether portals.
|
* portals from ordinary vanilla Nether portals.
|
||||||
*/
|
*/
|
||||||
public static boolean shouldUseVanillaAuthentication(String username) {
|
public static AuthManager.LoginRoute resolveLoginRoute(String username) {
|
||||||
return AUTH.shouldUseVanillaAuthentication(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) {
|
public static boolean isAuthenticated(ServerPlayer player) {
|
||||||
|
|||||||
+34
-7
@@ -1,22 +1,49 @@
|
|||||||
package id.dartokloning.lobbysystems.mixin;
|
package id.dartokloning.lobbysystems.mixin;
|
||||||
|
|
||||||
|
import com.mojang.authlib.GameProfile;
|
||||||
|
import id.dartokloning.lobbysystems.AuthManager;
|
||||||
import id.dartokloning.lobbysystems.LobbySystemsMod;
|
import id.dartokloning.lobbysystems.LobbySystemsMod;
|
||||||
|
import net.minecraft.core.UUIDUtil;
|
||||||
import net.minecraft.network.protocol.login.ServerboundHelloPacket;
|
import net.minecraft.network.protocol.login.ServerboundHelloPacket;
|
||||||
import net.minecraft.server.MinecraftServer;
|
import net.minecraft.server.MinecraftServer;
|
||||||
import net.minecraft.server.network.ServerLoginPacketListenerImpl;
|
import net.minecraft.server.network.ServerLoginPacketListenerImpl;
|
||||||
|
import org.spongepowered.asm.mixin.Final;
|
||||||
import org.spongepowered.asm.mixin.Mixin;
|
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.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)
|
@Mixin(ServerLoginPacketListenerImpl.class)
|
||||||
public abstract class ServerLoginPacketListenerImplMixin {
|
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",
|
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) {
|
private void lobbySystems$hybridAuthentication(ServerboundHelloPacket packet, CallbackInfo ci) {
|
||||||
if (!server.usesAuthentication()) return false;
|
if (!server.usesAuthentication()) return;
|
||||||
return LobbySystemsMod.shouldUseVanillaAuthentication(packet.name());
|
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
],
|
],
|
||||||
"license": "All Rights Reserved",
|
"license": "All Rights Reserved",
|
||||||
"environment": "server",
|
"environment": "server",
|
||||||
|
"accessWidener": "lobby-systems.accesswidener",
|
||||||
"entrypoints": {
|
"entrypoints": {
|
||||||
"main": [
|
"main": [
|
||||||
"id.dartokloning.lobbysystems.LobbySystemsMod"
|
"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;
|
||||||
Reference in New Issue
Block a user