Newer
Older
sklauncher / launcher / src / main / java / com / skcraft / launcher / auth / YggdrasilLoginService.java
/*
 * SK's Minecraft Launcher
 * Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> and contributors
 * Please see LICENSE.txt for license information.
 */

package com.skcraft.launcher.auth;

import com.fasterxml.jackson.annotation.*;
import com.skcraft.launcher.util.HttpRequest;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.ToString;

import java.io.IOException;
import java.net.URL;
import java.util.Collections;
import java.util.Map;

/**
 * Creates authenticated sessions using the Mojang Yggdrasil login protocol.
 */
@RequiredArgsConstructor
public class YggdrasilLoginService implements LoginService {

    private final URL authUrl;
    private final String clientId;

    public Session login(String id, String password)
            throws IOException, InterruptedException, AuthenticationException {
        AuthenticatePayload payload = new AuthenticatePayload(new Agent("Minecraft"), id, password, clientId);

        return call(this.authUrl, payload, null);
    }

    @Override
    public Session restore(SavedSession savedSession)
            throws IOException, InterruptedException, AuthenticationException {
        RefreshPayload payload = new RefreshPayload(savedSession.getAccessToken(), clientId);

        return call(new URL(this.authUrl, "/refresh"), payload, savedSession);
    }

    private Session call(URL url, Object payload, SavedSession previous)
            throws IOException, InterruptedException, AuthenticationException {
        HttpRequest req = HttpRequest
                .post(url)
                .bodyJson(payload)
                .execute();

        if (req.getResponseCode() != 200) {
            ErrorResponse error = req.returnContent().asJson(ErrorResponse.class);

            throw new AuthenticationException(error.getErrorMessage(), true);
        } else {
            AuthenticateResponse response = req.returnContent().asJson(AuthenticateResponse.class);
            Profile profile = response.getSelectedProfile();

            if (profile == null) return null; // Minecraft not owned

            if (previous != null && previous.getAvatarImage() != null) {
                profile.setAvatarImage(previous.getAvatarImage());
            }

            // DEPRECEATION: minecraft services API no longer accepts yggdrasil tokens
            // login still works though. until it doesn't, this class will remain

            return profile;
        }
    }

    @Data
    private static class Agent {
        private final String name;
        private final int version = 1;
    }

    @Data
    private static class AuthenticatePayload {
        private final Agent agent;
        private final String username;
        private final String password;
        private final String clientToken;
    }

    @Data
    private static class RefreshPayload {
        private final String accessToken;
        private final String clientToken;
        private boolean requestUser = true;
    }

    @Data
    @JsonIgnoreProperties(ignoreUnknown = true)
    private static class AuthenticateResponse {
        private String accessToken;
        private String clientToken;
        @JsonManagedReference private Profile selectedProfile;
    }

    @Data
    private static class ErrorResponse {
        private String error;
        private String errorMessage;
        private String cause;
    }

    /**
     * Return in the list of available profiles.
     */
    @Data
    @ToString(exclude = "response")
    @JsonIgnoreProperties(ignoreUnknown = true)
    private static class Profile implements Session {
        @JsonProperty("id") private String uuid;
        private String name;
        private boolean legacy;
        private byte[] avatarImage;
        @JsonIgnore private final Map<String, String> userProperties = Collections.emptyMap();
        @JsonBackReference private AuthenticateResponse response;

        @Override
        @JsonIgnore
        public String getSessionToken() {
            return String.format("token:%s:%s", getAccessToken(), getUuid());
        }

        @Override
        @JsonIgnore
        public String getAccessToken() {
            return response.getAccessToken();
        }

        @Override
        @JsonIgnore
        public UserType getUserType() {
            return legacy ? UserType.LEGACY : UserType.MOJANG;
        }

        @Override
        public boolean isOnline() {
            return true;
        }
    }

}