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