Mirror von
https://github.com/GeyserMC/Geyser.git
synchronisiert 2024-12-26 16:12:46 +01:00
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>
Dieser Commit ist enthalten in:
Ursprung
0251bb64b8
Commit
d0220a9b71
@ -59,6 +59,7 @@ import org.geysermc.geyser.registry.BlockRegistries;
|
||||
import org.geysermc.geyser.registry.Registries;
|
||||
import org.geysermc.geyser.scoreboard.ScoreboardUpdater;
|
||||
import org.geysermc.geyser.session.GeyserSession;
|
||||
import org.geysermc.geyser.session.PendingMicrosoftAuthentication;
|
||||
import org.geysermc.geyser.session.SessionManager;
|
||||
import org.geysermc.geyser.session.auth.AuthType;
|
||||
import org.geysermc.geyser.skin.FloodgateSkinUploader;
|
||||
@ -125,6 +126,8 @@ public class GeyserImpl implements GeyserApi {
|
||||
|
||||
private Metrics metrics;
|
||||
|
||||
private PendingMicrosoftAuthentication pendingMicrosoftAuthentication;
|
||||
|
||||
private static GeyserImpl instance;
|
||||
|
||||
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.");
|
||||
}
|
||||
|
||||
pendingMicrosoftAuthentication = new PendingMicrosoftAuthentication(config.getPendingAuthenticationTimeout());
|
||||
|
||||
this.newsHandler = new NewsHandler(branch, buildNumber);
|
||||
|
||||
CooldownUtils.setDefaultShowCooldown(config.getShowCooldown());
|
||||
|
@ -100,6 +100,8 @@ public interface GeyserConfiguration {
|
||||
|
||||
IMetricsInfo getMetrics();
|
||||
|
||||
int getPendingAuthenticationTimeout();
|
||||
|
||||
interface IBedrockConfiguration {
|
||||
|
||||
String getAddress();
|
||||
|
@ -141,6 +141,9 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration
|
||||
|
||||
private MetricsInfo metrics = new MetricsInfo();
|
||||
|
||||
@JsonProperty("pending-authentication-timeout")
|
||||
private int pendingAuthenticationTimeout = 120;
|
||||
|
||||
@Getter
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class BedrockConfiguration implements IBedrockConfiguration {
|
||||
|
@ -32,6 +32,7 @@ import com.nukkitx.protocol.bedrock.data.ResourcePackType;
|
||||
import com.nukkitx.protocol.bedrock.packet.*;
|
||||
import com.nukkitx.protocol.bedrock.v471.Bedrock_v471;
|
||||
import org.geysermc.geyser.GeyserImpl;
|
||||
import org.geysermc.geyser.session.PendingMicrosoftAuthentication;
|
||||
import org.geysermc.geyser.session.auth.AuthType;
|
||||
import org.geysermc.geyser.configuration.GeyserConfiguration;
|
||||
import org.geysermc.geyser.session.GeyserSession;
|
||||
@ -199,6 +200,12 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
|
||||
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;
|
||||
}
|
||||
|
@ -26,7 +26,6 @@
|
||||
package org.geysermc.geyser.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;
|
||||
@ -119,7 +118,6 @@ import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
@ -712,65 +710,57 @@ public class GeyserSession implements GeyserConnection, CommandSender {
|
||||
packet.setTime(16000);
|
||||
sendUpstreamPacket(packet);
|
||||
|
||||
// new thread so clients don't timeout
|
||||
MsaAuthenticationService msaAuthenticationService = new MsaAuthenticationService(GeyserImpl.OAUTH_CLIENT_ID);
|
||||
final PendingMicrosoftAuthentication.AuthenticationTask task = geyser.getPendingMicrosoftAuthentication().getOrCreateTask(
|
||||
getAuthData().xuid()
|
||||
);
|
||||
task.setOnline(true);
|
||||
task.resetTimer();
|
||||
|
||||
// Use a future to prevent timeouts as all the authentication is handled sync
|
||||
// This will be changed with the new protocol library.
|
||||
CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
return msaAuthenticationService.getAuthCode();
|
||||
} catch (RequestException e) {
|
||||
throw new CompletionException(e);
|
||||
}
|
||||
}).whenComplete((response, ex) -> {
|
||||
if (ex != null) {
|
||||
ex.printStackTrace();
|
||||
disconnect(ex.toString());
|
||||
return;
|
||||
}
|
||||
LoginEncryptionUtils.buildAndShowMicrosoftCodeWindow(this, response);
|
||||
attemptCodeAuthentication(msaAuthenticationService);
|
||||
});
|
||||
if (task.getAuthentication().isDone()) {
|
||||
onMicrosoftLoginComplete(task);
|
||||
} else {
|
||||
task.getCode().whenComplete((response, ex) -> {
|
||||
boolean connected = !closed;
|
||||
if (ex != null) {
|
||||
if (connected) {
|
||||
geyser.getLogger().error("Failed to get Microsoft auth code", ex);
|
||||
disconnect(ex.toString());
|
||||
}
|
||||
task.cleanup(); // error getting auth code -> clean up immediately
|
||||
} else if (connected) {
|
||||
LoginEncryptionUtils.buildAndShowMicrosoftCodeWindow(this, response);
|
||||
task.getAuthentication().whenComplete((r, $) -> onMicrosoftLoginComplete(task));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll every second to see if the user has successfully signed in
|
||||
*/
|
||||
private void attemptCodeAuthentication(MsaAuthenticationService msaAuthenticationService) {
|
||||
if (loggedIn || closed) {
|
||||
return;
|
||||
public boolean onMicrosoftLoginComplete(PendingMicrosoftAuthentication.AuthenticationTask task) {
|
||||
if (closed) {
|
||||
return false;
|
||||
}
|
||||
CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
msaAuthenticationService.login();
|
||||
GameProfile profile = msaAuthenticationService.getSelectedProfile();
|
||||
if (profile == null) {
|
||||
// Java account is offline
|
||||
disconnect(GeyserLocale.getPlayerLocaleString("geyser.network.remote.invalid_account", clientData.getLanguageCode()));
|
||||
return null;
|
||||
}
|
||||
|
||||
return new MinecraftProtocol(profile, msaAuthenticationService.getAccessToken());
|
||||
} catch (RequestException e) {
|
||||
throw new CompletionException(e);
|
||||
}
|
||||
}).whenComplete((response, ex) -> {
|
||||
if (ex != null) {
|
||||
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;
|
||||
task.cleanup(); // player is online -> remove pending authentication immediately
|
||||
Throwable ex = task.getLoginException();
|
||||
if (ex != null) {
|
||||
geyser.getLogger().error("Failed to log in with Microsoft code!", ex);
|
||||
disconnect(ex.toString());
|
||||
} else {
|
||||
GameProfile selectedProfile = task.getMsaAuthenticationService().getSelectedProfile();
|
||||
if (selectedProfile == null) {
|
||||
disconnect(GeyserLocale.getPlayerLocaleString(
|
||||
"geyser.network.remote.invalid_account",
|
||||
clientData.getLanguageCode()
|
||||
));
|
||||
} else {
|
||||
this.protocol = new MinecraftProtocol(
|
||||
selectedProfile,
|
||||
task.getMsaAuthenticationService().getAccessToken()
|
||||
);
|
||||
connectDownstream();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -970,6 +960,12 @@ public class GeyserSession implements GeyserConnection, CommandSender {
|
||||
geyser.getSessionManager().removeSession(this);
|
||||
upstream.disconnect(reason);
|
||||
}
|
||||
if (authData != null) {
|
||||
PendingMicrosoftAuthentication.AuthenticationTask task = geyser.getPendingMicrosoftAuthentication().getTask(authData.xuid());
|
||||
if (task != null) {
|
||||
task.setOnline(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tickThread != null) {
|
||||
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
@ -48,6 +48,7 @@ import org.geysermc.cumulus.SimpleForm;
|
||||
import org.geysermc.cumulus.response.CustomFormResponse;
|
||||
import org.geysermc.cumulus.response.ModalFormResponse;
|
||||
import org.geysermc.cumulus.response.SimpleFormResponse;
|
||||
import org.geysermc.geyser.text.ChatColor;
|
||||
import org.geysermc.geyser.text.GeyserLocale;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
@ -312,10 +313,23 @@ public class LoginEncryptionUtils {
|
||||
* Shows the code that a user must input into their browser
|
||||
*/
|
||||
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(
|
||||
ModalForm.builder()
|
||||
.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")
|
||||
.button2("%menu.disconnect")
|
||||
.responseHandler((form, responseData) -> {
|
||||
|
@ -81,6 +81,10 @@ floodgate-key-file: key.pem
|
||||
# password: "this isn't really my password"
|
||||
# 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.
|
||||
# Disabling this will prevent command suggestions from being sent and solve freezing for Bedrock clients.
|
||||
command-suggestions: true
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit bdee0d0f3f8a1271cd001f0bd0d672d0010be1db
|
||||
Subproject commit 5db9d29ece0b3d810ae42f6bdc9eeefd76e3d99d
|
Laden…
In neuem Issue referenzieren
Einen Benutzer sperren