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>
This commit is contained in:
rtm516 2021-01-11 20:52:02 +00:00 committed by GitHub
parent 6aa74a2322
commit 1c0cc4622a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 338 additions and 153 deletions

View file

@ -132,6 +132,10 @@
<groupId>com.github.steveice10</groupId> <groupId>com.github.steveice10</groupId>
<artifactId>packetlib</artifactId> <artifactId>packetlib</artifactId>
</exclusion> </exclusion>
<exclusion>
<groupId>com.github.steveice10</groupId>
<artifactId>mcauthlib</artifactId>
</exclusion>
</exclusions> </exclusions>
</dependency> </dependency>
<dependency> <dependency>
@ -198,6 +202,11 @@
<version>4.13.1</version> <version>4.13.1</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>com.github.GeyserMC</groupId>
<artifactId>MCAuthLib</artifactId>
<version>0e48a094f2</version>
</dependency>
</dependencies> </dependencies>
<build> <build>

View file

@ -86,6 +86,11 @@ public class GeyserConnector {
public static final String GIT_VERSION = "DEV"; // A fallback for running in IDEs 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 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 static final String IP_REGEX = "\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b";
private final List<GeyserSession> players = new ArrayList<>(); private final List<GeyserSession> players = new ArrayList<>();
@ -101,8 +106,8 @@ public class GeyserConnector {
private final ScheduledExecutorService generalThreadPool; private final ScheduledExecutorService generalThreadPool;
private BedrockServer bedrockServer; private BedrockServer bedrockServer;
private PlatformType platformType; private final PlatformType platformType;
private GeyserBootstrap bootstrap; private final GeyserBootstrap bootstrap;
private Metrics metrics; private Metrics metrics;

View file

@ -118,6 +118,8 @@ public interface GeyserConfiguration {
String getAuthType(); String getAuthType();
boolean isPasswordAuthentication();
boolean isUseProxyProtocol(); boolean isUseProxyProtocol();
} }
@ -125,6 +127,12 @@ public interface GeyserConfiguration {
String getEmail(); String getEmail();
String getPassword(); String getPassword();
/**
* Will be removed after Microsoft accounts are fully migrated
*/
@Deprecated
boolean isMicrosoftAccount();
} }
interface IMetricsInfo { interface IMetricsInfo {

View file

@ -149,17 +149,24 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration
@JsonProperty("auth-type") @JsonProperty("auth-type")
private String authType = "online"; private String authType = "online";
@JsonProperty("allow-password-authentication")
private boolean passwordAuthentication = true;
@JsonProperty("use-proxy-protocol") @JsonProperty("use-proxy-protocol")
private boolean useProxyProtocol = false; private boolean useProxyProtocol = false;
} }
@Getter @Getter
@JsonIgnoreProperties(ignoreUnknown = true) // DO NOT REMOVE THIS! Otherwise, after we remove microsoft-account configs will not load
public static class UserAuthenticationInfo implements IUserAuthenticationInfo { public static class UserAuthenticationInfo implements IUserAuthenticationInfo {
@AsteriskSerializer.Asterisk() @AsteriskSerializer.Asterisk()
private String email; private String email;
@AsteriskSerializer.Asterisk() @AsteriskSerializer.Asterisk()
private String password; private String password;
@JsonProperty("microsoft-account")
private boolean microsoftAccount = false;
} }
@Getter @Getter

View file

@ -161,6 +161,7 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
if (info != null) { if (info != null) {
connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.stored_credentials", session.getAuthData().getName())); connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.stored_credentials", session.getAuthData().getName()));
session.setMicrosoftAccount(info.isMicrosoftAccount());
session.authenticate(info.getEmail(), info.getPassword()); session.authenticate(info.getEmail(), info.getPassword());
// TODO send a message to bedrock user telling them they are connected (if nothing like a motd // TODO send a message to bedrock user telling them they are connected (if nothing like a motd

View file

@ -26,8 +26,12 @@
package org.geysermc.connector.network.session; package org.geysermc.connector.network.session;
import com.github.steveice10.mc.auth.data.GameProfile; 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.InvalidCredentialsException;
import com.github.steveice10.mc.auth.exception.request.RequestException; 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.MinecraftConstants;
import com.github.steveice10.mc.protocol.MinecraftProtocol; import com.github.steveice10.mc.protocol.MinecraftProtocol;
import com.github.steveice10.mc.protocol.data.SubProtocol; import com.github.steveice10.mc.protocol.data.SubProtocol;
@ -110,6 +114,10 @@ public class GeyserSession implements CommandSender {
@Setter @Setter
private BedrockClientData clientData; private BedrockClientData clientData;
@Deprecated
@Setter
private boolean microsoftAccount;
private final SessionPlayerEntity playerEntity; private final SessionPlayerEntity playerEntity;
private PlayerInventory inventory; 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. * 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 daylightCycle = true;
private boolean reducedDebugInfo = false; private boolean reducedDebugInfo = false;
@ -443,139 +450,22 @@ public class GeyserSession implements CommandSender {
new Thread(() -> { new Thread(() -> {
try { try {
if (password != null && !password.isEmpty()) { 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 { } else {
protocol = new MinecraftProtocol(username); protocol = new MinecraftProtocol(username);
} }
boolean floodgate = connector.getAuthType() == AuthType.FLOODGATE; connectDownstream();
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);
} catch (InvalidCredentialsException | IllegalArgumentException e) { } catch (InvalidCredentialsException | IllegalArgumentException e) {
connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.login.invalid", username)); connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.login.invalid", username));
disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.login.invalid.kick", getClientData().getLanguageCode())); disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.login.invalid.kick", getClientData().getLanguageCode()));
@ -585,6 +475,199 @@ public class GeyserSession implements CommandSender {
}).start(); }).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) { public void disconnect(String reason) {
if (!closed) { if (!closed) {
loggedIn = false; loggedIn = false;
@ -872,6 +955,18 @@ public class GeyserSession implements CommandSender {
reducedDebugInfo = value; 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 * Send a gamerule value to the client
* *

View file

@ -81,7 +81,7 @@ public class JavaPlayerPositionRotationTranslator extends PacketTranslator<Serve
ChunkUtils.updateChunkPosition(session, pos.toInt()); ChunkUtils.updateChunkPosition(session, pos.toInt());
session.getConnector().getLogger().info(LanguageUtils.getLocaleStringLog("geyser.entity.player.spawn", packet.getX(), packet.getY(), packet.getZ())); session.getConnector().getLogger().debug(LanguageUtils.getLocaleStringLog("geyser.entity.player.spawn", packet.getX(), packet.getY(), packet.getZ()));
return; return;
} }

View file

@ -46,17 +46,10 @@ public class JavaUpdateTimeTranslator extends PacketTranslator<ServerUpdateTimeP
session.sendUpstreamPacket(setTimePacket); session.sendUpstreamPacket(setTimePacket);
if (!session.isDaylightCycle() && time >= 0) { if (!session.isDaylightCycle() && time >= 0) {
// Client thinks there is no daylight cycle but there is // Client thinks there is no daylight cycle but there is
setDoDaylightCycleGamerule(session, true); session.setDaylightCycle(true);
} else if (session.isDaylightCycle() && time < 0) { } else if (session.isDaylightCycle() && time < 0) {
// Client thinks there is daylight cycle but there isn't // 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);
}
} }

View file

@ -29,19 +29,18 @@ import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.JsonNodeType; import com.fasterxml.jackson.databind.node.JsonNodeType;
import com.github.steveice10.mc.auth.service.MsaAuthenticationService;
import com.nimbusds.jose.JWSObject; import com.nimbusds.jose.JWSObject;
import com.nukkitx.network.util.Preconditions; import com.nukkitx.network.util.Preconditions;
import com.nukkitx.protocol.bedrock.packet.LoginPacket; import com.nukkitx.protocol.bedrock.packet.LoginPacket;
import com.nukkitx.protocol.bedrock.packet.ServerToClientHandshakePacket; import com.nukkitx.protocol.bedrock.packet.ServerToClientHandshakePacket;
import com.nukkitx.protocol.bedrock.util.EncryptionUtils; import com.nukkitx.protocol.bedrock.util.EncryptionUtils;
import org.geysermc.common.window.CustomFormBuilder; import org.geysermc.common.window.*;
import org.geysermc.common.window.CustomFormWindow;
import org.geysermc.common.window.FormWindow;
import org.geysermc.common.window.SimpleFormWindow;
import org.geysermc.common.window.button.FormButton; import org.geysermc.common.window.button.FormButton;
import org.geysermc.common.window.component.InputComponent; import org.geysermc.common.window.component.InputComponent;
import org.geysermc.common.window.component.LabelComponent; import org.geysermc.common.window.component.LabelComponent;
import org.geysermc.common.window.response.CustomFormResponse; import org.geysermc.common.window.response.CustomFormResponse;
import org.geysermc.common.window.response.ModalFormResponse;
import org.geysermc.common.window.response.SimpleFormResponse; import org.geysermc.common.window.response.SimpleFormResponse;
import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.session.GeyserSession;
@ -156,13 +155,21 @@ public class LoginEncryptionUtils {
session.sendUpstreamPacketImmediately(packet); session.sendUpstreamPacketImmediately(packet);
} }
private static int AUTH_FORM_ID = 1336; private static final int AUTH_MSA_DETAILS_FORM_ID = 1334;
private static int AUTH_DETAILS_FORM_ID = 1337; 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) { 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(); 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)); 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))); window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.btn_disconnect", userLanguage)));
session.sendForm(window, AUTH_FORM_ID); session.sendForm(window, AUTH_FORM_ID);
@ -179,12 +186,33 @@ public class LoginEncryptionUtils {
session.sendForm(window, AUTH_DETAILS_FORM_ID); 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) { public static boolean authenticateFromForm(GeyserSession session, GeyserConnector connector, int formId, String formData) {
WindowCache windowCache = session.getWindowCache(); WindowCache windowCache = session.getWindowCache();
if (!windowCache.getWindows().containsKey(formId)) if (!windowCache.getWindows().containsKey(formId))
return false; 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); FormWindow window = windowCache.getWindows().remove(formId);
window.setResponse(formData.trim()); window.setResponse(formData.trim());
@ -205,16 +233,50 @@ public class LoginEncryptionUtils {
showLoginDetailsWindow(session); showLoginDetailsWindow(session);
} }
} else if (formId == AUTH_FORM_ID && window instanceof SimpleFormWindow) { } 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(); SimpleFormResponse response = (SimpleFormResponse) window.getResponse();
if (response != null) { if (response != null) {
if (response.getClickedButtonId() == 0) { if (isPasswordAuthentication && response.getClickedButtonId() == 0) {
session.setMicrosoftAccount(false);
showLoginDetailsWindow(session); 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())); session.disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.disconnect", session.getLocale()));
} }
} else { } else {
showLoginWindow(session); 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);
}
} }
} }
} }

View file

@ -32,6 +32,9 @@ remote:
port: 25565 port: 25565
# Authentication type. Can be offline, online, or floodgate (see https://github.com/GeyserMC/Geyser/wiki/Floodgate). # Authentication type. Can be offline, online, or floodgate (see https://github.com/GeyserMC/Geyser/wiki/Floodgate).
auth-type: online 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. # Whether to enable PROXY protocol or not while connecting to the server.
# This is useful only when: # This is useful only when:
# 1) Your server supports PROXY protocol (it probably doesn't) # 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 # BedrockAccountUsername: # Your Minecraft: Bedrock Edition username
# email: javaccountemail@example.com # Your Minecraft: Java Edition email # email: javaccountemail@example.com # Your Minecraft: Java Edition email
# password: javaccountpassword123 # Your Minecraft: Java Edition password # password: javaccountpassword123 # Your Minecraft: Java Edition password
# microsoft-account: true # Whether the account is a Mojang or Microsoft account.
# #
# bluerkelp2: # bluerkelp2:
# email: not_really_my_email_address_mr_minecrafter53267@gmail.com # email: not_really_my_email_address_mr_minecrafter53267@gmail.com
# password: "this isn't really my password" # 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. # 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. # Disabling this will prevent command suggestions from being sent and solve freezing for Bedrock clients.

@ -1 +1 @@
Subproject commit 1a00766840baf1f512d98f5a75c177c8bcfba6f3 Subproject commit 6f246c24ddbd543a359d651e706da470fe53ceeb