From 1c0cc4622a04df9f3c3bdccac4c3a600cda0b94e Mon Sep 17 00:00:00 2001 From: rtm516 Date: Mon, 11 Jan 2021 20:52:02 +0000 Subject: [PATCH] Microsoft account authentication (#1808) Microsoft accounts can now use Geyser, while maintaining full backwards compatibility with Mojang accounts. Co-authored-by: Camotoy <20743703+Camotoy@users.noreply.github.com> --- connector/pom.xml | 9 + .../geysermc/connector/GeyserConnector.java | 9 +- .../configuration/GeyserConfiguration.java | 8 + .../GeyserJacksonConfiguration.java | 7 + .../network/UpstreamPacketHandler.java | 1 + .../network/session/GeyserSession.java | 355 +++++++++++------- .../JavaPlayerPositionRotationTranslator.java | 2 +- .../java/world/JavaUpdateTimeTranslator.java | 11 +- .../connector/utils/LoginEncryptionUtils.java | 82 +++- connector/src/main/resources/config.yml | 5 + connector/src/main/resources/languages | 2 +- 11 files changed, 338 insertions(+), 153 deletions(-) diff --git a/connector/pom.xml b/connector/pom.xml index de095a33b..a7001be00 100644 --- a/connector/pom.xml +++ b/connector/pom.xml @@ -132,6 +132,10 @@ com.github.steveice10 packetlib + + com.github.steveice10 + mcauthlib + @@ -198,6 +202,11 @@ 4.13.1 test + + com.github.GeyserMC + MCAuthLib + 0e48a094f2 + diff --git a/connector/src/main/java/org/geysermc/connector/GeyserConnector.java b/connector/src/main/java/org/geysermc/connector/GeyserConnector.java index 5dd00dabe..d61500e04 100644 --- a/connector/src/main/java/org/geysermc/connector/GeyserConnector.java +++ b/connector/src/main/java/org/geysermc/connector/GeyserConnector.java @@ -86,6 +86,11 @@ public class GeyserConnector { public static final String GIT_VERSION = "DEV"; // A fallback for running in IDEs public static final String VERSION = "DEV"; // A fallback for running in IDEs + /** + * Oauth client ID for Microsoft authentication + */ + public static final String OAUTH_CLIENT_ID = "204cefd1-4818-4de1-b98d-513fae875d88"; + private static final String IP_REGEX = "\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b"; private final List players = new ArrayList<>(); @@ -101,8 +106,8 @@ public class GeyserConnector { private final ScheduledExecutorService generalThreadPool; private BedrockServer bedrockServer; - private PlatformType platformType; - private GeyserBootstrap bootstrap; + private final PlatformType platformType; + private final GeyserBootstrap bootstrap; private Metrics metrics; diff --git a/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java b/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java index d21893f89..e21aa6bb8 100644 --- a/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java +++ b/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java @@ -118,6 +118,8 @@ public interface GeyserConfiguration { String getAuthType(); + boolean isPasswordAuthentication(); + boolean isUseProxyProtocol(); } @@ -125,6 +127,12 @@ public interface GeyserConfiguration { String getEmail(); String getPassword(); + + /** + * Will be removed after Microsoft accounts are fully migrated + */ + @Deprecated + boolean isMicrosoftAccount(); } interface IMetricsInfo { diff --git a/connector/src/main/java/org/geysermc/connector/configuration/GeyserJacksonConfiguration.java b/connector/src/main/java/org/geysermc/connector/configuration/GeyserJacksonConfiguration.java index 3a8946e00..7c9532ff8 100644 --- a/connector/src/main/java/org/geysermc/connector/configuration/GeyserJacksonConfiguration.java +++ b/connector/src/main/java/org/geysermc/connector/configuration/GeyserJacksonConfiguration.java @@ -149,17 +149,24 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration @JsonProperty("auth-type") private String authType = "online"; + @JsonProperty("allow-password-authentication") + private boolean passwordAuthentication = true; + @JsonProperty("use-proxy-protocol") private boolean useProxyProtocol = false; } @Getter + @JsonIgnoreProperties(ignoreUnknown = true) // DO NOT REMOVE THIS! Otherwise, after we remove microsoft-account configs will not load public static class UserAuthenticationInfo implements IUserAuthenticationInfo { @AsteriskSerializer.Asterisk() private String email; @AsteriskSerializer.Asterisk() private String password; + + @JsonProperty("microsoft-account") + private boolean microsoftAccount = false; } @Getter diff --git a/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java b/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java index a6a369e45..3922a95ff 100644 --- a/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java +++ b/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java @@ -161,6 +161,7 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { if (info != null) { connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.stored_credentials", session.getAuthData().getName())); + session.setMicrosoftAccount(info.isMicrosoftAccount()); session.authenticate(info.getEmail(), info.getPassword()); // TODO send a message to bedrock user telling them they are connected (if nothing like a motd diff --git a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java index 8a0362663..a82ea061e 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java @@ -26,8 +26,12 @@ package org.geysermc.connector.network.session; import com.github.steveice10.mc.auth.data.GameProfile; +import com.github.steveice10.mc.auth.exception.request.AuthPendingException; import com.github.steveice10.mc.auth.exception.request.InvalidCredentialsException; import com.github.steveice10.mc.auth.exception.request.RequestException; +import com.github.steveice10.mc.auth.service.AuthenticationService; +import com.github.steveice10.mc.auth.service.MojangAuthenticationService; +import com.github.steveice10.mc.auth.service.MsaAuthenticationService; import com.github.steveice10.mc.protocol.MinecraftConstants; import com.github.steveice10.mc.protocol.MinecraftProtocol; import com.github.steveice10.mc.protocol.data.SubProtocol; @@ -110,6 +114,10 @@ public class GeyserSession implements CommandSender { @Setter private BedrockClientData clientData; + @Deprecated + @Setter + private boolean microsoftAccount; + private final SessionPlayerEntity playerEntity; private PlayerInventory inventory; @@ -257,7 +265,6 @@ public class GeyserSession implements CommandSender { /** * Controls whether the daylight cycle gamerule has been sent to the client, so the sun/moon remain motionless. */ - @Setter private boolean daylightCycle = true; private boolean reducedDebugInfo = false; @@ -443,139 +450,22 @@ public class GeyserSession implements CommandSender { new Thread(() -> { try { if (password != null && !password.isEmpty()) { - protocol = new MinecraftProtocol(username, password); + AuthenticationService authenticationService; + if (microsoftAccount) { + authenticationService = new MsaAuthenticationService(GeyserConnector.OAUTH_CLIENT_ID); + } else { + authenticationService = new MojangAuthenticationService(); + } + authenticationService.setUsername(username); + authenticationService.setPassword(password); + authenticationService.login(); + + protocol = new MinecraftProtocol(authenticationService); } else { protocol = new MinecraftProtocol(username); } - boolean floodgate = connector.getAuthType() == AuthType.FLOODGATE; - final PublicKey publicKey; - - if (floodgate) { - PublicKey key = null; - try { - key = EncryptionUtil.getKeyFromFile( - connector.getConfig().getFloodgateKeyPath(), - PublicKey.class - ); - } catch (IOException | InvalidKeySpecException | NoSuchAlgorithmException e) { - connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.bad_key"), e); - } - publicKey = key; - } else publicKey = null; - - if (publicKey != null) { - connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.loaded_key")); - } - - // Start ticking - tickThread = connector.getGeneralThreadPool().scheduleAtFixedRate(this::tick, 50, 50, TimeUnit.MILLISECONDS); - - downstream = new Client(remoteServer.getAddress(), remoteServer.getPort(), protocol, new TcpSessionFactory()); - if (connector.getConfig().getRemote().isUseProxyProtocol()) { - downstream.getSession().setFlag(BuiltinFlags.ENABLE_CLIENT_PROXY_PROTOCOL, true); - downstream.getSession().setFlag(BuiltinFlags.CLIENT_PROXIED_ADDRESS, upstream.getAddress()); - } - // Let Geyser handle sending the keep alive - downstream.getSession().setFlag(MinecraftConstants.AUTOMATIC_KEEP_ALIVE_MANAGEMENT, false); - downstream.getSession().addListener(new SessionAdapter() { - @Override - public void packetSending(PacketSendingEvent event) { - //todo move this somewhere else - if (event.getPacket() instanceof HandshakePacket && floodgate) { - String encrypted = ""; - try { - encrypted = EncryptionUtil.encryptBedrockData(publicKey, new BedrockData( - clientData.getGameVersion(), - authData.getName(), - authData.getXboxUUID(), - clientData.getDeviceOS().ordinal(), - clientData.getLanguageCode(), - clientData.getCurrentInputMode().ordinal(), - upstream.getSession().getAddress().getAddress().getHostAddress() - )); - } catch (Exception e) { - connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.encrypt_fail"), e); - } - - HandshakePacket handshakePacket = event.getPacket(); - event.setPacket(new HandshakePacket( - handshakePacket.getProtocolVersion(), - handshakePacket.getHostname() + '\0' + BedrockData.FLOODGATE_IDENTIFIER + '\0' + encrypted, - handshakePacket.getPort(), - handshakePacket.getIntent() - )); - } - } - - @Override - public void connected(ConnectedEvent event) { - loggingIn = false; - loggedIn = true; - if (protocol.getProfile() == null) { - // Java account is offline - disconnect(LanguageUtils.getPlayerLocaleString("geyser.network.remote.invalid_account", clientData.getLanguageCode())); - return; - } - connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.remote.connect", authData.getName(), protocol.getProfile().getName(), remoteServer.getAddress())); - playerEntity.setUuid(protocol.getProfile().getId()); - playerEntity.setUsername(protocol.getProfile().getName()); - - String locale = clientData.getLanguageCode(); - - // Let the user know there locale may take some time to download - // as it has to be extracted from a JAR - if (locale.toLowerCase().equals("en_us") && !LocaleUtils.LOCALE_MAPPINGS.containsKey("en_us")) { - // This should probably be left hardcoded as it will only show for en_us clients - sendMessage("Loading your locale (en_us); if this isn't already downloaded, this may take some time"); - } - - // Download and load the language for the player - LocaleUtils.downloadAndLoadLocale(locale); - } - - @Override - public void disconnected(DisconnectedEvent event) { - loggingIn = false; - loggedIn = false; - connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.remote.disconnect", authData.getName(), remoteServer.getAddress(), event.getReason())); - if (event.getCause() != null) { - event.getCause().printStackTrace(); - } - - upstream.disconnect(MessageTranslator.convertMessageLenient(event.getReason())); - } - - @Override - public void packetReceived(PacketReceivedEvent event) { - if (!closed) { - // Required, or else Floodgate players break with Bukkit chunk caching - if (event.getPacket() instanceof LoginSuccessPacket) { - GameProfile profile = ((LoginSuccessPacket) event.getPacket()).getProfile(); - playerEntity.setUsername(profile.getName()); - playerEntity.setUuid(profile.getId()); - - // Check if they are not using a linked account - if (connector.getAuthType() == AuthType.OFFLINE || playerEntity.getUuid().getMostSignificantBits() == 0) { - SkinManager.handleBedrockSkin(playerEntity, clientData); - } - } - - PacketTranslatorRegistry.JAVA_TRANSLATOR.translate(event.getPacket().getClass(), event.getPacket(), GeyserSession.this); - } - } - - @Override - public void packetError(PacketErrorEvent event) { - connector.getLogger().warning(LanguageUtils.getLocaleStringLog("geyser.network.downstream_error", event.getCause().getMessage())); - if (connector.getConfig().isDebugMode()) - event.getCause().printStackTrace(); - event.setSuppress(true); - } - }); - - downstream.getSession().connect(); - connector.addPlayer(this); + connectDownstream(); } catch (InvalidCredentialsException | IllegalArgumentException e) { connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.login.invalid", username)); disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.login.invalid.kick", getClientData().getLanguageCode())); @@ -585,6 +475,199 @@ public class GeyserSession implements CommandSender { }).start(); } + /** + * Present a form window to the user asking to log in with another web browser + */ + public void authenticateWithMicrosoftCode() { + if (loggedIn) { + connector.getLogger().severe(LanguageUtils.getLocaleStringLog("geyser.auth.already_loggedin", getAuthData().getName())); + return; + } + + loggingIn = true; + // new thread so clients don't timeout + new Thread(() -> { + try { + MsaAuthenticationService msaAuthenticationService = new MsaAuthenticationService(GeyserConnector.OAUTH_CLIENT_ID); + + MsaAuthenticationService.MsCodeResponse response = msaAuthenticationService.getAuthCode(); + LoginEncryptionUtils.showMicrosoftCodeWindow(this, response); + + // This just looks cool + SetTimePacket packet = new SetTimePacket(); + packet.setTime(16000); + sendUpstreamPacket(packet); + + // Wait for the code to validate + attemptCodeAuthentication(msaAuthenticationService); + } catch (InvalidCredentialsException | IllegalArgumentException e) { + connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.login.invalid", getAuthData().getName())); + disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.login.invalid.kick", getClientData().getLanguageCode())); + } catch (RequestException ex) { + ex.printStackTrace(); + } + }).start(); + } + + /** + * Poll every second to see if the user has successfully signed in + */ + private void attemptCodeAuthentication(MsaAuthenticationService msaAuthenticationService) { + if (loggedIn || closed) { + return; + } + try { + msaAuthenticationService.login(); + protocol = new MinecraftProtocol(msaAuthenticationService); + + connectDownstream(); + } catch (RequestException e) { + if (!(e instanceof AuthPendingException)) { + e.printStackTrace(); + } else { + // Wait one second before trying again + connector.getGeneralThreadPool().schedule(() -> attemptCodeAuthentication(msaAuthenticationService), 1, TimeUnit.SECONDS); + } + } + } + + /** + * After getting whatever credentials needed, we attempt to join the Java server. + */ + private void connectDownstream() { + boolean floodgate = connector.getAuthType() == AuthType.FLOODGATE; + final PublicKey publicKey; + + if (floodgate) { + PublicKey key = null; + try { + key = EncryptionUtil.getKeyFromFile( + connector.getConfig().getFloodgateKeyPath(), + PublicKey.class + ); + } catch (IOException | InvalidKeySpecException | NoSuchAlgorithmException e) { + connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.bad_key"), e); + } + publicKey = key; + } else publicKey = null; + + if (publicKey != null) { + connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.loaded_key")); + } + + // Start ticking + tickThread = connector.getGeneralThreadPool().scheduleAtFixedRate(this::tick, 50, 50, TimeUnit.MILLISECONDS); + + downstream = new Client(remoteServer.getAddress(), remoteServer.getPort(), protocol, new TcpSessionFactory()); + if (connector.getConfig().getRemote().isUseProxyProtocol()) { + downstream.getSession().setFlag(BuiltinFlags.ENABLE_CLIENT_PROXY_PROTOCOL, true); + downstream.getSession().setFlag(BuiltinFlags.CLIENT_PROXIED_ADDRESS, upstream.getAddress()); + } + // Let Geyser handle sending the keep alive + downstream.getSession().setFlag(MinecraftConstants.AUTOMATIC_KEEP_ALIVE_MANAGEMENT, false); + downstream.getSession().addListener(new SessionAdapter() { + @Override + public void packetSending(PacketSendingEvent event) { + //todo move this somewhere else + if (event.getPacket() instanceof HandshakePacket && floodgate) { + String encrypted = ""; + try { + encrypted = EncryptionUtil.encryptBedrockData(publicKey, new BedrockData( + clientData.getGameVersion(), + authData.getName(), + authData.getXboxUUID(), + clientData.getDeviceOS().ordinal(), + clientData.getLanguageCode(), + clientData.getCurrentInputMode().ordinal(), + upstream.getSession().getAddress().getAddress().getHostAddress() + )); + } catch (Exception e) { + connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.encrypt_fail"), e); + } + + HandshakePacket handshakePacket = event.getPacket(); + event.setPacket(new HandshakePacket( + handshakePacket.getProtocolVersion(), + handshakePacket.getHostname() + '\0' + BedrockData.FLOODGATE_IDENTIFIER + '\0' + encrypted, + handshakePacket.getPort(), + handshakePacket.getIntent() + )); + } + } + + @Override + public void connected(ConnectedEvent event) { + loggingIn = false; + loggedIn = true; + if (protocol.getProfile() == null) { + // Java account is offline + disconnect(LanguageUtils.getPlayerLocaleString("geyser.network.remote.invalid_account", clientData.getLanguageCode())); + return; + } + connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.remote.connect", authData.getName(), protocol.getProfile().getName(), remoteServer.getAddress())); + playerEntity.setUuid(protocol.getProfile().getId()); + playerEntity.setUsername(protocol.getProfile().getName()); + + String locale = clientData.getLanguageCode(); + + // Let the user know there locale may take some time to download + // as it has to be extracted from a JAR + if (locale.toLowerCase().equals("en_us") && !LocaleUtils.LOCALE_MAPPINGS.containsKey("en_us")) { + // This should probably be left hardcoded as it will only show for en_us clients + sendMessage("Loading your locale (en_us); if this isn't already downloaded, this may take some time"); + } + + // Download and load the language for the player + LocaleUtils.downloadAndLoadLocale(locale); + } + + @Override + public void disconnected(DisconnectedEvent event) { + loggingIn = false; + loggedIn = false; + connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.remote.disconnect", authData.getName(), remoteServer.getAddress(), event.getReason())); + if (event.getCause() != null) { + event.getCause().printStackTrace(); + } + + upstream.disconnect(MessageTranslator.convertMessageLenient(event.getReason())); + } + + @Override + public void packetReceived(PacketReceivedEvent event) { + if (!closed) { + // Required, or else Floodgate players break with Bukkit chunk caching + if (event.getPacket() instanceof LoginSuccessPacket) { + GameProfile profile = ((LoginSuccessPacket) event.getPacket()).getProfile(); + playerEntity.setUsername(profile.getName()); + playerEntity.setUuid(profile.getId()); + + // Check if they are not using a linked account + if (connector.getAuthType() == AuthType.OFFLINE || playerEntity.getUuid().getMostSignificantBits() == 0) { + SkinManager.handleBedrockSkin(playerEntity, clientData); + } + } + + PacketTranslatorRegistry.JAVA_TRANSLATOR.translate(event.getPacket().getClass(), event.getPacket(), GeyserSession.this); + } + } + + @Override + public void packetError(PacketErrorEvent event) { + connector.getLogger().warning(LanguageUtils.getLocaleStringLog("geyser.network.downstream_error", event.getCause().getMessage())); + if (connector.getConfig().isDebugMode()) + event.getCause().printStackTrace(); + event.setSuppress(true); + } + }); + + if (!daylightCycle) { + setDaylightCycle(true); + } + downstream.getSession().connect(); + connector.addPlayer(this); + } + public void disconnect(String reason) { if (!closed) { loggedIn = false; @@ -872,6 +955,18 @@ public class GeyserSession implements CommandSender { reducedDebugInfo = value; } + /** + * Changes the daylight cycle gamerule on the client + * This is used in the login screen along-side normal usage + * + * @param doCycle If the cycle should continue + */ + public void setDaylightCycle(boolean doCycle) { + sendGameRule("dodaylightcycle", doCycle); + // Save the value so we don't have to constantly send a daylight cycle gamerule update + this.daylightCycle = doCycle; + } + /** * Send a gamerule value to the client * diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerPositionRotationTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerPositionRotationTranslator.java index d5d8e7d3c..b379443e3 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerPositionRotationTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerPositionRotationTranslator.java @@ -81,7 +81,7 @@ public class JavaPlayerPositionRotationTranslator extends PacketTranslator= 0) { // Client thinks there is no daylight cycle but there is - setDoDaylightCycleGamerule(session, true); + session.setDaylightCycle(true); } else if (session.isDaylightCycle() && time < 0) { // Client thinks there is daylight cycle but there isn't - setDoDaylightCycleGamerule(session, false); + session.setDaylightCycle(false); } } - - private void setDoDaylightCycleGamerule(GeyserSession session, boolean doCycle) { - session.sendGameRule("dodaylightcycle", doCycle); - // Save the value so we don't have to constantly send a daylight cycle gamerule update - session.setDaylightCycle(doCycle); - } - } diff --git a/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java b/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java index 5c212ba02..fd7ef4e64 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java @@ -29,19 +29,18 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.JsonNodeType; +import com.github.steveice10.mc.auth.service.MsaAuthenticationService; import com.nimbusds.jose.JWSObject; import com.nukkitx.network.util.Preconditions; import com.nukkitx.protocol.bedrock.packet.LoginPacket; import com.nukkitx.protocol.bedrock.packet.ServerToClientHandshakePacket; import com.nukkitx.protocol.bedrock.util.EncryptionUtils; -import org.geysermc.common.window.CustomFormBuilder; -import org.geysermc.common.window.CustomFormWindow; -import org.geysermc.common.window.FormWindow; -import org.geysermc.common.window.SimpleFormWindow; +import org.geysermc.common.window.*; import org.geysermc.common.window.button.FormButton; import org.geysermc.common.window.component.InputComponent; import org.geysermc.common.window.component.LabelComponent; import org.geysermc.common.window.response.CustomFormResponse; +import org.geysermc.common.window.response.ModalFormResponse; import org.geysermc.common.window.response.SimpleFormResponse; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.network.session.GeyserSession; @@ -156,13 +155,21 @@ public class LoginEncryptionUtils { session.sendUpstreamPacketImmediately(packet); } - private static int AUTH_FORM_ID = 1336; - private static int AUTH_DETAILS_FORM_ID = 1337; + private static final int AUTH_MSA_DETAILS_FORM_ID = 1334; + private static final int AUTH_MSA_CODE_FORM_ID = 1335; + private static final int AUTH_FORM_ID = 1336; + private static final int AUTH_DETAILS_FORM_ID = 1337; public static void showLoginWindow(GeyserSession session) { + // Set DoDaylightCycle to false so the time doesn't accelerate while we're here + session.setDaylightCycle(false); + String userLanguage = session.getLocale(); SimpleFormWindow window = new SimpleFormWindow(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.title", userLanguage), LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.desc", userLanguage)); - window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.btn_login", userLanguage))); + if (session.getConnector().getConfig().getRemote().isPasswordAuthentication()) { + window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.btn_login.mojang", userLanguage))); + } + window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.btn_login.microsoft", userLanguage))); window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.btn_disconnect", userLanguage))); session.sendForm(window, AUTH_FORM_ID); @@ -179,12 +186,33 @@ public class LoginEncryptionUtils { session.sendForm(window, AUTH_DETAILS_FORM_ID); } + /** + * Prompts the user between either OAuth code login or manual password authentication + */ + public static void showMicrosoftAuthenticationWindow(GeyserSession session) { + String userLanguage = session.getLocale(); + SimpleFormWindow window = new SimpleFormWindow(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.btn_login.microsoft", userLanguage), ""); + window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.method.browser", userLanguage))); + window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.method.password", userLanguage))); // This form won't show if password authentication is disabled + window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.btn_disconnect", userLanguage))); + session.sendForm(window, AUTH_MSA_DETAILS_FORM_ID); + } + + /** + * Shows the code that a user must input into their browser + */ + public static void showMicrosoftCodeWindow(GeyserSession session, MsaAuthenticationService.MsCodeResponse response) { + ModalFormWindow msaCodeWindow = new ModalFormWindow("%xbox.signin", "%xbox.signin.website\n%xbox.signin.url\n%xbox.signin.enterCode\n" + + response.user_code, "Done", "%menu.disconnect"); + session.sendForm(msaCodeWindow, LoginEncryptionUtils.AUTH_MSA_CODE_FORM_ID); + } + public static boolean authenticateFromForm(GeyserSession session, GeyserConnector connector, int formId, String formData) { WindowCache windowCache = session.getWindowCache(); if (!windowCache.getWindows().containsKey(formId)) return false; - if(formId == AUTH_FORM_ID || formId == AUTH_DETAILS_FORM_ID) { + if (formId == AUTH_MSA_DETAILS_FORM_ID || formId == AUTH_FORM_ID || formId == AUTH_DETAILS_FORM_ID || formId == AUTH_MSA_CODE_FORM_ID) { FormWindow window = windowCache.getWindows().remove(formId); window.setResponse(formData.trim()); @@ -205,16 +233,50 @@ public class LoginEncryptionUtils { showLoginDetailsWindow(session); } } else if (formId == AUTH_FORM_ID && window instanceof SimpleFormWindow) { + boolean isPasswordAuthentication = session.getConnector().getConfig().getRemote().isPasswordAuthentication(); + int microsoftButton = isPasswordAuthentication ? 1 : 0; + int disconnectButton = isPasswordAuthentication ? 2 : 1; SimpleFormResponse response = (SimpleFormResponse) window.getResponse(); if (response != null) { - if (response.getClickedButtonId() == 0) { + if (isPasswordAuthentication && response.getClickedButtonId() == 0) { + session.setMicrosoftAccount(false); showLoginDetailsWindow(session); - } else if(response.getClickedButtonId() == 1) { + } else if (response.getClickedButtonId() == microsoftButton) { + session.setMicrosoftAccount(true); + if (isPasswordAuthentication) { + showMicrosoftAuthenticationWindow(session); + } else { + // Just show the OAuth code + session.authenticateWithMicrosoftCode(); + } + } else if (response.getClickedButtonId() == disconnectButton) { session.disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.disconnect", session.getLocale())); } } else { showLoginWindow(session); } + } else if (formId == AUTH_MSA_DETAILS_FORM_ID && window instanceof SimpleFormWindow) { + SimpleFormResponse response = (SimpleFormResponse) window.getResponse(); + if (response != null) { + if (response.getClickedButtonId() == 0) { + session.authenticateWithMicrosoftCode(); + } else if (response.getClickedButtonId() == 1) { + showLoginDetailsWindow(session); + } else if (response.getClickedButtonId() == 2) { + session.disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.disconnect", session.getLocale())); + } + } else { + showLoginWindow(session); + } + } else if (formId == AUTH_MSA_CODE_FORM_ID && window instanceof ModalFormWindow) { + ModalFormResponse response = (ModalFormResponse) window.getResponse(); + if (response != null) { + if (response.getClickedButtonId() == 1) { + session.disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.disconnect", session.getLocale())); + } + } else { + showMicrosoftAuthenticationWindow(session); + } } } } diff --git a/connector/src/main/resources/config.yml b/connector/src/main/resources/config.yml index 228852785..07b73173e 100644 --- a/connector/src/main/resources/config.yml +++ b/connector/src/main/resources/config.yml @@ -32,6 +32,9 @@ remote: port: 25565 # Authentication type. Can be offline, online, or floodgate (see https://github.com/GeyserMC/Geyser/wiki/Floodgate). auth-type: online + # Allow for password-based authentication methods through Geyser. Only useful in online mode. + # If this is false, users must authenticate to Microsoft using a code provided by Geyser on their desktop. + allow-password-authentication: true # Whether to enable PROXY protocol or not while connecting to the server. # This is useful only when: # 1) Your server supports PROXY protocol (it probably doesn't) @@ -52,10 +55,12 @@ floodgate-key-file: public-key.pem # BedrockAccountUsername: # Your Minecraft: Bedrock Edition username # email: javaccountemail@example.com # Your Minecraft: Java Edition email # password: javaccountpassword123 # Your Minecraft: Java Edition password +# microsoft-account: true # Whether the account is a Mojang or Microsoft account. # # bluerkelp2: # email: not_really_my_email_address_mr_minecrafter53267@gmail.com # password: "this isn't really my password" +# microsoft-account: false # Bedrock clients can freeze when opening up the command prompt for the first time if given a lot of commands. # Disabling this will prevent command suggestions from being sent and solve freezing for Bedrock clients. diff --git a/connector/src/main/resources/languages b/connector/src/main/resources/languages index 1a0076684..6f246c24d 160000 --- a/connector/src/main/resources/languages +++ b/connector/src/main/resources/languages @@ -1 +1 @@ -Subproject commit 1a00766840baf1f512d98f5a75c177c8bcfba6f3 +Subproject commit 6f246c24ddbd543a359d651e706da470fe53ceeb