Authenticate through Mojang's session server; check client version

Stop sending the Minecraft access token to the Saturn server. The
connect loop now:

1. fetches GET /versions and compares this client's mod version:
   supported -> connect; deprecated -> warn, connect; neither -> warn,
   don't connect (warnings are shown once per status change)
2. requests auth_challenge with the username to get a server id
3. calls Mojang's session `join` with the access token and server id
   (the token only goes to Mojang); a refused token gives up
4. requests auth_verify, which the server confirms with `hasJoined`

Add SaturnProvider.getModVersion() and showWarning() for the version
check and its toast.

Refs saturnclientmc/saturnclient#7

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-09-25 15:22:37 +02:00
co-authored by claude
parent 510760d590
commit 2de394ebbf
4 changed files with 206 additions and 9 deletions
@@ -1,5 +1,9 @@
package org.saturnclient.client;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.UUID;
import java.util.concurrent.CompletionException;
@@ -13,6 +17,8 @@ import org.saturnclient.common.ref.game.MinecraftClientRef;
import org.saturnclient.cosmetics.Cloaks;
import org.saturnclient.cosmetics.Hats;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.selimaj.session.Session;
import dev.selimaj.session.types.SessionErrorException;
@@ -31,6 +37,22 @@ public class ServiceClient {
private static final AtomicBoolean CONNECTING = new AtomicBoolean();
private static volatile boolean stopping = false;
private static final HttpClient HTTP = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(CONNECT_TIMEOUT_SECONDS))
.build();
private static final ObjectMapper MAPPER = new ObjectMapper();
private enum VersionStatus {
SUPPORTED,
/** Still accepted, but the player should update. */
DEPRECATED,
/** Not accepted; don't connect. */
UNSUPPORTED
}
/** Last status the player was warned about, so reconnects don't repeat the toast. */
private static volatile VersionStatus warnedStatus = VersionStatus.SUPPORTED;
public static void initialize() {
Providers.saturn.getClient().onClientStopping(() -> {
stopping = true;
@@ -130,6 +152,19 @@ public class ServiceClient {
}
uuid = playerUuid;
VersionStatus version;
try {
version = checkVersion();
} catch (Exception e) {
Providers.saturn.logError("Unable to fetch the Saturn version manifest: " + rootMessage(e));
return ConnectResult.RETRY;
}
if (version == VersionStatus.UNSUPPORTED) {
return ConnectResult.GIVE_UP;
}
Providers.saturn.logInfo("Authenticating with UUID: " + playerUuid);
Session s;
@@ -142,17 +177,46 @@ public class ServiceClient {
ServiceMethods.Types.Player response;
try {
response = s.request(ServiceMethods.Authenticate, accessToken, REQUEST_TIMEOUT).get();
} catch (ExecutionException e) {
s.close();
// 1. The server hands out a one-time server id for this connection.
String serverId;
try {
serverId = s.request(ServiceMethods.AuthChallenge, username, REQUEST_TIMEOUT).get();
} catch (ExecutionException e) {
s.close();
if (e.getCause() instanceof SessionErrorException rejected) {
Providers.saturn.logError("Saturn server rejected authentication: " + rejected.getMessage());
return ConnectResult.GIVE_UP;
if (e.getCause() instanceof SessionErrorException rejected) {
Providers.saturn.logError("Saturn server rejected authentication: " + rejected.getMessage());
return ConnectResult.GIVE_UP;
}
Providers.saturn.logError("Authentication failed: " + rootMessage(e));
return ConnectResult.RETRY;
}
Providers.saturn.logError("Authentication failed: " + rootMessage(e));
return ConnectResult.RETRY;
// 2. Join with it at Mojang; the access token only goes to Mojang.
switch (MojangSession.join(HTTP, accessToken, playerUuid, serverId)) {
case JOINED -> {
}
case REJECTED -> {
s.close();
Providers.saturn.logError("Mojang rejected the Minecraft session");
return ConnectResult.GIVE_UP;
}
case FAILED -> {
s.close();
Providers.saturn.logError("Unable to reach the Mojang session server");
return ConnectResult.RETRY;
}
}
// 3. The server confirms the join with Mojang and logs us in.
try {
response = s.request(ServiceMethods.AuthVerify, null, REQUEST_TIMEOUT).get();
} catch (ExecutionException e) {
s.close();
Providers.saturn.logError("Authentication failed: " + rootMessage(e));
return ConnectResult.RETRY;
}
} catch (InterruptedException e) {
s.close();
return ConnectResult.GIVE_UP;
@@ -183,6 +247,59 @@ public class ServiceClient {
return ConnectResult.CONNECTED;
}
/**
* Checks this client's version against the server's manifest and warns the
* player if it's deprecated or unsupported.
*/
private static VersionStatus checkVersion() throws Exception {
URI server = URI.create(SERVER_URI);
String scheme = "wss".equals(server.getScheme()) ? "https" : "http";
URI manifestUri = new URI(scheme, server.getAuthority(), "/versions", null, null);
HttpResponse<String> res = HTTP.send(
HttpRequest.newBuilder(manifestUri).timeout(REQUEST_TIMEOUT).GET().build(),
HttpResponse.BodyHandlers.ofString());
if (res.statusCode() != 200) {
throw new IllegalStateException("GET /versions returned " + res.statusCode());
}
ServiceMethods.VersionManifest manifest = MAPPER.readValue(res.body(), ServiceMethods.VersionManifest.class);
// Drop build metadata such as "+1.21.11": the manifest lists mod versions.
String version = Providers.saturn.getModVersion().split("\\+", 2)[0];
VersionStatus status;
if (manifest.supported() != null && manifest.supported().contains(version)) {
status = VersionStatus.SUPPORTED;
} else if (manifest.deprecated() != null && manifest.deprecated().contains(version)) {
status = VersionStatus.DEPRECATED;
} else {
status = VersionStatus.UNSUPPORTED;
}
if (status != warnedStatus) {
warnedStatus = status;
switch (status) {
case DEPRECATED -> {
Providers.saturn.logError("Saturn Client " + version + " is deprecated");
Providers.saturn.showWarning("Saturn Client update available",
"Version " + version + " is deprecated and will stop working soon. Please update.");
}
case UNSUPPORTED -> {
Providers.saturn.logError("Saturn Client " + version + " is no longer supported, not connecting");
Providers.saturn.showWarning("Saturn Client is outdated",
"Version " + version + " is no longer supported. Update to use cosmetics and emotes.");
}
case SUPPORTED -> {
}
}
}
return status;
}
private static void onDisconnected(Session closed) {
if (session != closed) {
return;