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

  1. Perform a full re-authentication (authenticate with username/password) instead of refresh/validate, which re-establishes a matching client token.
  2. Verify you are talking to the same yggdrasil server the original token was issued by.
  3. 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

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


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)