HMCL-dev/HMCL · error · AuthenticationException
Client token changed from
Error message
Client token changed from
What it means
YggdrasilService.handleAuthenticationResponse throws AuthenticationException when the clientToken in the server's authentication/refresh/validate response differs from the one HMCL sent. Yggdrasil requires the server to echo the client token; a mismatch means the server state no longer matches the local client state.
Solutions
- Perform a full re-authentication (authenticate with username/password) instead of refresh/validate, which re-establishes a matching client token.
- Verify you are talking to the same yggdrasil server the original token was issued by.
- Clear stored tokens/sessions for that account and log in again; avoid sharing one account across multiple launchers simultaneously.
Example fix
// before
service.validate(accessToken); // AuthenticationException: client token changed
// after
try {
service.validate(accessToken);
} catch (AuthenticationException e) {
YggdrasilSession s = service.authenticate(username, password); // full re-login
} Defensive patterns
Strategy: retry
Validate before calling
String echoed = parseClientTokenFromResponse(responseText);
if (echoed != null && !expectedClientToken.equals(echoed)) {
// skip validate/refresh; go straight to full authenticate
} Try / catch
try {
service.validate(accessToken);
} catch (AuthenticationException e) {
if (e.getMessage() != null && e.getMessage().startsWith("Client token changed")) {
// full re-login to resynchronize the client token
service.authenticate(username, password);
}
} Prevention
- Don't reuse accounts across multiple launchers/instances simultaneously.
- Re-authenticate after the auth server is restarted or migrated.
- Always authenticate against the same server the tokens were issued by.
When it happens
Trigger: Any authenticate/refresh/validate round-trip where the response JSON's clientToken differs from the request's, typically after the auth server was reset, migrated, or the client token was regenerated.
Common situations: Auth server (or authlib-injector backend) restarted with a fresh client token store; switching servers while reusing cached sessions; two HMCL instances/other launchers invalidating each other's tokens on the same account.
Related errors
- Selected profile changed
- Profile name is missing
- Failed to select character
- Selected profile changed
- Profile name is missing
AI-assisted analysis of HMCL-dev/HMCL@24702dc5a0 (2026-09-10).
Data as JSON: /api/errors/9041bfad2859dd53.
Report an issue: GitHub.
Appendix: source
Thrown at HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilService.java:207
byte[] decodedBinary;
try {
decodedBinary = Base64.getDecoder().decode(encodedTextures);
} catch (IllegalArgumentException e) {
throw new ServerResponseMalformedException(e);
}
TextureResponse texturePayload = fromJson(new String(decodedBinary, UTF_8), TextureResponse.class);
return Optional.ofNullable(texturePayload.textures);
} else {
return Optional.empty();
}
}
private static YggdrasilSession handleAuthenticationResponse(String responseText, String clientToken) throws AuthenticationException {
AuthenticationResponse response = fromJson(responseText, AuthenticationResponse.class);
handleErrorMessage(response);
if (!clientToken.equals(response.clientToken))
throw new AuthenticationException("Client token changed from " + clientToken + " to " + response.clientToken);
return new YggdrasilSession(
response.clientToken,
response.accessToken,
response.selectedProfile,
response.availableProfiles == null ? null : unmodifiableList(response.availableProfiles),
response.user == null ? null : response.user.properties());
}
private static void requireEmpty(String response) throws AuthenticationException {
if (StringUtils.isBlank(response))
return;
handleErrorMessage(fromJson(response, ErrorResponse.class));
}
private static void handleErrorMessage(ErrorResponse response) throws AuthenticationException {
if (!StringUtils.isBlank(response.error)) {View on GitHub (pinned to 24702dc5a0)