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
@@ -0,0 +1,60 @@
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 com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
/**
* Mojang's session server "join" call, the same one vanilla makes when joining
* an online-mode server. The access token is only ever sent to Mojang; the
* Saturn server then confirms the join with {@code hasJoined}.
*/
public final class MojangSession {
private static final URI JOIN_URI = URI.create("https://sessionserver.mojang.com/session/minecraft/join");
private static final ObjectMapper MAPPER = new ObjectMapper();
public enum JoinResult {
JOINED,
/** Mojang refused the access token; retrying won't help. */
REJECTED,
/** Network or server error; worth retrying. */
FAILED
}
private MojangSession() {
}
public static JoinResult join(HttpClient http, String accessToken, UUID profile, String serverId) {
ObjectNode body = MAPPER.createObjectNode()
.put("accessToken", accessToken)
.put("selectedProfile", profile.toString().replace("-", ""))
.put("serverId", serverId);
HttpRequest request = HttpRequest.newBuilder(JOIN_URI)
.timeout(Duration.ofSeconds(10))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body.toString()))
.build();
try {
int status = http.send(request, HttpResponse.BodyHandlers.discarding()).statusCode();
if (status == 204 || status == 200) {
return JoinResult.JOINED;
}
return status == 401 || status == 403 ? JoinResult.REJECTED : JoinResult.FAILED;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return JoinResult.FAILED;
} catch (Exception e) {
return JoinResult.FAILED;
}
}
}