Allow single-device Microsoft authentication (#2688)

By default, there is a two-minute delay if you disconnect so you can authenticate your Microsoft account.

Co-authored-by: rtm516 <rtm516@users.noreply.github.com>
Co-authored-by: Camotoy <20743703+Camotoy@users.noreply.github.com>
This commit is contained in:
turikhay 2022-02-27 01:45:56 +05:00 committed by GitHub
parent 0251bb64b8
commit d0220a9b71
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 259 additions and 57 deletions

View file

@ -59,6 +59,7 @@ import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.scoreboard.ScoreboardUpdater; import org.geysermc.geyser.scoreboard.ScoreboardUpdater;
import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.PendingMicrosoftAuthentication;
import org.geysermc.geyser.session.SessionManager; import org.geysermc.geyser.session.SessionManager;
import org.geysermc.geyser.session.auth.AuthType; import org.geysermc.geyser.session.auth.AuthType;
import org.geysermc.geyser.skin.FloodgateSkinUploader; import org.geysermc.geyser.skin.FloodgateSkinUploader;
@ -125,6 +126,8 @@ public class GeyserImpl implements GeyserApi {
private Metrics metrics; private Metrics metrics;
private PendingMicrosoftAuthentication pendingMicrosoftAuthentication;
private static GeyserImpl instance; private static GeyserImpl instance;
private GeyserImpl(PlatformType platformType, GeyserBootstrap bootstrap) { private GeyserImpl(PlatformType platformType, GeyserBootstrap bootstrap) {
@ -268,6 +271,8 @@ public class GeyserImpl implements GeyserApi {
logger.debug("Not getting git properties for the news handler as we are in a development environment."); logger.debug("Not getting git properties for the news handler as we are in a development environment.");
} }
pendingMicrosoftAuthentication = new PendingMicrosoftAuthentication(config.getPendingAuthenticationTimeout());
this.newsHandler = new NewsHandler(branch, buildNumber); this.newsHandler = new NewsHandler(branch, buildNumber);
CooldownUtils.setDefaultShowCooldown(config.getShowCooldown()); CooldownUtils.setDefaultShowCooldown(config.getShowCooldown());

View file

@ -100,6 +100,8 @@ public interface GeyserConfiguration {
IMetricsInfo getMetrics(); IMetricsInfo getMetrics();
int getPendingAuthenticationTimeout();
interface IBedrockConfiguration { interface IBedrockConfiguration {
String getAddress(); String getAddress();

View file

@ -141,6 +141,9 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration
private MetricsInfo metrics = new MetricsInfo(); private MetricsInfo metrics = new MetricsInfo();
@JsonProperty("pending-authentication-timeout")
private int pendingAuthenticationTimeout = 120;
@Getter @Getter
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
public static class BedrockConfiguration implements IBedrockConfiguration { public static class BedrockConfiguration implements IBedrockConfiguration {

View file

@ -32,6 +32,7 @@ import com.nukkitx.protocol.bedrock.data.ResourcePackType;
import com.nukkitx.protocol.bedrock.packet.*; import com.nukkitx.protocol.bedrock.packet.*;
import com.nukkitx.protocol.bedrock.v471.Bedrock_v471; import com.nukkitx.protocol.bedrock.v471.Bedrock_v471;
import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.session.PendingMicrosoftAuthentication;
import org.geysermc.geyser.session.auth.AuthType; import org.geysermc.geyser.session.auth.AuthType;
import org.geysermc.geyser.configuration.GeyserConfiguration; import org.geysermc.geyser.configuration.GeyserConfiguration;
import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.GeyserSession;
@ -199,6 +200,12 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
return true; return true;
} }
} }
PendingMicrosoftAuthentication.AuthenticationTask task = geyser.getPendingMicrosoftAuthentication().getTask(session.getAuthData().xuid());
if (task != null) {
if (task.getAuthentication().isDone() && session.onMicrosoftLoginComplete(task)) {
return true;
}
}
return false; return false;
} }

View file

@ -26,7 +26,6 @@
package org.geysermc.geyser.session; package org.geysermc.geyser.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.AuthenticationService;
@ -119,7 +118,6 @@ import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.*; import java.util.*;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@ -712,65 +710,57 @@ public class GeyserSession implements GeyserConnection, CommandSender {
packet.setTime(16000); packet.setTime(16000);
sendUpstreamPacket(packet); sendUpstreamPacket(packet);
// new thread so clients don't timeout final PendingMicrosoftAuthentication.AuthenticationTask task = geyser.getPendingMicrosoftAuthentication().getOrCreateTask(
MsaAuthenticationService msaAuthenticationService = new MsaAuthenticationService(GeyserImpl.OAUTH_CLIENT_ID); getAuthData().xuid()
);
task.setOnline(true);
task.resetTimer();
// Use a future to prevent timeouts as all the authentication is handled sync if (task.getAuthentication().isDone()) {
// This will be changed with the new protocol library. onMicrosoftLoginComplete(task);
CompletableFuture.supplyAsync(() -> { } else {
try { task.getCode().whenComplete((response, ex) -> {
return msaAuthenticationService.getAuthCode(); boolean connected = !closed;
} catch (RequestException e) { if (ex != null) {
throw new CompletionException(e); if (connected) {
} geyser.getLogger().error("Failed to get Microsoft auth code", ex);
}).whenComplete((response, ex) -> { disconnect(ex.toString());
if (ex != null) { }
ex.printStackTrace(); task.cleanup(); // error getting auth code -> clean up immediately
disconnect(ex.toString()); } else if (connected) {
return; LoginEncryptionUtils.buildAndShowMicrosoftCodeWindow(this, response);
} task.getAuthentication().whenComplete((r, $) -> onMicrosoftLoginComplete(task));
LoginEncryptionUtils.buildAndShowMicrosoftCodeWindow(this, response); }
attemptCodeAuthentication(msaAuthenticationService); });
}); }
} }
/** public boolean onMicrosoftLoginComplete(PendingMicrosoftAuthentication.AuthenticationTask task) {
* Poll every second to see if the user has successfully signed in if (closed) {
*/ return false;
private void attemptCodeAuthentication(MsaAuthenticationService msaAuthenticationService) {
if (loggedIn || closed) {
return;
} }
CompletableFuture.supplyAsync(() -> { task.cleanup(); // player is online -> remove pending authentication immediately
try { Throwable ex = task.getLoginException();
msaAuthenticationService.login(); if (ex != null) {
GameProfile profile = msaAuthenticationService.getSelectedProfile(); geyser.getLogger().error("Failed to log in with Microsoft code!", ex);
if (profile == null) { disconnect(ex.toString());
// Java account is offline } else {
disconnect(GeyserLocale.getPlayerLocaleString("geyser.network.remote.invalid_account", clientData.getLanguageCode())); GameProfile selectedProfile = task.getMsaAuthenticationService().getSelectedProfile();
return null; if (selectedProfile == null) {
} disconnect(GeyserLocale.getPlayerLocaleString(
"geyser.network.remote.invalid_account",
return new MinecraftProtocol(profile, msaAuthenticationService.getAccessToken()); clientData.getLanguageCode()
} catch (RequestException e) { ));
throw new CompletionException(e); } else {
} this.protocol = new MinecraftProtocol(
}).whenComplete((response, ex) -> { selectedProfile,
if (ex != null) { task.getMsaAuthenticationService().getAccessToken()
if (!(ex instanceof CompletionException completionException) || !(completionException.getCause() instanceof AuthPendingException)) { );
geyser.getLogger().error("Failed to log in with Microsoft code!", ex);
disconnect(ex.toString());
} else {
// Wait one second before trying again
geyser.getScheduledThread().schedule(() -> attemptCodeAuthentication(msaAuthenticationService), 1, TimeUnit.SECONDS);
}
return;
}
if (!closed) {
this.protocol = response;
connectDownstream(); connectDownstream();
return true;
} }
}); }
return false;
} }
/** /**
@ -970,6 +960,12 @@ public class GeyserSession implements GeyserConnection, CommandSender {
geyser.getSessionManager().removeSession(this); geyser.getSessionManager().removeSession(this);
upstream.disconnect(reason); upstream.disconnect(reason);
} }
if (authData != null) {
PendingMicrosoftAuthentication.AuthenticationTask task = geyser.getPendingMicrosoftAuthentication().getTask(authData.xuid());
if (task != null) {
task.setOnline(false);
}
}
} }
if (tickThread != null) { if (tickThread != null) {

View file

@ -0,0 +1,171 @@
/*
* Copyright (c) 2019-2021 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.session;
import com.github.steveice10.mc.auth.exception.request.AuthPendingException;
import com.github.steveice10.mc.auth.exception.request.RequestException;
import com.github.steveice10.mc.auth.service.MsaAuthenticationService;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import lombok.SneakyThrows;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.GeyserLogger;
import javax.annotation.Nonnull;
import java.util.concurrent.*;
/**
* Pending Microsoft authentication task cache.
* It permits user to exit the server while they authorize Geyser to access their Microsoft account.
*/
public class PendingMicrosoftAuthentication {
private final LoadingCache<String, AuthenticationTask> authentications;
public PendingMicrosoftAuthentication(int timeoutSeconds) {
this.authentications = CacheBuilder.newBuilder()
.build(new CacheLoader<>() {
@Override
public AuthenticationTask load(@NonNull String userKey) {
return new AuthenticationTask(userKey, timeoutSeconds * 1000L);
}
});
}
public AuthenticationTask getTask(@Nonnull String userKey) {
return authentications.getIfPresent(userKey);
}
@SneakyThrows(ExecutionException.class)
public AuthenticationTask getOrCreateTask(@Nonnull String userKey) {
return authentications.get(userKey);
}
public class AuthenticationTask {
private static final Executor DELAYED_BY_ONE_SECOND = CompletableFuture.delayedExecutor(1, TimeUnit.SECONDS);
@Getter
private final MsaAuthenticationService msaAuthenticationService = new MsaAuthenticationService(GeyserImpl.OAUTH_CLIENT_ID);
private final String userKey;
private final long timeoutMs;
private long remainingTimeMs;
@Setter
private boolean online = true;
@Getter
private final CompletableFuture<MsaAuthenticationService.MsCodeResponse> code;
@Getter
private final CompletableFuture<MsaAuthenticationService> authentication;
@Getter
private volatile Throwable loginException;
private AuthenticationTask(String userKey, long timeoutMs) {
this.userKey = userKey;
this.timeoutMs = timeoutMs;
this.remainingTimeMs = timeoutMs;
// Request the code
this.code = CompletableFuture.supplyAsync(this::tryGetCode);
this.authentication = new CompletableFuture<>();
// Once the code is received, continuously try to request the access token, profile, etc
this.code.thenRun(() -> performLoginAttempt(System.currentTimeMillis()));
this.authentication.whenComplete((r, ex) -> {
this.loginException = ex;
// avoid memory leak, in case player doesn't connect again
CompletableFuture.delayedExecutor(timeoutMs, TimeUnit.MILLISECONDS).execute(this::cleanup);
});
}
public void resetTimer() {
this.remainingTimeMs = this.timeoutMs;
}
public void cleanup() {
GeyserLogger logger = GeyserImpl.getInstance().getLogger();
if (logger.isDebug()) {
logger.debug("Cleaning up authentication task for " + userKey);
}
authentications.invalidate(userKey);
}
private MsaAuthenticationService.MsCodeResponse tryGetCode() throws CompletionException {
try {
return msaAuthenticationService.getAuthCode();
} catch (RequestException e) {
throw new CompletionException(e);
}
}
private void performLoginAttempt(long lastAttempt) {
CompletableFuture.runAsync(() -> {
try {
msaAuthenticationService.login();
} catch (AuthPendingException e) {
long currentAttempt = System.currentTimeMillis();
if (!online) {
// decrement timer only when player's offline
remainingTimeMs -= currentAttempt - lastAttempt;
if (remainingTimeMs <= 0L) {
// time's up
authentication.completeExceptionally(new TaskTimeoutException());
cleanup();
return;
}
}
// try again in 1 second
performLoginAttempt(currentAttempt);
return;
} catch (Exception e) {
authentication.completeExceptionally(e);
return;
}
// login successful
authentication.complete(msaAuthenticationService);
}, DELAYED_BY_ONE_SECOND);
}
@Override
public String toString() {
return getClass().getSimpleName() + "{userKey='" + userKey + "'}";
}
}
/**
* @see PendingMicrosoftAuthentication
*/
public static class TaskTimeoutException extends Exception {
TaskTimeoutException() {
super("It took too long to authorize Geyser to access your Microsoft account. " +
"Please request new code and try again.");
}
}
}

View file

@ -48,6 +48,7 @@ import org.geysermc.cumulus.SimpleForm;
import org.geysermc.cumulus.response.CustomFormResponse; import org.geysermc.cumulus.response.CustomFormResponse;
import org.geysermc.cumulus.response.ModalFormResponse; import org.geysermc.cumulus.response.ModalFormResponse;
import org.geysermc.cumulus.response.SimpleFormResponse; import org.geysermc.cumulus.response.SimpleFormResponse;
import org.geysermc.geyser.text.ChatColor;
import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.text.GeyserLocale;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
@ -312,10 +313,23 @@ public class LoginEncryptionUtils {
* Shows the code that a user must input into their browser * Shows the code that a user must input into their browser
*/ */
public static void buildAndShowMicrosoftCodeWindow(GeyserSession session, MsaAuthenticationService.MsCodeResponse msCode) { public static void buildAndShowMicrosoftCodeWindow(GeyserSession session, MsaAuthenticationService.MsCodeResponse msCode) {
StringBuilder message = new StringBuilder("%xbox.signin.website\n")
.append(ChatColor.AQUA)
.append("%xbox.signin.url")
.append(ChatColor.RESET)
.append("\n%xbox.signin.enterCode\n")
.append(ChatColor.GREEN)
.append(msCode.user_code);
int timeout = session.getGeyser().getConfig().getPendingAuthenticationTimeout();
if (timeout != 0) {
message.append("\n\n")
.append(ChatColor.RESET)
.append(GeyserLocale.getPlayerLocaleString("geyser.auth.login.timeout", session.getLocale(), String.valueOf(timeout)));
}
session.sendForm( session.sendForm(
ModalForm.builder() ModalForm.builder()
.title("%xbox.signin") .title("%xbox.signin")
.content("%xbox.signin.website\n%xbox.signin.url\n%xbox.signin.enterCode\n" + msCode.user_code) .content(message.toString())
.button1("%gui.done") .button1("%gui.done")
.button2("%menu.disconnect") .button2("%menu.disconnect")
.responseHandler((form, responseData) -> { .responseHandler((form, responseData) -> {

View file

@ -81,6 +81,10 @@ floodgate-key-file: key.pem
# password: "this isn't really my password" # password: "this isn't really my password"
# microsoft-account: false # microsoft-account: false
# Specify how many seconds to wait while user authorizes Geyser to access their Microsoft account.
# User is allowed to disconnect from the server during this period.
pending-authentication-timeout: 120
# 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.
command-suggestions: true command-suggestions: true

@ -1 +1 @@
Subproject commit bdee0d0f3f8a1271cd001f0bd0d672d0010be1db Subproject commit 5db9d29ece0b3d810ae42f6bdc9eeefd76e3d99d