1
0
mirror of https://github.com/SKCraft/Launcher.git synced 2024-11-27 12:46:22 +01:00

Redesign login dialogs & support new Microsoft logins (#398)

This commit is contained in:
Henry 2021-02-17 01:15:39 +00:00 committed by GitHub
commit 4177c15675
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 1194 additions and 468 deletions

View File

@ -27,10 +27,10 @@ subprojects {
apply plugin: 'maven'
group = 'com.skcraft'
version = '4.4-SNAPSHOT'
version = '4.5-SNAPSHOT'
sourceCompatibility = 1.6
targetCompatibility = 1.6
sourceCompatibility = 1.8
targetCompatibility = 1.8
repositories {
mavenCentral()
@ -42,4 +42,9 @@ subprojects {
options.addStringOption('Xdoclint:none', '-quiet')
}
}
tasks.withType(JavaExec) {
workingDir = new File(rootDir, "run/")
workingDir.mkdirs()
}
}

View File

@ -21,18 +21,18 @@ import com.skcraft.launcher.auth.Session;
import com.skcraft.launcher.builder.BuilderConfig;
import com.skcraft.launcher.builder.FnPatternList;
import com.skcraft.launcher.creator.Creator;
import com.skcraft.launcher.creator.model.creator.*;
import com.skcraft.launcher.creator.controller.task.*;
import com.skcraft.launcher.creator.dialog.*;
import com.skcraft.launcher.creator.dialog.BuildDialog.BuildOptions;
import com.skcraft.launcher.creator.dialog.DeployServerDialog.DeployOptions;
import com.skcraft.launcher.creator.model.creator.*;
import com.skcraft.launcher.creator.model.swing.PackTableModel;
import com.skcraft.launcher.creator.server.TestServer;
import com.skcraft.launcher.creator.server.TestServerBuilder;
import com.skcraft.launcher.creator.swing.PackDirectoryFilter;
import com.skcraft.launcher.dialog.AccountSelectDialog;
import com.skcraft.launcher.dialog.ConfigurationDialog;
import com.skcraft.launcher.dialog.ConsoleFrame;
import com.skcraft.launcher.dialog.LoginDialog;
import com.skcraft.launcher.dialog.ProgressDialog;
import com.skcraft.launcher.model.modpack.LaunchModifier;
import com.skcraft.launcher.persistence.Persistence;
@ -225,7 +225,7 @@ public class PackManagerController {
if (config.isOfflineEnabled()) {
return true;
} else {
Session session = LoginDialog.showLoginRequest(frame, launcher);
Session session = AccountSelectDialog.showAccountRequest(frame, launcher);
if (session != null) {
config.setOfflineEnabled(true);
Persistence.commitAndForget(config);
@ -734,7 +734,7 @@ public class PackManagerController {
Session session;
if (online) {
session = LoginDialog.showLoginRequest(frame, launcher);
session = AccountSelectDialog.showAccountRequest(frame, launcher);
if (session == null) {
return;
}

View File

@ -12,9 +12,7 @@ import com.google.common.base.Strings;
import com.google.common.base.Supplier;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.skcraft.launcher.auth.AccountList;
import com.skcraft.launcher.auth.LoginService;
import com.skcraft.launcher.auth.YggdrasilLoginService;
import com.skcraft.launcher.auth.*;
import com.skcraft.launcher.launch.LaunchSupervisor;
import com.skcraft.launcher.model.minecraft.Library;
import com.skcraft.launcher.model.minecraft.VersionManifest;
@ -100,10 +98,6 @@ public final class Launcher {
setDefaultConfig();
if (accounts.getSize() > 0) {
accounts.setSelectedItem(accounts.getElementAt(0));
}
executor.submit(new Runnable() {
@Override
public void run() {
@ -161,12 +155,29 @@ public final class Launcher {
}
/**
* Get a login service.
* Get the Yggdrasil login service.
*
* @return a login service
* @return the Yggdrasil (legacy) login service
*/
public LoginService getLoginService() {
return new YggdrasilLoginService(HttpRequest.url(getProperties().getProperty("yggdrasilAuthUrl")));
public YggdrasilLoginService getYggdrasil() {
return new YggdrasilLoginService(HttpRequest.url(getProperties().getProperty("yggdrasilAuthUrl")), accounts.getClientId());
}
/**
* Get the Microsoft login service.
*
* @return the Microsoft (current) login service
*/
public MicrosoftLoginService getMicrosoftLogin() {
return new MicrosoftLoginService(getProperties().getProperty("microsoftClientId"));
}
public LoginService getLoginService(UserType type) {
if (type == UserType.MICROSOFT) {
return getMicrosoftLogin();
} else {
return getYggdrasil();
}
}
/**

View File

@ -1,91 +0,0 @@
/*
* 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.JsonIgnoreProperties;
import com.google.common.base.Strings;
import lombok.Data;
import lombok.NonNull;
import java.util.Date;
/**
* A user account that can be stored and loaded.
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class Account implements Comparable<Account> {
private String id;
private String password;
private Date lastUsed;
/**
* Create a new account.
*/
public Account() {
}
/**
* Create a new account with the given ID.
*
* @param id the ID
*/
public Account(String id) {
setId(id);
}
/**
* Set the account's stored password, that may be stored to disk.
*
* @param password the password
*/
public void setPassword(String password) {
if (password != null && password.isEmpty()) {
password = null;
}
this.password = Strings.emptyToNull(password);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Account account = (Account) o;
if (!id.equalsIgnoreCase(account.id)) return false;
return true;
}
@Override
public int hashCode() {
return id.toLowerCase().hashCode();
}
@Override
public int compareTo(@NonNull Account o) {
Date otherDate = o.getLastUsed();
if (otherDate == null && lastUsed == null) {
return 0;
} else if (otherDate == null) {
return -1;
} else if (lastUsed == null) {
return 1;
} else {
return -lastUsed.compareTo(otherDate);
}
}
@Override
public String toString() {
return getId();
}
}

View File

@ -1,134 +1,78 @@
/*
* 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.JsonAutoDetect;
import com.beust.jcommander.internal.Lists;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.skcraft.launcher.dialog.component.ListListenerReducer;
import com.skcraft.launcher.persistence.Scrambled;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import lombok.ToString;
import org.apache.commons.lang.RandomStringUtils;
import javax.swing.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import java.util.List;
/**
* A list of accounts that can be stored to disk.
* Persisted account list
*/
@Scrambled("ACCOUNT_LIST")
@Scrambled("ACCOUNT_LIST_NOT_SECURITY!")
@Getter
@Setter
@ToString
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonAutoDetect(
getterVisibility = JsonAutoDetect.Visibility.NONE,
setterVisibility = JsonAutoDetect.Visibility.NONE,
fieldVisibility = JsonAutoDetect.Visibility.NONE)
public class AccountList extends AbstractListModel implements ComboBoxModel {
public class AccountList implements ListModel<SavedSession> {
private List<SavedSession> accounts = Lists.newArrayList();
private String clientId = RandomStringUtils.randomAlphanumeric(24);
@JsonProperty
@Getter
private List<Account> accounts = new ArrayList<Account>();
private transient Account selected;
@JsonIgnore private final ListListenerReducer listeners = new ListListenerReducer();
/**
* Add a new account.
*
* <p>If there is already an existing account with the same ID, then the
* new account will not be added.</p>
*
* @param account the account to add
*/
public synchronized void add(@NonNull Account account) {
if (!accounts.contains(account)) {
accounts.add(account);
Collections.sort(accounts);
fireContentsChanged(this, 0, accounts.size());
}
}
public synchronized void add(SavedSession session) {
accounts.add(session);
/**
* Remove an account.
*
* @param account the account
*/
public synchronized void remove(@NonNull Account account) {
Iterator<Account> it = accounts.iterator();
while (it.hasNext()) {
Account other = it.next();
if (other.equals(account)) {
it.remove();
fireContentsChanged(this, 0, accounts.size() + 1);
break;
}
}
}
int index = accounts.size() - 1;
listeners.intervalAdded(new ListDataEvent(this, ListDataEvent.INTERVAL_ADDED, index, index));
}
/**
* Set the list of accounts.
*
* @param accounts the list of accounts
*/
public synchronized void setAccounts(@NonNull List<Account> accounts) {
this.accounts = accounts;
Collections.sort(accounts);
}
public synchronized void remove(SavedSession session) {
int index = accounts.indexOf(session);
@Override
@JsonIgnore
public synchronized int getSize() {
return accounts.size();
}
if (index > -1) {
accounts.remove(index);
listeners.intervalRemoved(new ListDataEvent(this, ListDataEvent.INTERVAL_REMOVED, index, index));
}
}
@Override
public synchronized Account getElementAt(int index) {
try {
return accounts.get(index);
} catch (IndexOutOfBoundsException e) {
return null;
}
}
public synchronized void update(SavedSession newSavedSession) {
int index = accounts.indexOf(newSavedSession);
@Override
public void setSelectedItem(Object item) {
if (item == null) {
selected = null;
return;
}
if (index > -1) {
accounts.set(index, newSavedSession);
listeners.contentsChanged(new ListDataEvent(this, ListDataEvent.CONTENTS_CHANGED, index, index));
} else {
this.add(newSavedSession);
}
}
if (item instanceof Account) {
this.selected = (Account) item;
} else {
String id = String.valueOf(item).trim();
Account account = new Account(id);
for (Account test : accounts) {
if (test.equals(account)) {
account = test;
break;
}
}
selected = account;
}
@Override
public int getSize() {
return accounts.size();
}
if (selected.getId() == null || selected.getId().isEmpty()) {
selected = null;
}
}
@Override
public SavedSession getElementAt(int index) {
return accounts.get(index);
}
@Override
@JsonIgnore
public Account getSelectedItem() {
return selected;
}
@Override
public void addListDataListener(ListDataListener l) {
listeners.addListDataListener(l);
}
public synchronized void forgetPasswords() {
for (Account account : accounts) {
account.setPassword(null);
}
}
@Override
public void removeListDataListener(ListDataListener l) {
listeners.removeListDataListener(l);
}
}

View File

@ -17,6 +17,10 @@ public class AuthenticationException extends LauncherException {
super(message, localizedMessage);
}
public AuthenticationException(String message) {
super(message, message);
}
public AuthenticationException(Throwable cause, String localizedMessage) {
super(cause, localizedMessage);
}

View File

@ -7,7 +7,6 @@
package com.skcraft.launcher.auth;
import java.io.IOException;
import java.util.List;
/**
* A service for creating authenticated sessions.
@ -15,17 +14,15 @@ import java.util.List;
public interface LoginService {
/**
* Attempt to login with the given details.
* Attempt to restore a saved session into an active session.
*
* @param agent the game to authenticate for, such as "Minecraft"
* @param id the login ID
* @param password the password
* @return a list of authenticated sessions, which corresponds to identities
* @param savedSession Session to restore
* @return An authenticated session, which corresponds to a Minecraft account
* @throws IOException thrown on I/O error
* @throws InterruptedException thrown if interrupted
* @throws AuthenticationException thrown on an authentication error
*/
List<? extends Session> login(String agent, String id, String password)
Session restore(SavedSession savedSession)
throws IOException, InterruptedException, AuthenticationException;
}

View File

@ -0,0 +1,177 @@
package com.skcraft.launcher.auth;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import com.skcraft.launcher.auth.microsoft.MicrosoftWebAuthorizer;
import com.skcraft.launcher.auth.microsoft.MinecraftServicesAuthorizer;
import com.skcraft.launcher.auth.microsoft.OauthResult;
import com.skcraft.launcher.auth.microsoft.XboxTokenAuthorizer;
import com.skcraft.launcher.auth.microsoft.model.McAuthResponse;
import com.skcraft.launcher.auth.microsoft.model.McProfileResponse;
import com.skcraft.launcher.auth.microsoft.model.TokenResponse;
import com.skcraft.launcher.auth.microsoft.model.XboxAuthorization;
import com.skcraft.launcher.util.HttpRequest;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import java.io.IOException;
import java.net.URL;
import java.util.Collections;
import java.util.Map;
import java.util.function.Consumer;
import static com.skcraft.launcher.util.HttpRequest.url;
@RequiredArgsConstructor
public class MicrosoftLoginService implements LoginService {
private static final URL MS_TOKEN_URL = url("https://login.live.com/oauth20_token.srf");
private final String clientId;
/**
* Trigger a full login sequence with the Microsoft authenticator.
*
* @param oauthDone Callback called when OAuth is complete and automatic login is about to begin.
* @return Valid {@link Session} instance representing the logged-in player.
* @throws IOException if any I/O error occurs.
* @throws InterruptedException if the current thread is interrupted
* @throws AuthenticationException if authentication fails in any way, this is thrown with a human-useful message.
*/
public Session login(Receiver oauthDone) throws IOException, InterruptedException, AuthenticationException {
MicrosoftWebAuthorizer authorizer = new MicrosoftWebAuthorizer(clientId);
OauthResult auth = authorizer.authorize();
if (auth.isError()) {
OauthResult.Error error = (OauthResult.Error) auth;
throw new AuthenticationException(error.getErrorMessage());
}
TokenResponse response = exchangeToken(form -> {
form.add("grant_type", "authorization_code");
form.add("redirect_uri", authorizer.getRedirectUri());
form.add("code", ((OauthResult.Success) auth).getAuthCode());
});
oauthDone.tell();
Profile session = performLogin(response.getAccessToken(), null);
session.setRefreshToken(response.getRefreshToken());
return session;
}
@Override
public Session restore(SavedSession savedSession)
throws IOException, InterruptedException, AuthenticationException {
TokenResponse response = exchangeToken(form -> {
form.add("grant_type", "refresh_token");
form.add("refresh_token", savedSession.getRefreshToken());
});
Profile session = performLogin(response.getAccessToken(), savedSession);
session.setRefreshToken(response.getRefreshToken());
return session;
}
private TokenResponse exchangeToken(Consumer<HttpRequest.Form> formConsumer)
throws IOException, InterruptedException, AuthenticationException {
HttpRequest.Form form = HttpRequest.Form.form();
form.add("client_id", clientId);
formConsumer.accept(form);
return HttpRequest.post(MS_TOKEN_URL)
.bodyForm(form)
.execute()
.expectResponseCodeOr(200, (req) -> {
TokenError error = req.returnContent().asJson(TokenError.class);
return new AuthenticationException(error.errorDescription);
})
.returnContent()
.asJson(TokenResponse.class);
}
private Profile performLogin(String microsoftToken, SavedSession previous)
throws IOException, InterruptedException, AuthenticationException {
XboxAuthorization xboxAuthorization = XboxTokenAuthorizer.authorizeWithXbox(microsoftToken);
McAuthResponse auth = MinecraftServicesAuthorizer.authorizeWithMinecraft(xboxAuthorization);
McProfileResponse profile = MinecraftServicesAuthorizer.getUserProfile(auth);
Profile session = new Profile(auth, profile);
if (previous != null && previous.getAvatarImage() != null) {
session.setAvatarImage(previous.getAvatarImage());
} else {
session.setAvatarImage(VisageSkinService.fetchSkinHead(profile.getUuid()));
}
return session;
}
@Data
public static class Profile implements Session {
private final McAuthResponse auth;
private final McProfileResponse profile;
private final Map<String, String> userProperties = Collections.emptyMap();
private String refreshToken;
private byte[] avatarImage;
@Override
public String getUuid() {
return profile.getUuid();
}
@Override
public String getName() {
return profile.getName();
}
@Override
public String getAccessToken() {
return auth.getAccessToken();
}
@Override
public String getSessionToken() {
return String.format("token:%s:%s", getAccessToken(), getUuid());
}
@Override
public UserType getUserType() {
return UserType.MICROSOFT;
}
@Override
public boolean isOnline() {
return true;
}
@Override
public SavedSession toSavedSession() {
SavedSession savedSession = new SavedSession();
savedSession.setType(getUserType());
savedSession.setUsername(getName());
savedSession.setUuid(getUuid());
savedSession.setAccessToken(getAccessToken());
savedSession.setRefreshToken(getRefreshToken());
savedSession.setAvatarImage(getAvatarImage());
return savedSession;
}
}
@Data
@JsonNaming(PropertyNamingStrategy.LowerCaseWithUnderscoresStrategy.class)
@JsonIgnoreProperties(ignoreUnknown = true)
private static class TokenError {
private String error;
private String errorDescription;
}
@FunctionalInterface
public interface Receiver {
void tell();
}
}

View File

@ -37,11 +37,6 @@ public class OfflineSession implements Session {
return (new UUID(0, 0)).toString();
}
@Override
public String getClientToken() {
return "0";
}
@Override
public String getAccessToken() {
return "0";
@ -62,6 +57,11 @@ public class OfflineSession implements Session {
return UserType.LEGACY;
}
@Override
public byte[] getAvatarImage() {
return null;
}
@Override
public boolean isOnline() {
return false;

View File

@ -0,0 +1,37 @@
package com.skcraft.launcher.auth;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import org.apache.commons.lang.builder.HashCodeBuilder;
/**
* Represents a session saved to disk.
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class SavedSession {
private UserType type;
private String uuid;
private String username;
private String accessToken;
private String refreshToken;
private byte[] avatarImage;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SavedSession that = (SavedSession) o;
return getUuid().equals(that.getUuid());
}
@Override
public int hashCode() {
return new HashCodeBuilder(17, 37)
.append(uuid)
.toHashCode();
}
}

View File

@ -27,13 +27,6 @@ public interface Session {
*/
String getName();
/**
* Get the client token.
*
* @return client token
*/
String getClientToken();
/**
* Get the access token.
*
@ -64,6 +57,13 @@ public interface Session {
*/
UserType getUserType();
/**
* Get the user's avatar
*
* @return User's avatar as a base64 string.
*/
byte[] getAvatarImage();
/**
* Return true if the user is in an online session.
*
@ -71,4 +71,20 @@ public interface Session {
*/
boolean isOnline();
/**
* Convert this session to a saved session
* @return Saved session that represents this active session
*/
default SavedSession toSavedSession() {
SavedSession savedSession = new SavedSession();
savedSession.setType(getUserType());
savedSession.setUsername(getName());
savedSession.setUuid(getUuid());
savedSession.setAccessToken(getAccessToken());
savedSession.setAvatarImage(getAvatarImage());
return savedSession;
}
}

View File

@ -18,7 +18,11 @@ public enum UserType {
/**
* Mojang accounts login with an email address.
*/
MOJANG;
MOJANG,
/**
* Microsoft accounts login via OAuth.
*/
MICROSOFT;
/**
* Return a lowercase version of the enum type.

View File

@ -0,0 +1,19 @@
package com.skcraft.launcher.auth;
import com.skcraft.launcher.util.HttpRequest;
import java.io.IOException;
import static com.skcraft.launcher.util.HttpRequest.url;
public class VisageSkinService {
public static byte[] fetchSkinHead(String uuid) throws IOException, InterruptedException {
String skinUrl = String.format("https://visage.surgeplay.com/face/32/%s.png", uuid);
return HttpRequest.get(url(skinUrl))
.execute()
.expectResponseCode(200)
.returnContent()
.asBytes();
}
}

View File

@ -9,47 +9,60 @@ package com.skcraft.launcher.auth;
import com.fasterxml.jackson.annotation.*;
import com.skcraft.launcher.util.HttpRequest;
import lombok.Data;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import java.io.IOException;
import java.net.URL;
import java.util.Collections;
import java.util.List;
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;
/**
* Create a new login service with the given authentication URL.
*
* @param authUrl the authentication URL
*/
public YggdrasilLoginService(@NonNull URL authUrl) {
this.authUrl = authUrl;
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 List<? extends Session> login(String agent, String id, String password)
public Session restore(SavedSession savedSession)
throws IOException, InterruptedException, AuthenticationException {
Object payload = new AuthenticatePayload(new Agent(agent), id, password);
RefreshPayload payload = new RefreshPayload(savedSession.getAccessToken(), clientId);
HttpRequest request = HttpRequest
.post(authUrl)
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 (request.getResponseCode() != 200) {
ErrorResponse error = request.returnContent().asJson(ErrorResponse.class);
throw new AuthenticationException(error.getErrorMessage(), error.getErrorMessage());
if (req.getResponseCode() != 200) {
ErrorResponse error = req.returnContent().asJson(ErrorResponse.class);
throw new AuthenticationException(error.getErrorMessage());
} else {
AuthenticateResponse response = request.returnContent().asJson(AuthenticateResponse.class);
return response.getAvailableProfiles();
AuthenticateResponse response = req.returnContent().asJson(AuthenticateResponse.class);
Profile profile = response.getSelectedProfile();
if (previous != null && previous.getAvatarImage() != null) {
profile.setAvatarImage(previous.getAvatarImage());
} else {
profile.setAvatarImage(VisageSkinService.fetchSkinHead(profile.getUuid()));
}
return profile;
}
}
@ -64,6 +77,14 @@ public class YggdrasilLoginService implements LoginService {
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
@ -71,8 +92,7 @@ public class YggdrasilLoginService implements LoginService {
private static class AuthenticateResponse {
private String accessToken;
private String clientToken;
@JsonManagedReference private List<Profile> availableProfiles;
private Profile selectedProfile;
@JsonManagedReference private Profile selectedProfile;
}
@Data
@ -92,6 +112,7 @@ public class YggdrasilLoginService implements LoginService {
@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;
@ -101,12 +122,6 @@ public class YggdrasilLoginService implements LoginService {
return String.format("token:%s:%s", getAccessToken(), getUuid());
}
@Override
@JsonIgnore
public String getClientToken() {
return response.getClientToken();
}
@Override
@JsonIgnore
public String getAccessToken() {

View File

@ -0,0 +1,58 @@
package com.skcraft.launcher.auth.microsoft;
import com.skcraft.launcher.auth.AuthenticationException;
import com.skcraft.launcher.util.HttpRequest;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.awt.*;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
/**
* Handles the Microsoft leg of OAuth authorization.
*/
@RequiredArgsConstructor
public class MicrosoftWebAuthorizer {
private final String clientId;
@Getter private String redirectUri;
public OauthResult authorize() throws IOException, AuthenticationException, InterruptedException {
if (Desktop.isDesktopSupported()) {
// Interactive auth
return authorizeInteractive();
} else {
// TODO Device code auth
return null;
}
}
private OauthResult authorizeInteractive() throws IOException, AuthenticationException, InterruptedException {
OauthHttpHandler httpHandler = new OauthHttpHandler();
Desktop.getDesktop().browse(generateInteractiveUrl(httpHandler.getPort()));
return httpHandler.await();
}
private URI generateInteractiveUrl(int port) throws AuthenticationException {
redirectUri = "http://localhost:" + port;
URI interactive;
try {
HttpRequest.Form query = HttpRequest.Form.form();
query.add("client_id", clientId);
query.add("scope", "XboxLive.signin XboxLive.offline_access");
query.add("response_type", "code");
query.add("redirect_uri", redirectUri);
query.add("prompt", "select_account");
interactive = new URI("https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?"
+ query.toString());
} catch (URISyntaxException e) {
throw new AuthenticationException(e, "Failed to generate OAuth URL");
}
return interactive;
}
}

View File

@ -0,0 +1,48 @@
package com.skcraft.launcher.auth.microsoft;
import com.skcraft.launcher.auth.AuthenticationException;
import com.skcraft.launcher.auth.microsoft.model.*;
import com.skcraft.launcher.util.HttpRequest;
import com.skcraft.launcher.util.SharedLocale;
import java.io.IOException;
import java.net.URL;
import static com.skcraft.launcher.util.HttpRequest.url;
public class MinecraftServicesAuthorizer {
private static final URL MC_SERVICES_LOGIN = url("https://api.minecraftservices.com/authentication/login_with_xbox");
private static final URL MC_SERVICES_PROFILE = url("https://api.minecraftservices.com/minecraft/profile");
public static McAuthResponse authorizeWithMinecraft(XboxAuthorization auth) throws IOException, InterruptedException {
McAuthRequest request = new McAuthRequest("XBL3.0 x=" + auth.getCombinedToken());
return HttpRequest.post(MC_SERVICES_LOGIN)
.bodyJson(request)
.header("Accept", "application/json")
.execute()
.expectResponseCode(200)
.returnContent()
.asJson(McAuthResponse.class);
}
public static McProfileResponse getUserProfile(McAuthResponse auth)
throws IOException, InterruptedException, AuthenticationException {
return HttpRequest.get(MC_SERVICES_PROFILE)
.header("Authorization", auth.getAuthorization())
.execute()
.expectResponseCodeOr(200, req -> {
McServicesError error = req.returnContent().asJson(McServicesError.class);
if (error.getError().equals("NOT_FOUND")) {
return new AuthenticationException("No Minecraft profile",
SharedLocale.tr("login.minecraftNotOwnedError"));
}
return new AuthenticationException(error.getErrorMessage(),
SharedLocale.tr("login.minecraft.error", error.getErrorMessage()));
})
.returnContent()
.asJson(McProfileResponse.class);
}
}

View File

@ -0,0 +1,66 @@
package com.skcraft.launcher.auth.microsoft;
import com.google.common.base.Charsets;
import com.google.common.base.Splitter;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import lombok.extern.java.Log;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
@Log
public class OauthHttpHandler {
private Executor executor = Executors.newCachedThreadPool();
private HttpServer server;
private OauthResult result;
public OauthHttpHandler() throws IOException {
server = HttpServer.create(new InetSocketAddress("localhost", 0), 0);
server.createContext("/", new Handler());
server.setExecutor(executor);
server.start();
}
public int getPort() {
return server.getAddress().getPort();
}
public OauthResult await() throws InterruptedException {
synchronized (this) {
this.wait();
}
server.stop(3);
return result;
}
private class Handler implements HttpHandler {
@Override
public void handle(HttpExchange httpExchange) throws IOException {
String query = httpExchange.getRequestURI().getQuery();
Map<String, String> qs = Splitter.on('&').withKeyValueSeparator('=').split(query);
if (qs.get("error") != null) {
result = new OauthResult.Error(qs.get("error_description"));
} else {
result = new OauthResult.Success(qs.get("code"));
}
synchronized (OauthHttpHandler.this) {
OauthHttpHandler.this.notifyAll();
}
byte[] response = "OK: you can close the browser now".getBytes(Charsets.UTF_8);
httpExchange.sendResponseHeaders(200, response.length);
httpExchange.getResponseBody().write(response);
httpExchange.getResponseBody().flush();
httpExchange.getResponseBody().close();
}
}
}

View File

@ -0,0 +1,28 @@
package com.skcraft.launcher.auth.microsoft;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
public interface OauthResult {
boolean isError();
@RequiredArgsConstructor
class Success implements OauthResult {
@Getter private final String authCode;
@Override
public boolean isError() {
return false;
}
}
@RequiredArgsConstructor
class Error implements OauthResult {
@Getter private final String errorMessage;
@Override
public boolean isError() {
return true;
}
}
}

View File

@ -0,0 +1,65 @@
package com.skcraft.launcher.auth.microsoft;
import com.skcraft.launcher.auth.AuthenticationException;
import com.skcraft.launcher.auth.microsoft.model.*;
import com.skcraft.launcher.util.HttpRequest;
import com.skcraft.launcher.util.SharedLocale;
import java.io.IOException;
import java.net.URL;
import static com.skcraft.launcher.util.HttpRequest.url;
public class XboxTokenAuthorizer {
private static final URL XBL_AUTHENTICATE_URL = url("https://user.auth.xboxlive.com/user/authenticate");
private static final URL XSTS_AUTHENTICATE_URL = url("https://xsts.auth.xboxlive.com/xsts/authorize");
public static XboxAuthorization authorizeWithXbox(String accessToken)
throws IOException, InterruptedException, AuthenticationException {
XboxAuthRequest<XblAuthProperties> xblPayload =
new XboxAuthRequest<>(new XblAuthProperties("d=" + accessToken));
XboxAuthResponse xblResponse = HttpRequest.post(XBL_AUTHENTICATE_URL)
.bodyJson(xblPayload)
.header("Accept", "application/json")
.execute()
.expectResponseCodeOr(200, (req) ->
new AuthenticationException("Error authenticating with Xbox Live",
SharedLocale.tr("login.xbox.generic")))
.returnContent()
.asJson(XboxAuthResponse.class);
XboxAuthRequest<XstsAuthProperties> xstsPayload =
new XboxAuthRequest<>(new XstsAuthProperties(xblResponse.getToken()));
xstsPayload.setRelyingParty("rp://api.minecraftservices.com/");
XboxAuthResponse xstsResponse = HttpRequest.post(XSTS_AUTHENTICATE_URL)
.bodyJson(xstsPayload)
.header("Accept", "application/json")
.execute()
.expectResponseCodeOr(200, (req) -> {
XstsError xstsError = req.returnContent().asJson(XstsError.class);
return new AuthenticationException(xstsError.getMessage(), getErrorMessage(xstsError));
})
.returnContent()
.asJson(XboxAuthResponse.class);
return new XboxAuthorization(xstsResponse.getToken(), xstsResponse.getUhs());
}
private static String getErrorMessage(XstsError xstsError) {
long xboxErrorCode = xstsError.getXErr();
if (xboxErrorCode == 2148916233L) {
return SharedLocale.tr("login.xbox.noXboxAccount");
}
if (xboxErrorCode == 2148916238L) {
return SharedLocale.tr("login.xbox.isChild");
}
if (!xstsError.getMessage().isEmpty()) {
return SharedLocale.tr("login.xbox.errorMessage", xstsError.getMessage());
}
return SharedLocale.tr("login.xbox.unknown", xboxErrorCode);
}
}

View File

@ -0,0 +1,8 @@
package com.skcraft.launcher.auth.microsoft.model;
import lombok.Data;
@Data
public class McAuthRequest {
private final String identityToken;
}

View File

@ -0,0 +1,21 @@
package com.skcraft.launcher.auth.microsoft.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.Data;
@Data
@JsonNaming(PropertyNamingStrategy.LowerCaseWithUnderscoresStrategy.class)
@JsonIgnoreProperties(ignoreUnknown = true)
public class McAuthResponse {
private String accessToken;
private String tokenType;
private int expiresIn;
@JsonIgnore
public String getAuthorization() {
return String.format("%s %s", tokenType, accessToken);
}
}

View File

@ -0,0 +1,12 @@
package com.skcraft.launcher.auth.microsoft.model;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class McProfileResponse {
@JsonProperty("id") private String uuid;
private String name;
}

View File

@ -0,0 +1,11 @@
package com.skcraft.launcher.auth.microsoft.model;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class McServicesError {
private String error;
private String errorMessage;
}

View File

@ -0,0 +1,15 @@
package com.skcraft.launcher.auth.microsoft.model;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.Data;
@Data
@JsonNaming(PropertyNamingStrategy.LowerCaseWithUnderscoresStrategy.class)
@JsonIgnoreProperties(ignoreUnknown = true)
public class TokenResponse {
private String tokenType;
private String accessToken;
private String refreshToken;
}

View File

@ -0,0 +1,15 @@
package com.skcraft.launcher.auth.microsoft.model;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.Data;
import lombok.NonNull;
@Data
@JsonNaming(PropertyNamingStrategy.PascalCaseStrategy.class)
public class XblAuthProperties {
private String authMethod = "RPS";
private String siteName = "user.auth.xboxlive.com";
@NonNull
private String rpsTicket;
}

View File

@ -0,0 +1,14 @@
package com.skcraft.launcher.auth.microsoft.model;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.Data;
import lombok.NonNull;
@Data
@JsonNaming(PropertyNamingStrategy.PascalCaseStrategy.class)
public class XboxAuthRequest<T> {
@NonNull private T properties;
private String relyingParty = "http://auth.xboxlive.com";
private String tokenType = "JWT";
}

View File

@ -0,0 +1,32 @@
package com.skcraft.launcher.auth.microsoft.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.Data;
import java.util.List;
@Data
@JsonNaming(PropertyNamingStrategy.PascalCaseStrategy.class)
@JsonIgnoreProperties(ignoreUnknown = true)
public class XboxAuthResponse {
private String token;
private DisplayClaims displayClaims;
@JsonIgnore
public String getUhs() {
return getDisplayClaims().getXui().get(0).getUhs();
}
@Data
public static class DisplayClaims {
private List<UhsContainer> xui;
}
@Data
public static class UhsContainer {
private String uhs;
}
}

View File

@ -0,0 +1,13 @@
package com.skcraft.launcher.auth.microsoft.model;
import lombok.Data;
@Data
public class XboxAuthorization {
private final String token;
private final String uhs;
public String getCombinedToken() {
return String.format("%s;%s", uhs, token);
}
}

View File

@ -0,0 +1,19 @@
package com.skcraft.launcher.auth.microsoft.model;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.Data;
import java.util.Collections;
import java.util.List;
@Data
@JsonNaming(PropertyNamingStrategy.PascalCaseStrategy.class)
public class XstsAuthProperties {
private String sandboxId = "RETAIL";
private List<String> userTokens;
public XstsAuthProperties(String token) {
this.userTokens = Collections.singletonList(token);
}
}

View File

@ -0,0 +1,17 @@
package com.skcraft.launcher.auth.microsoft.model;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.Data;
@Data
@JsonNaming(PropertyNamingStrategy.PascalCaseStrategy.class)
@JsonIgnoreProperties(ignoreUnknown = true)
public class XstsError {
@JsonProperty("XErr")
private long xErr;
private String message;
private String redirect;
}

View File

@ -0,0 +1,242 @@
package com.skcraft.launcher.dialog;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.skcraft.concurrency.ObservableFuture;
import com.skcraft.concurrency.ProgressObservable;
import com.skcraft.concurrency.SettableProgress;
import com.skcraft.launcher.Launcher;
import com.skcraft.launcher.auth.LoginService;
import com.skcraft.launcher.auth.OfflineSession;
import com.skcraft.launcher.auth.SavedSession;
import com.skcraft.launcher.auth.Session;
import com.skcraft.launcher.persistence.Persistence;
import com.skcraft.launcher.swing.LinedBoxPanel;
import com.skcraft.launcher.swing.SwingHelper;
import com.skcraft.launcher.util.SharedLocale;
import com.skcraft.launcher.util.SwingExecutor;
import lombok.RequiredArgsConstructor;
import javax.swing.*;
import java.awt.*;
import java.util.concurrent.Callable;
public class AccountSelectDialog extends JDialog {
private final JList<SavedSession> accountList;
private final JButton loginButton = new JButton(SharedLocale.tr("accounts.play"));
private final JButton cancelButton = new JButton(SharedLocale.tr("button.cancel"));
private final JButton addMojangButton = new JButton(SharedLocale.tr("accounts.addMojang"));
private final JButton addMicrosoftButton = new JButton(SharedLocale.tr("accounts.addMicrosoft"));
private final JButton removeSelected = new JButton(SharedLocale.tr("accounts.removeSelected"));
private final JButton offlineButton = new JButton(SharedLocale.tr("login.playOffline"));
private final LinedBoxPanel buttonsPanel = new LinedBoxPanel(true);
private final Launcher launcher;
private Session selected;
public AccountSelectDialog(Window owner, Launcher launcher) {
super(owner, ModalityType.DOCUMENT_MODAL);
this.launcher = launcher;
this.accountList = new JList<>(launcher.getAccounts());
setTitle(SharedLocale.tr("accounts.title"));
initComponents();
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
setMinimumSize(new Dimension(350, 170));
setResizable(false);
pack();
setLocationRelativeTo(owner);
}
private void initComponents() {
setLayout(new BorderLayout());
accountList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
accountList.setLayoutOrientation(JList.VERTICAL);
accountList.setVisibleRowCount(0);
accountList.setCellRenderer(new AccountRenderer());
JScrollPane accountPane = new JScrollPane(accountList);
accountPane.setPreferredSize(new Dimension(250, 100));
accountPane.setAlignmentX(CENTER_ALIGNMENT);
loginButton.setFont(loginButton.getFont().deriveFont(Font.BOLD));
loginButton.setMargin(new Insets(0, 10, 0, 10));
buttonsPanel.setBorder(BorderFactory.createEmptyBorder(26, 13, 13, 13));
if (launcher.getConfig().isOfflineEnabled()) {
buttonsPanel.addElement(offlineButton);
}
buttonsPanel.addGlue();
buttonsPanel.addElement(loginButton);
buttonsPanel.addElement(cancelButton);
LinedBoxPanel loginButtonsRow = new LinedBoxPanel(true);
loginButtonsRow.add(addMojangButton);
loginButtonsRow.add(addMicrosoftButton);
loginButtonsRow.addGlue();
loginButtonsRow.add(removeSelected);
loginButtonsRow.setAlignmentX(CENTER_ALIGNMENT);
loginButtonsRow.setBorder(null);
JPanel listPane = new JPanel();
listPane.setLayout(new BoxLayout(listPane, BoxLayout.Y_AXIS));
listPane.add(accountPane);
listPane.add(Box.createVerticalStrut(5));
listPane.add(loginButtonsRow);
listPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
listPane.setAlignmentX(CENTER_ALIGNMENT);
add(listPane, BorderLayout.CENTER);
add(buttonsPanel, BorderLayout.SOUTH);
loginButton.addActionListener(ev -> attemptExistingLogin(accountList.getSelectedValue()));
cancelButton.addActionListener(ev -> dispose());
addMojangButton.addActionListener(ev -> {
Session newSession = LoginDialog.showLoginRequest(this, launcher);
if (newSession != null) {
launcher.getAccounts().add(newSession.toSavedSession());
setResult(newSession);
}
});
addMicrosoftButton.addActionListener(ev -> attemptMicrosoftLogin());
offlineButton.addActionListener(ev ->
setResult(new OfflineSession(launcher.getProperties().getProperty("offlinePlayerName"))));
removeSelected.addActionListener(ev -> {
if (accountList.getSelectedValue() != null) {
boolean confirmed = SwingHelper.confirmDialog(this, SharedLocale.tr("accounts.confirmForget"),
SharedLocale.tr("accounts.confirmForgetTitle"));
if (confirmed) {
launcher.getAccounts().remove(accountList.getSelectedValue());
}
}
});
accountList.setSelectedIndex(0);
}
@Override
public void dispose() {
accountList.setModel(new DefaultListModel<>());
super.dispose();
}
public static Session showAccountRequest(Window owner, Launcher launcher) {
AccountSelectDialog dialog = new AccountSelectDialog(owner, launcher);
dialog.setVisible(true);
if (dialog.selected != null && dialog.selected.isOnline()) {
launcher.getAccounts().update(dialog.selected.toSavedSession());
}
Persistence.commitAndForget(launcher.getAccounts());
return dialog.selected;
}
private void setResult(Session result) {
this.selected = result;
dispose();
}
private void attemptMicrosoftLogin() {
String status = SharedLocale.tr("login.microsoft.seeBrowser");
SettableProgress progress = new SettableProgress(status, -1);
ListenableFuture<?> future = launcher.getExecutor().submit(() -> {
Session newSession = launcher.getMicrosoftLogin().login(() ->
progress.set(SharedLocale.tr("login.loggingInStatus"), -1));
if (newSession != null) {
launcher.getAccounts().add(newSession.toSavedSession());
setResult(newSession);
}
return null;
});
ProgressDialog.showProgress(this, future, progress,
SharedLocale.tr("login.loggingInTitle"), status);
SwingHelper.addErrorDialogCallback(this, future);
}
private void attemptExistingLogin(SavedSession session) {
if (session == null) return;
LoginService loginService = launcher.getLoginService(session.getType());
RestoreSessionCallable callable = new RestoreSessionCallable(loginService, session);
ObservableFuture<Session> future = new ObservableFuture<>(launcher.getExecutor().submit(callable), callable);
Futures.addCallback(future, new FutureCallback<Session>() {
@Override
public void onSuccess(Session result) {
setResult(result);
}
@Override
public void onFailure(Throwable t) {
t.printStackTrace();
}
}, SwingExecutor.INSTANCE);
ProgressDialog.showProgress(this, future, SharedLocale.tr("login.loggingInTitle"),
SharedLocale.tr("login.loggingInStatus"));
SwingHelper.addErrorDialogCallback(this, future);
}
@RequiredArgsConstructor
private static class RestoreSessionCallable implements Callable<Session>, ProgressObservable {
private final LoginService service;
private final SavedSession session;
@Override
public Session call() throws Exception {
return service.restore(session);
}
@Override
public String getStatus() {
return SharedLocale.tr("accounts.refreshingStatus");
}
@Override
public double getProgress() {
return -1;
}
}
private static class AccountRenderer extends JLabel implements ListCellRenderer<SavedSession> {
public AccountRenderer() {
setHorizontalAlignment(CENTER);
}
@Override
public Component getListCellRendererComponent(JList<? extends SavedSession> list, SavedSession value, int index, boolean isSelected, boolean cellHasFocus) {
setText(value.getUsername());
if (value.getAvatarImage() != null) {
setIcon(new ImageIcon(value.getAvatarImage()));
} else {
setIcon(SwingHelper.createIcon(Launcher.class, "default_skin.png", 32, 32));
}
if (isSelected) {
setOpaque(true);
setBackground(list.getSelectionBackground());
setForeground(list.getSelectionForeground());
} else {
setOpaque(false);
setForeground(list.getForeground());
}
return this;
}
}
}

View File

@ -6,27 +6,28 @@
package com.skcraft.launcher.dialog;
import com.google.common.base.Strings;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.skcraft.concurrency.ObservableFuture;
import com.skcraft.concurrency.ProgressObservable;
import com.skcraft.launcher.Configuration;
import com.skcraft.launcher.Launcher;
import com.skcraft.launcher.auth.*;
import com.skcraft.launcher.swing.*;
import com.skcraft.launcher.auth.AuthenticationException;
import com.skcraft.launcher.auth.Session;
import com.skcraft.launcher.auth.YggdrasilLoginService;
import com.skcraft.launcher.persistence.Persistence;
import com.skcraft.launcher.swing.*;
import com.skcraft.launcher.util.SharedLocale;
import com.skcraft.launcher.util.SwingExecutor;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.IOException;
import java.util.Date;
import java.util.List;
import java.util.concurrent.Callable;
/**
@ -35,16 +36,12 @@ import java.util.concurrent.Callable;
public class LoginDialog extends JDialog {
private final Launcher launcher;
@Getter private final AccountList accounts;
@Getter private Session session;
private final JComboBox idCombo = new JComboBox();
private final JTextField usernameText = new JTextField();
private final JPasswordField passwordText = new JPasswordField();
private final JCheckBox rememberIdCheck = new JCheckBox(SharedLocale.tr("login.rememberId"));
private final JCheckBox rememberPassCheck = new JCheckBox(SharedLocale.tr("login.rememberPassword"));
private final JButton loginButton = new JButton(SharedLocale.tr("login.login"));
private final LinkButton recoverButton = new LinkButton(SharedLocale.tr("login.recoverAccount"));
private final JButton offlineButton = new JButton(SharedLocale.tr("login.playOffline"));
private final JButton cancelButton = new JButton(SharedLocale.tr("button.cancel"));
private final FormPanel formPanel = new FormPanel();
private final LinedBoxPanel buttonsPanel = new LinedBoxPanel(true);
@ -59,7 +56,6 @@ public class LoginDialog extends JDialog {
super(owner, ModalityType.DOCUMENT_MODAL);
this.launcher = launcher;
this.accounts = launcher.getAccounts();
setTitle(SharedLocale.tr("login.title"));
initComponents();
@ -73,39 +69,21 @@ public class LoginDialog extends JDialog {
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent event) {
removeListeners();
dispose();
}
});
}
@SuppressWarnings("unchecked")
private void removeListeners() {
idCombo.setModel(new DefaultComboBoxModel());
}
@SuppressWarnings("unchecked")
private void initComponents() {
idCombo.setModel(getAccounts());
updateSelection();
rememberIdCheck.setBorder(BorderFactory.createEmptyBorder());
rememberPassCheck.setBorder(BorderFactory.createEmptyBorder());
idCombo.setEditable(true);
idCombo.getEditor().selectAll();
usernameText.setEditable(true);
loginButton.setFont(loginButton.getFont().deriveFont(Font.BOLD));
formPanel.addRow(new JLabel(SharedLocale.tr("login.idEmail")), idCombo);
formPanel.addRow(new JLabel(SharedLocale.tr("login.idEmail")), usernameText);
formPanel.addRow(new JLabel(SharedLocale.tr("login.password")), passwordText);
formPanel.addRow(new JLabel(), rememberIdCheck);
formPanel.addRow(new JLabel(), rememberPassCheck);
buttonsPanel.setBorder(BorderFactory.createEmptyBorder(26, 13, 13, 13));
if (launcher.getConfig().isOfflineEnabled()) {
buttonsPanel.addElement(offlineButton);
buttonsPanel.addElement(Box.createHorizontalStrut(2));
}
buttonsPanel.addElement(recoverButton);
buttonsPanel.addGlue();
buttonsPanel.addElement(loginButton);
@ -118,171 +96,30 @@ public class LoginDialog extends JDialog {
passwordText.setComponentPopupMenu(TextFieldPopupMenu.INSTANCE);
idCombo.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
updateSelection();
}
});
idCombo.getEditor().getEditorComponent().addMouseListener(new PopupMouseAdapter() {
@Override
protected void showPopup(MouseEvent e) {
popupManageMenu(e.getComponent(), e.getX(), e.getY());
}
});
recoverButton.addActionListener(
ActionListeners.openURL(recoverButton, launcher.getProperties().getProperty("resetPasswordUrl")));
loginButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
prepareLogin();
}
});
offlineButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
setResult(new OfflineSession(launcher.getProperties().getProperty("offlinePlayerName")));
removeListeners();
dispose();
}
});
cancelButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
removeListeners();
dispose();
}
});
rememberPassCheck.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (rememberPassCheck.isSelected()) {
rememberIdCheck.setSelected(true);
}
}
});
rememberIdCheck.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (!rememberIdCheck.isSelected()) {
rememberPassCheck.setSelected(false);
}
}
});
}
private void popupManageMenu(Component component, int x, int y) {
Object selected = idCombo.getSelectedItem();
JPopupMenu popup = new JPopupMenu();
JMenuItem menuItem;
if (selected != null && selected instanceof Account) {
final Account account = (Account) selected;
menuItem = new JMenuItem(SharedLocale.tr("login.forgetUser"));
menuItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
accounts.remove(account);
Persistence.commitAndForget(accounts);
}
});
popup.add(menuItem);
if (!Strings.isNullOrEmpty(account.getPassword())) {
menuItem = new JMenuItem(SharedLocale.tr("login.forgetPassword"));
menuItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
account.setPassword(null);
Persistence.commitAndForget(accounts);
}
});
popup.add(menuItem);
}
}
menuItem = new JMenuItem(SharedLocale.tr("login.forgetAllPasswords"));
menuItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (SwingHelper.confirmDialog(LoginDialog.this,
SharedLocale.tr("login.confirmForgetAllPasswords"),
SharedLocale.tr("login.forgetAllPasswordsTitle"))) {
accounts.forgetPasswords();
Persistence.commitAndForget(accounts);
}
}
});
popup.add(menuItem);
popup.show(component, x, y);
}
private void updateSelection() {
Object selected = idCombo.getSelectedItem();
if (selected != null && selected instanceof Account) {
Account account = (Account) selected;
String password = account.getPassword();
rememberIdCheck.setSelected(true);
if (!Strings.isNullOrEmpty(password)) {
rememberPassCheck.setSelected(true);
passwordText.setText(password);
} else {
rememberPassCheck.setSelected(false);
}
} else {
passwordText.setText("");
rememberIdCheck.setSelected(true);
rememberPassCheck.setSelected(false);
}
loginButton.addActionListener(e -> prepareLogin());
cancelButton.addActionListener(e -> dispose());
}
@SuppressWarnings("deprecation")
private void prepareLogin() {
Object selected = idCombo.getSelectedItem();
if (selected != null && selected instanceof Account) {
Account account = (Account) selected;
if (!usernameText.getText().isEmpty()) {
String password = passwordText.getText();
if (password == null || password.isEmpty()) {
SwingHelper.showErrorDialog(this, SharedLocale.tr("login.noPasswordError"), SharedLocale.tr("login.noPasswordTitle"));
} else {
if (rememberPassCheck.isSelected()) {
account.setPassword(password);
} else {
account.setPassword(null);
}
if (rememberIdCheck.isSelected()) {
accounts.add(account);
} else {
accounts.remove(account);
}
account.setLastUsed(new Date());
Persistence.commitAndForget(accounts);
attemptLogin(account, password);
attemptLogin(usernameText.getText(), password);
}
} else {
SwingHelper.showErrorDialog(this, SharedLocale.tr("login.noLoginError"), SharedLocale.tr("login.noLoginTitle"));
}
}
private void attemptLogin(Account account, String password) {
LoginCallable callable = new LoginCallable(account, password);
private void attemptLogin(String username, String password) {
LoginCallable callable = new LoginCallable(username, password);
ObservableFuture<Session> future = new ObservableFuture<Session>(
launcher.getExecutor().submit(callable), callable);
@ -303,7 +140,6 @@ public class LoginDialog extends JDialog {
private void setResult(Session session) {
this.session = session;
removeListeners();
dispose();
}
@ -313,23 +149,19 @@ public class LoginDialog extends JDialog {
return dialog.getSession();
}
private class LoginCallable implements Callable<Session>,ProgressObservable {
private final Account account;
@RequiredArgsConstructor
private class LoginCallable implements Callable<Session>, ProgressObservable {
private final String username;
private final String password;
private LoginCallable(Account account, String password) {
this.account = account;
this.password = password;
}
@Override
public Session call() throws AuthenticationException, IOException, InterruptedException {
LoginService service = launcher.getLoginService();
List<? extends Session> identities = service.login(launcher.getProperties().getProperty("agentName"), account.getId(), password);
YggdrasilLoginService service = launcher.getYggdrasil();
Session identity = service.login(username, password);
// The list of identities (profiles in Mojang terms) corresponds to whether the account
// The presence of the identity (profile in Mojang terms) corresponds to whether the account
// owns the game, so we need to check that
if (identities.size() > 0) {
if (identity != null) {
// Set offline enabled flag to true
Configuration config = launcher.getConfig();
if (!config.isOfflineEnabled()) {
@ -337,8 +169,7 @@ public class LoginDialog extends JDialog {
Persistence.commitAndForget(config);
}
Persistence.commitAndForget(getAccounts());
return identities.get(0);
return identity;
} else {
throw new AuthenticationException("Minecraft not owned", SharedLocale.tr("login.minecraftNotOwnedError"));
}

View File

@ -0,0 +1,34 @@
package com.skcraft.launcher.dialog.component;
import com.beust.jcommander.internal.Lists;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import java.util.List;
public class ListListenerReducer implements ListDataListener {
private final List<ListDataListener> listeners = Lists.newArrayList();
@Override
public void intervalAdded(ListDataEvent e) {
listeners.forEach(it -> it.intervalAdded(e));
}
@Override
public void intervalRemoved(ListDataEvent e) {
listeners.forEach(it -> it.intervalRemoved(e));
}
@Override
public void contentsChanged(ListDataEvent e) {
listeners.forEach(it -> it.contentsChanged(e));
}
public void addListDataListener(ListDataListener l) {
listeners.add(l);
}
public void removeListDataListener(ListDataListener l) {
listeners.remove(l);
}
}

View File

@ -13,7 +13,7 @@ import com.skcraft.concurrency.ObservableFuture;
import com.skcraft.launcher.Instance;
import com.skcraft.launcher.Launcher;
import com.skcraft.launcher.auth.Session;
import com.skcraft.launcher.dialog.LoginDialog;
import com.skcraft.launcher.dialog.AccountSelectDialog;
import com.skcraft.launcher.dialog.ProgressDialog;
import com.skcraft.launcher.launch.LaunchOptions.UpdatePolicy;
import com.skcraft.launcher.persistence.Persistence;
@ -61,7 +61,7 @@ public class LaunchSupervisor {
if (options.getSession() != null) {
session = options.getSession();
} else {
session = LoginDialog.showLoginRequest(window, launcher);
session = AccountSelectDialog.showAccountRequest(window, launcher);
if (session == null) {
return;
}

View File

@ -0,0 +1,8 @@
package com.skcraft.launcher.util;
import java.io.IOException;
@FunctionalInterface
public interface HttpFunction<T, V> {
V call(T arg) throws IOException, InterruptedException;
}

View File

@ -202,6 +202,27 @@ public class HttpRequest implements Closeable, ProgressObservable {
throw new IOException("Did not get expected response code, got " + responseCode + " for " + url);
}
/**
* Continue if the response code matches, otherwise call the provided function
* to generate an exception.
*
* @param code HTTP status code to continue on.
* @param onError Function invoked when the code does not match, should return an error that will be thrown.
* @return this object if successful
* @throws Exception either an {@link IOException} on I/O error or a user-defined {@link Exception} subclass
* if the code does not match.
*/
public <E extends Exception> HttpRequest expectResponseCodeOr(int code, HttpFunction<HttpRequest, E> onError)
throws E, IOException, InterruptedException {
int responseCode = getResponseCode();
if (code == responseCode) return this;
E exc = onError.call(this);
close();
throw exc;
}
/**
* Get the response code.
*

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 B

View File

@ -87,27 +87,37 @@ launcher.notInstalledHint=(not installed)
launcher.requiresUpdateHint=(requires update)
launcher.updatePendingHint=(pending update)
login.rememberId=Remember my account in the list
login.rememberPassword=Remember my password
accounts.title=Select the account to play with
accounts.play=Play!
accounts.refreshingStatus=Refreshing login session...
accounts.addMojang=Add Mojang account
accounts.addMicrosoft=Add Microsoft account
accounts.removeSelected=Forget selected account
accounts.confirmForgetTitle=Forget account
accounts.confirmForget=Are you sure that you want to forget that account?
login.login=Login...
login.recoverAccount=Forgot your login?
login.playOffline=Play offline
login.title=Minecraft Login
login.idEmail=ID/Email\:
login.password=Password\:
login.forgetUser=Forget selected user
login.forgetPassword=Forget password
login.forgetAllPasswords=Forget all passwords...
login.confirmForgetAllPasswords=Are you sure that you want to forget all saved passwords?
login.forgetAllPasswordsTitle=Forget passwords
login.noPasswordError=Please enter a password.
login.noPasswordTitle=Missing Password
login.loggingInTitle=Logging in...
login.loggingInStatus=Logging in to Mojang...
login.loggingInStatus=Logging in to Minecraft...
login.noLoginError=Please enter your account details.
login.noLoginTitle=Missing Account
login.minecraftNotOwnedError=Sorry, Minecraft is not owned on that account.
login.microsoft.seeBrowser=Check your browser to login with Microsoft.
login.xbox.generic=Failed to authenticate with Xbox Live.
login.xbox.noXboxAccount=That account does not have an Xbox account associated!
login.xbox.isChild=The account is a child (under 18) and cannot proceed unless it is part of a Family.
login.xbox.unknown=An unknown error occurred while logging in with Xbox (XErr {0})
login.xbox.errorMessage=An unknown error occurred while logging in with Xbox: {0}
login.minecraft.error=An error occurred while authorizing with Minecraft services: {0}
console.title=Messages and Errors
console.launcherConsoleTitle=Launcher Messages
console.uploadLog=Upload Log

View File

@ -5,7 +5,6 @@
#
version=${project.version}
agentName=Minecraft
launcherShortname=SKCLauncher
offlinePlayerName=Player
@ -13,6 +12,7 @@ versionManifestUrl=https://launchermeta.mojang.com/mc/game/version_manifest.json
librariesSource=https://libraries.minecraft.net/
assetsSource=http://resources.download.minecraft.net/
yggdrasilAuthUrl=https://authserver.mojang.com/authenticate
microsoftClientId=d18bb4d8-a27f-4451-a87f-fe6de4436813
resetPasswordUrl=https://minecraft.net/resetpassword
newsUrl=http://update.skcraft.com/template/news.html?version=%s