From a85b312b40cb7f819d9cba896463ed3bfb38e4c5 Mon Sep 17 00:00:00 2001 From: Alex <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Wed, 24 Jul 2024 23:06:59 +0200 Subject: [PATCH] Port to MinecraftAuth (#4779) Co-authored-by: Kas-tle <26531652+Kas-tle@users.noreply.github.com> Co-authored-by: onebeastchris Co-authored-by: Camotoy <20743703+Camotoy@users.noreply.github.com> --- bootstrap/mod/fabric/build.gradle.kts | 2 +- core/build.gradle.kts | 5 +- .../java/org/geysermc/geyser/Constants.java | 4 +- .../java/org/geysermc/geyser/GeyserImpl.java | 91 +++++++-- .../geyser/item/type/PlayerHeadItem.java | 2 +- .../geyser/level/block/type/SkullBlock.java | 2 +- .../geyser/network/UpstreamPacketHandler.java | 6 +- .../geyser/session/GeyserSession.java | 192 +++++++++++------- .../PendingMicrosoftAuthentication.java | 136 +++++-------- .../geyser/skin/FakeHeadProvider.java | 16 +- .../translator/item/ItemTranslator.java | 14 +- ...SetLocalPlayerAsInitializedTranslator.java | 4 +- .../java/JavaGameProfileTranslator.java | 2 +- .../JavaPlayerInfoUpdateTranslator.java | 2 +- .../geyser/util/LoginEncryptionUtils.java | 6 +- .../geyser/util/MinecraftAuthLogger.java | 49 +++++ gradle/libs.versions.toml | 6 +- 17 files changed, 320 insertions(+), 219 deletions(-) create mode 100644 core/src/main/java/org/geysermc/geyser/util/MinecraftAuthLogger.java diff --git a/bootstrap/mod/fabric/build.gradle.kts b/bootstrap/mod/fabric/build.gradle.kts index 25bd0af9d..0d083fcf7 100644 --- a/bootstrap/mod/fabric/build.gradle.kts +++ b/bootstrap/mod/fabric/build.gradle.kts @@ -25,7 +25,7 @@ dependencies { shadow(libs.protocol.connection) { isTransitive = false } shadow(libs.protocol.common) { isTransitive = false } shadow(libs.protocol.codec) { isTransitive = false } - shadow(libs.mcauthlib) { isTransitive = false } + shadow(libs.minecraftauth) { isTransitive = false } shadow(libs.raknet) { isTransitive = false } // Consequences of shading + relocating mcauthlib: shadow/relocate mcpl! diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 1d1794cf7..3b5cc3df9 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -25,11 +25,10 @@ dependencies { api(libs.bundles.protocol) - api(libs.mcauthlib) + api(libs.minecraftauth) api(libs.mcprotocollib) { exclude("io.netty", "netty-all") - exclude("com.github.GeyserMC", "packetlib") - exclude("com.github.GeyserMC", "mcauthlib") + exclude("net.raphimc", "MinecraftAuth") } implementation(libs.raknet) { diff --git a/core/src/main/java/org/geysermc/geyser/Constants.java b/core/src/main/java/org/geysermc/geyser/Constants.java index 588b25172..534cb30ad 100644 --- a/core/src/main/java/org/geysermc/geyser/Constants.java +++ b/core/src/main/java/org/geysermc/geyser/Constants.java @@ -39,7 +39,9 @@ public final class Constants { public static final String GEYSER_DOWNLOAD_LOCATION = "https://geysermc.org/download"; public static final String UPDATE_PERMISSION = "geyser.update"; + @Deprecated static final String SAVED_REFRESH_TOKEN_FILE = "saved-refresh-tokens.json"; + static final String SAVED_AUTH_CHAINS_FILE = "saved-auth-chains.json"; public static final String GEYSER_CUSTOM_NAMESPACE = "geyser_custom"; @@ -54,4 +56,4 @@ public final class Constants { } GLOBAL_API_WS_URI = wsUri; } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java index 9ee182edd..8f88f5b6a 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java @@ -29,6 +29,7 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.gson.Gson; import io.netty.channel.epoll.Epoll; import io.netty.util.NettyRuntime; import io.netty.util.concurrent.DefaultThreadFactory; @@ -38,6 +39,8 @@ import lombok.Getter; import lombok.Setter; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; +import net.raphimc.minecraftauth.step.java.session.StepFullJavaSession; +import net.raphimc.minecraftauth.step.msa.StepMsaToken; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -93,6 +96,7 @@ import org.geysermc.geyser.util.AssetUtils; import org.geysermc.geyser.util.CooldownUtils; import org.geysermc.geyser.util.DimensionUtils; import org.geysermc.geyser.util.Metrics; +import org.geysermc.geyser.util.MinecraftAuthLogger; import org.geysermc.geyser.util.NewsHandler; import org.geysermc.geyser.util.VersionCheckUtils; import org.geysermc.geyser.util.WebUtils; @@ -179,7 +183,7 @@ public class GeyserImpl implements GeyserApi { private PendingMicrosoftAuthentication pendingMicrosoftAuthentication; @Getter(AccessLevel.NONE) - private Map savedRefreshTokens; + private Map savedAuthChains; @Getter private static GeyserImpl instance; @@ -552,37 +556,84 @@ public class GeyserImpl implements GeyserApi { if (config.getRemote().authType() == AuthType.ONLINE) { // May be written/read to on multiple threads from each GeyserSession as well as writing the config - savedRefreshTokens = new ConcurrentHashMap<>(); + savedAuthChains = new ConcurrentHashMap<>(); - File tokensFile = bootstrap.getSavedUserLoginsFolder().resolve(Constants.SAVED_REFRESH_TOKEN_FILE).toFile(); - if (tokensFile.exists()) { + // TODO Remove after a while - just a migration help + //noinspection deprecation + File refreshTokensFile = bootstrap.getSavedUserLoginsFolder().resolve(Constants.SAVED_REFRESH_TOKEN_FILE).toFile(); + if (refreshTokensFile.exists()) { + logger.info("Migrating refresh tokens to auth chains..."); + TypeReference> type = new TypeReference<>() { }; + Map refreshTokens = null; + try { + refreshTokens = JSON_MAPPER.readValue(refreshTokensFile, type); + } catch (IOException e) { + // ignored - we'll just delete this file :)) + } + + if (refreshTokens != null) { + List validUsers = config.getSavedUserLogins(); + final Gson gson = new Gson(); + for (Map.Entry entry : refreshTokens.entrySet()) { + String user = entry.getKey(); + if (!validUsers.contains(user)) { + continue; + } + + // Migrate refresh tokens to auth chains + try { + StepFullJavaSession javaSession = PendingMicrosoftAuthentication.AUTH_FLOW.apply(false, 10); + StepFullJavaSession.FullJavaSession fullJavaSession = javaSession.getFromInput( + MinecraftAuthLogger.INSTANCE, + PendingMicrosoftAuthentication.AUTH_CLIENT, + new StepMsaToken.RefreshToken(entry.getValue()) + ); + + String authChain = gson.toJson(javaSession.toJson(fullJavaSession)); + savedAuthChains.put(user, authChain); + } catch (Exception e) { + GeyserImpl.getInstance().getLogger().warning("Could not migrate " + entry.getKey() + " to an auth chain! " + + "They will need to sign in the next time they join Geyser."); + } + + // Ensure the new additions are written to the file + scheduleAuthChainsWrite(); + } + } + + // Finally: Delete it. Goodbye! + refreshTokensFile.delete(); + } + + File authChainsFile = bootstrap.getSavedUserLoginsFolder().resolve(Constants.SAVED_AUTH_CHAINS_FILE).toFile(); + if (authChainsFile.exists()) { TypeReference> type = new TypeReference<>() { }; - Map refreshTokenFile = null; + Map authChainFile = null; try { - refreshTokenFile = JSON_MAPPER.readValue(tokensFile, type); + authChainFile = JSON_MAPPER.readValue(authChainsFile, type); } catch (IOException e) { logger.error("Cannot load saved user tokens!", e); } - if (refreshTokenFile != null) { + if (authChainFile != null) { List validUsers = config.getSavedUserLogins(); boolean doWrite = false; - for (Map.Entry entry : refreshTokenFile.entrySet()) { + for (Map.Entry entry : authChainFile.entrySet()) { String user = entry.getKey(); if (!validUsers.contains(user)) { // Perform a write to this file to purge the now-unused name doWrite = true; continue; } - savedRefreshTokens.put(user, entry.getValue()); + savedAuthChains.put(user, entry.getValue()); } if (doWrite) { - scheduleRefreshTokensWrite(); + scheduleAuthChainsWrite(); } } } } else { - savedRefreshTokens = null; + savedAuthChains = null; } newsHandler.handleNews(null, NewsItemAction.ON_SERVER_STARTED); @@ -829,11 +880,11 @@ public class GeyserImpl implements GeyserApi { } @Nullable - public String refreshTokenFor(@NonNull String bedrockName) { - return savedRefreshTokens.get(bedrockName); + public String authChainFor(@NonNull String bedrockName) { + return savedAuthChains.get(bedrockName); } - public void saveRefreshToken(@NonNull String bedrockName, @NonNull String refreshToken) { + public void saveAuthChain(@NonNull String bedrockName, @NonNull String authChain) { if (!getConfig().getSavedUserLogins().contains(bedrockName)) { // Do not save this login return; @@ -841,8 +892,8 @@ public class GeyserImpl implements GeyserApi { // We can safely overwrite old instances because MsaAuthenticationService#getLoginResponseFromRefreshToken // refreshes the token for us - if (!Objects.equals(refreshToken, savedRefreshTokens.put(bedrockName, refreshToken))) { - scheduleRefreshTokensWrite(); + if (!Objects.equals(authChain, savedAuthChains.put(bedrockName, authChain))) { + scheduleAuthChainsWrite(); } } @@ -852,15 +903,15 @@ public class GeyserImpl implements GeyserApi { } } - private void scheduleRefreshTokensWrite() { + private void scheduleAuthChainsWrite() { scheduledThread.execute(() -> { // Ensure all writes are handled on the same thread - File savedTokens = getBootstrap().getSavedUserLoginsFolder().resolve(Constants.SAVED_REFRESH_TOKEN_FILE).toFile(); + File savedAuthChains = getBootstrap().getSavedUserLoginsFolder().resolve(Constants.SAVED_AUTH_CHAINS_FILE).toFile(); TypeReference> type = new TypeReference<>() { }; - try (FileWriter writer = new FileWriter(savedTokens)) { + try (FileWriter writer = new FileWriter(savedAuthChains)) { JSON_MAPPER.writerFor(type) .withDefaultPrettyPrinter() - .writeValue(writer, savedRefreshTokens); + .writeValue(writer, this.savedAuthChains); } catch (IOException e) { getLogger().error("Unable to write saved refresh tokens!", e); } diff --git a/core/src/main/java/org/geysermc/geyser/item/type/PlayerHeadItem.java b/core/src/main/java/org/geysermc/geyser/item/type/PlayerHeadItem.java index 86572d60c..d4999c80d 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/PlayerHeadItem.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/PlayerHeadItem.java @@ -25,7 +25,7 @@ package org.geysermc.geyser.item.type; -import com.github.steveice10.mc.auth.data.GameProfile; +import org.geysermc.mcprotocollib.auth.GameProfile; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.level.block.type.Block; import org.geysermc.geyser.session.GeyserSession; diff --git a/core/src/main/java/org/geysermc/geyser/level/block/type/SkullBlock.java b/core/src/main/java/org/geysermc/geyser/level/block/type/SkullBlock.java index c4aae46a2..6e791d674 100644 --- a/core/src/main/java/org/geysermc/geyser/level/block/type/SkullBlock.java +++ b/core/src/main/java/org/geysermc/geyser/level/block/type/SkullBlock.java @@ -25,7 +25,7 @@ package org.geysermc.geyser.level.block.type; -import com.github.steveice10.mc.auth.data.GameProfile; +import org.geysermc.mcprotocollib.auth.GameProfile; import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtMapBuilder; diff --git a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java index c7aabb806..f56a8a43f 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -274,10 +274,10 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { private boolean couldLoginUserByName(String bedrockUsername) { if (geyser.getConfig().getSavedUserLogins().contains(bedrockUsername)) { - String refreshToken = geyser.refreshTokenFor(bedrockUsername); - if (refreshToken != null) { + String authChain = geyser.authChainFor(bedrockUsername); + if (authChain != null) { geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.auth.stored_credentials", session.getAuthData().name())); - session.authenticateWithRefreshToken(refreshToken); + session.authenticateWithAuthChain(authChain); return true; } } diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java index c2f94b1c6..f7e3bd43d 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -25,9 +25,8 @@ package org.geysermc.geyser.session; -import com.github.steveice10.mc.auth.data.GameProfile; -import com.github.steveice10.mc.auth.exception.request.RequestException; -import com.github.steveice10.mc.auth.service.MsaAuthenticationService; +import com.google.gson.Gson; +import com.google.gson.JsonObject; import io.netty.channel.Channel; import io.netty.channel.EventLoop; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; @@ -41,22 +40,60 @@ import lombok.Getter; import lombok.Setter; import lombok.experimental.Accessors; import net.kyori.adventure.key.Key; +import net.raphimc.minecraftauth.step.java.StepMCProfile; +import net.raphimc.minecraftauth.step.java.StepMCToken; +import net.raphimc.minecraftauth.step.java.session.StepFullJavaSession; import org.checkerframework.checker.index.qual.NonNegative; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.value.qual.IntRange; -import org.cloudburstmc.math.vector.*; +import org.cloudburstmc.math.vector.Vector2f; +import org.cloudburstmc.math.vector.Vector2i; +import org.cloudburstmc.math.vector.Vector3d; +import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.protocol.bedrock.BedrockDisconnectReasons; import org.cloudburstmc.protocol.bedrock.BedrockServerSession; -import org.cloudburstmc.protocol.bedrock.data.*; +import org.cloudburstmc.protocol.bedrock.data.Ability; +import org.cloudburstmc.protocol.bedrock.data.AbilityLayer; +import org.cloudburstmc.protocol.bedrock.data.AuthoritativeMovementMode; +import org.cloudburstmc.protocol.bedrock.data.ChatRestrictionLevel; +import org.cloudburstmc.protocol.bedrock.data.ExperimentData; +import org.cloudburstmc.protocol.bedrock.data.GamePublishSetting; +import org.cloudburstmc.protocol.bedrock.data.GameRuleData; +import org.cloudburstmc.protocol.bedrock.data.GameType; +import org.cloudburstmc.protocol.bedrock.data.PlayerPermission; +import org.cloudburstmc.protocol.bedrock.data.SoundEvent; +import org.cloudburstmc.protocol.bedrock.data.SpawnBiomeType; import org.cloudburstmc.protocol.bedrock.data.command.CommandEnumData; import org.cloudburstmc.protocol.bedrock.data.command.CommandPermission; import org.cloudburstmc.protocol.bedrock.data.command.SoftEnumUpdateType; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; -import org.cloudburstmc.protocol.bedrock.packet.*; +import org.cloudburstmc.protocol.bedrock.packet.AvailableEntityIdentifiersPacket; +import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket; +import org.cloudburstmc.protocol.bedrock.packet.BiomeDefinitionListPacket; +import org.cloudburstmc.protocol.bedrock.packet.CameraPresetsPacket; +import org.cloudburstmc.protocol.bedrock.packet.ChunkRadiusUpdatedPacket; +import org.cloudburstmc.protocol.bedrock.packet.CraftingDataPacket; +import org.cloudburstmc.protocol.bedrock.packet.CreativeContentPacket; +import org.cloudburstmc.protocol.bedrock.packet.EmoteListPacket; +import org.cloudburstmc.protocol.bedrock.packet.GameRulesChangedPacket; +import org.cloudburstmc.protocol.bedrock.packet.ItemComponentPacket; +import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEvent2Packet; +import org.cloudburstmc.protocol.bedrock.packet.PlayStatusPacket; +import org.cloudburstmc.protocol.bedrock.packet.SetTimePacket; +import org.cloudburstmc.protocol.bedrock.packet.StartGamePacket; +import org.cloudburstmc.protocol.bedrock.packet.SyncEntityPropertyPacket; +import org.cloudburstmc.protocol.bedrock.packet.TextPacket; +import org.cloudburstmc.protocol.bedrock.packet.TransferPacket; +import org.cloudburstmc.protocol.bedrock.packet.UpdateAbilitiesPacket; +import org.cloudburstmc.protocol.bedrock.packet.UpdateAdventureSettingsPacket; +import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket; +import org.cloudburstmc.protocol.bedrock.packet.UpdateClientInputLocksPacket; +import org.cloudburstmc.protocol.bedrock.packet.UpdateSoftEnumPacket; import org.cloudburstmc.protocol.common.util.OptionalBoolean; import org.geysermc.api.util.BedrockPlatform; import org.geysermc.api.util.InputMode; @@ -106,7 +143,22 @@ import org.geysermc.geyser.registry.type.BlockMappings; import org.geysermc.geyser.registry.type.ItemMappings; import org.geysermc.geyser.session.auth.AuthData; import org.geysermc.geyser.session.auth.BedrockClientData; -import org.geysermc.geyser.session.cache.*; +import org.geysermc.geyser.session.cache.AdvancementsCache; +import org.geysermc.geyser.session.cache.BookEditCache; +import org.geysermc.geyser.session.cache.ChunkCache; +import org.geysermc.geyser.session.cache.EntityCache; +import org.geysermc.geyser.session.cache.EntityEffectCache; +import org.geysermc.geyser.session.cache.FormCache; +import org.geysermc.geyser.session.cache.LodestoneCache; +import org.geysermc.geyser.session.cache.PistonCache; +import org.geysermc.geyser.session.cache.PreferencesCache; +import org.geysermc.geyser.session.cache.RegistryCache; +import org.geysermc.geyser.session.cache.SkullCache; +import org.geysermc.geyser.session.cache.StructureBlockCache; +import org.geysermc.geyser.session.cache.TagCache; +import org.geysermc.geyser.session.cache.TeleportCache; +import org.geysermc.geyser.session.cache.WorldBorder; +import org.geysermc.geyser.session.cache.WorldCache; import org.geysermc.geyser.skin.FloodgateSkinUploader; import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.text.MinecraftLocale; @@ -116,9 +168,15 @@ import org.geysermc.geyser.util.ChunkUtils; import org.geysermc.geyser.util.DimensionUtils; import org.geysermc.geyser.util.EntityUtils; import org.geysermc.geyser.util.LoginEncryptionUtils; +import org.geysermc.geyser.util.MinecraftAuthLogger; +import org.geysermc.mcprotocollib.auth.GameProfile; import org.geysermc.mcprotocollib.network.BuiltinFlags; import org.geysermc.mcprotocollib.network.Session; -import org.geysermc.mcprotocollib.network.event.session.*; +import org.geysermc.mcprotocollib.network.event.session.ConnectedEvent; +import org.geysermc.mcprotocollib.network.event.session.DisconnectedEvent; +import org.geysermc.mcprotocollib.network.event.session.PacketErrorEvent; +import org.geysermc.mcprotocollib.network.event.session.PacketSendingEvent; +import org.geysermc.mcprotocollib.network.event.session.SessionAdapter; import org.geysermc.mcprotocollib.network.packet.Packet; import org.geysermc.mcprotocollib.network.tcp.TcpClientSession; import org.geysermc.mcprotocollib.network.tcp.TcpSession; @@ -153,7 +211,16 @@ import java.net.ConnectException; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; import java.time.Instant; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ScheduledFuture; @@ -163,6 +230,8 @@ import java.util.concurrent.atomic.AtomicInteger; @Getter public class GeyserSession implements GeyserConnection, GeyserCommandSource { + private static final Gson GSON = new Gson(); + private final GeyserImpl geyser; private final UpstreamSession upstream; private DownstreamSession downstream; @@ -690,7 +759,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { } } - public void authenticateWithRefreshToken(String refreshToken) { + public void authenticateWithAuthChain(String authChain) { if (loggedIn) { geyser.getLogger().severe(GeyserLocale.getLocaleStringLog("geyser.auth.already_loggedin", getAuthData().name())); return; @@ -699,24 +768,23 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { loggingIn = true; CompletableFuture.supplyAsync(() -> { - MsaAuthenticationService service = new MsaAuthenticationService(GeyserImpl.OAUTH_CLIENT_ID); - service.setRefreshToken(refreshToken); + StepFullJavaSession step = PendingMicrosoftAuthentication.AUTH_FLOW.apply(true, 30); + StepFullJavaSession.FullJavaSession response; try { - service.login(); - } catch (RequestException e) { - geyser.getLogger().error("Error while attempting to use refresh token for " + bedrockUsername() + "!", e); + response = step.refresh(MinecraftAuthLogger.INSTANCE, PendingMicrosoftAuthentication.AUTH_CLIENT, step.fromJson(GSON.fromJson(authChain, JsonObject.class))); + } catch (Exception e) { + geyser.getLogger().error("Error while attempting to use auth chain for " + bedrockUsername() + "!", e); return Boolean.FALSE; } - GameProfile profile = service.getSelectedProfile(); - if (profile == null) { - // Java account is offline - disconnect(GeyserLocale.getPlayerLocaleString("geyser.network.remote.invalid_account", clientData.getLanguageCode())); - return null; - } + StepMCProfile.MCProfile mcProfile = response.getMcProfile(); + StepMCToken.MCToken mcToken = mcProfile.getMcToken(); - protocol = new MinecraftProtocol(profile, service.getAccessToken()); - geyser.saveRefreshToken(bedrockUsername(), service.getRefreshToken()); + protocol = new MinecraftProtocol( + new GameProfile(mcProfile.getId(), mcProfile.getName()), + mcToken.getAccessToken() + ); + geyser.saveAuthChain(bedrockUsername(), GSON.toJson(step.toJson(response))); return Boolean.TRUE; }).whenComplete((successful, ex) -> { if (this.closed) { @@ -761,25 +829,15 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { final PendingMicrosoftAuthentication.AuthenticationTask task = geyser.getPendingMicrosoftAuthentication().getOrCreateTask( getAuthData().xuid() ); - task.setOnline(true); - task.resetTimer(); - - if (task.getAuthentication().isDone()) { + if (task.getAuthentication() != null && task.getAuthentication().isDone()) { onMicrosoftLoginComplete(task); } else { - task.getCode(offlineAccess).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)); + task.resetRunningFlow(); + task.performLoginAttempt(offlineAccess, code -> { + if (!closed) { + LoginEncryptionUtils.buildAndShowMicrosoftCodeWindow(this, code); } - }); + }).handle((r, e) -> onMicrosoftLoginComplete(task)); } } @@ -791,36 +849,32 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { return false; } 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 { - MsaAuthenticationService service = task.getMsaAuthenticationService(); - GameProfile selectedProfile = service.getSelectedProfile(); - if (selectedProfile == null) { - disconnect(GeyserLocale.getPlayerLocaleString( - "geyser.network.remote.invalid_account", - clientData.getLanguageCode() - )); - } else { - this.protocol = new MinecraftProtocol( - selectedProfile, - service.getAccessToken() - ); - try { - connectDownstream(); - } catch (Throwable t) { - t.printStackTrace(); - return false; - } + return task.getAuthentication().handle((result, ex) -> { + if (ex != null) { + geyser.getLogger().error("Failed to log in with Microsoft code!", ex); + disconnect(ex.toString()); + return false; + } - // Save our refresh token for later use - geyser.saveRefreshToken(bedrockUsername(), service.getRefreshToken()); - return true; - } - } - return false; + StepMCProfile.MCProfile mcProfile = result.session().getMcProfile(); + StepMCToken.MCToken mcToken = mcProfile.getMcToken(); + + this.protocol = new MinecraftProtocol( + new GameProfile(mcProfile.getId(), mcProfile.getName()), + mcToken.getAccessToken() + ); + + try { + connectDownstream(); + } catch (Throwable t) { + t.printStackTrace(); + return false; + } + + // Save our auth chain for later use + geyser.saveAuthChain(bedrockUsername(), GSON.toJson(result.step().toJson(result.session()))); + return true; + }).getNow(false); } /** @@ -1103,7 +1157,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { if (authData != null) { PendingMicrosoftAuthentication.AuthenticationTask task = geyser.getPendingMicrosoftAuthentication().getTask(authData.xuid()); if (task != null) { - task.setOnline(false); + task.resetRunningFlow(); } } } diff --git a/core/src/main/java/org/geysermc/geyser/session/PendingMicrosoftAuthentication.java b/core/src/main/java/org/geysermc/geyser/session/PendingMicrosoftAuthentication.java index 0651039a0..7d0e2fbf9 100644 --- a/core/src/main/java/org/geysermc/geyser/session/PendingMicrosoftAuthentication.java +++ b/core/src/main/java/org/geysermc/geyser/session/PendingMicrosoftAuthentication.java @@ -25,27 +25,44 @@ 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.Setter; import lombok.SneakyThrows; +import net.lenni0451.commons.httpclient.HttpClient; +import net.raphimc.minecraftauth.MinecraftAuth; +import net.raphimc.minecraftauth.step.java.session.StepFullJavaSession; +import net.raphimc.minecraftauth.step.msa.StepMsaDeviceCode; +import net.raphimc.minecraftauth.util.MicrosoftConstants; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserLogger; +import org.geysermc.geyser.util.MinecraftAuthLogger; -import java.io.Serial; -import java.util.concurrent.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.function.BiFunction; +import java.util.function.Consumer; /** * Pending Microsoft authentication task cache. * It permits user to exit the server while they authorize Geyser to access their Microsoft account. */ public class PendingMicrosoftAuthentication { + public static final HttpClient AUTH_CLIENT = MinecraftAuth.createHttpClient(); + public static final BiFunction AUTH_FLOW = (offlineAccess, timeoutSec) -> MinecraftAuth.builder() + .withClientId(GeyserImpl.OAUTH_CLIENT_ID) + .withScope(offlineAccess ? "XboxLive.signin XboxLive.offline_access" : "XboxLive.signin") + .withTimeout(timeoutSec) + .deviceCode() + .withoutDeviceToken() + .regularAuthentication(MicrosoftConstants.JAVA_XSTS_RELYING_PARTY) + .buildMinecraftJavaProfileStep(false); /** * For GeyserConnect usage. */ @@ -57,8 +74,8 @@ public class PendingMicrosoftAuthentication { .build(new CacheLoader<>() { @Override public AuthenticationTask load(@NonNull String userKey) { - return storeServerInformation ? new ProxyAuthenticationTask(userKey, timeoutSeconds * 1000L) - : new AuthenticationTask(userKey, timeoutSeconds * 1000L); + return storeServerInformation ? new ProxyAuthenticationTask(userKey, timeoutSeconds) + : new AuthenticationTask(userKey, timeoutSeconds); } }); } @@ -80,37 +97,23 @@ public class PendingMicrosoftAuthentication { 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; - + private final int timeoutSec; @Getter - private final CompletableFuture authentication; + private CompletableFuture authentication; - @Getter - private volatile Throwable loginException; - - private AuthenticationTask(String userKey, long timeoutMs) { + private AuthenticationTask(String userKey, int timeoutSec) { this.userKey = userKey; - this.timeoutMs = timeoutMs; - this.remainingTimeMs = timeoutMs; - - this.authentication = new CompletableFuture<>(); - 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); - }); + this.timeoutSec = timeoutSec; } - public void resetTimer() { - this.remainingTimeMs = this.timeoutMs; + public void resetRunningFlow() { + if (authentication == null) { + return; + } + + // Interrupt the current flow + this.authentication.cancel(true); } public void cleanup() { @@ -121,52 +124,18 @@ public class PendingMicrosoftAuthentication { authentications.invalidate(userKey); } - public CompletableFuture getCode(boolean offlineAccess) { - // Request the code - CompletableFuture code = CompletableFuture.supplyAsync( - () -> tryGetCode(offlineAccess)); - // Once the code is received, continuously try to request the access token, profile, etc - code.thenRun(() -> performLoginAttempt(System.currentTimeMillis())); - return code; - } - - /** - * @param offlineAccess whether we want a refresh token for later use. - */ - private MsaAuthenticationService.MsCodeResponse tryGetCode(boolean offlineAccess) throws CompletionException { - try { - return msaAuthenticationService.getAuthCode(offlineAccess); - } catch (RequestException e) { - throw new CompletionException(e); - } - } - - private void performLoginAttempt(long lastAttempt) { - CompletableFuture.runAsync(() -> { + public CompletableFuture performLoginAttempt(boolean offlineAccess, Consumer deviceCodeConsumer) { + return authentication = CompletableFuture.supplyAsync(() -> { 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; + StepFullJavaSession step = AUTH_FLOW.apply(offlineAccess, timeoutSec); + return new StepChainResult(step, step.getFromInput(MinecraftAuthLogger.INSTANCE, AUTH_CLIENT, new StepMsaDeviceCode.MsaDeviceCodeCallback(deviceCodeConsumer))); } catch (Exception e) { - authentication.completeExceptionally(e); - return; + throw new CompletionException(e); } - // login successful - authentication.complete(msaAuthenticationService); - }, DELAYED_BY_ONE_SECOND); + }, DELAYED_BY_ONE_SECOND).whenComplete((r, ex) -> { + // avoid memory leak, in case player doesn't connect again + CompletableFuture.delayedExecutor(timeoutSec, TimeUnit.SECONDS).execute(this::cleanup); + }); } @Override @@ -181,22 +150,11 @@ public class PendingMicrosoftAuthentication { private String server; private int port; - private ProxyAuthenticationTask(String userKey, long timeoutMs) { - super(userKey, timeoutMs); + private ProxyAuthenticationTask(String userKey, int timeoutSec) { + super(userKey, timeoutSec); } } - /** - * @see PendingMicrosoftAuthentication - */ - public static class TaskTimeoutException extends Exception { - - @Serial - private static final long serialVersionUID = 1L; - - TaskTimeoutException() { - super("It took too long to authorize Geyser to access your Microsoft account. " + - "Please request new code and try again."); - } + public record StepChainResult(StepFullJavaSession step, StepFullJavaSession.FullJavaSession session) { } } diff --git a/core/src/main/java/org/geysermc/geyser/skin/FakeHeadProvider.java b/core/src/main/java/org/geysermc/geyser/skin/FakeHeadProvider.java index ef3ff3293..6f6bcb0ae 100644 --- a/core/src/main/java/org/geysermc/geyser/skin/FakeHeadProvider.java +++ b/core/src/main/java/org/geysermc/geyser/skin/FakeHeadProvider.java @@ -25,11 +25,10 @@ package org.geysermc.geyser.skin; -import com.github.steveice10.mc.auth.data.GameProfile; -import com.github.steveice10.mc.auth.data.GameProfile.Texture; -import com.github.steveice10.mc.auth.data.GameProfile.TextureModel; -import com.github.steveice10.mc.auth.data.GameProfile.TextureType; -import com.github.steveice10.mc.auth.exception.property.PropertyException; +import org.geysermc.mcprotocollib.auth.GameProfile; +import org.geysermc.mcprotocollib.auth.GameProfile.Texture; +import org.geysermc.mcprotocollib.auth.GameProfile.TextureModel; +import org.geysermc.mcprotocollib.auth.GameProfile.TextureType; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; @@ -113,12 +112,7 @@ public class FakeHeadProvider { return; } - Map textures = null; - try { - textures = profile.getTextures(false); - } catch (PropertyException e) { - session.getGeyser().getLogger().debug("Failed to get textures from GameProfile: " + e); - } + Map textures = profile.getTextures(false); if (textures == null || textures.isEmpty()) { loadHead(session, entity, profile.getName()); diff --git a/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java index e9527872a..aa0c3eb43 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java @@ -25,10 +25,9 @@ package org.geysermc.geyser.translator.item; -import com.github.steveice10.mc.auth.data.GameProfile; -import com.github.steveice10.mc.auth.data.GameProfile.Texture; -import com.github.steveice10.mc.auth.data.GameProfile.TextureType; -import com.github.steveice10.mc.auth.exception.property.PropertyException; +import org.geysermc.mcprotocollib.auth.GameProfile; +import org.geysermc.mcprotocollib.auth.GameProfile.Texture; +import org.geysermc.mcprotocollib.auth.GameProfile.TextureType; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.checkerframework.checker.nullness.qual.NonNull; @@ -487,12 +486,7 @@ public final class ItemTranslator { GameProfile profile = components.get(DataComponentType.PROFILE); if (profile != null) { - Map textures = null; - try { - textures = profile.getTextures(false); - } catch (PropertyException e) { - GeyserImpl.getInstance().getLogger().debug("Failed to get textures from GameProfile: " + e); - } + Map textures = profile.getTextures(false); if (textures == null || textures.isEmpty()) { return null; diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockSetLocalPlayerAsInitializedTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockSetLocalPlayerAsInitializedTranslator.java index de2df0cb7..47c5bfd35 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockSetLocalPlayerAsInitializedTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockSetLocalPlayerAsInitializedTranslator.java @@ -46,10 +46,10 @@ public class BedrockSetLocalPlayerAsInitializedTranslator extends PacketTranslat if (session.remoteServer().authType() == AuthType.ONLINE) { if (!session.isLoggedIn()) { if (session.getGeyser().getConfig().getSavedUserLogins().contains(session.bedrockUsername())) { - if (session.getGeyser().refreshTokenFor(session.bedrockUsername()) == null) { + if (session.getGeyser().authChainFor(session.bedrockUsername()) == null) { LoginEncryptionUtils.buildAndShowConsentWindow(session); } else { - // If the refresh token is not null and we're here, then the refresh token expired + // If the auth chain is not null and we're here, then it expired // and the expiration form has been cached session.getFormCache().resendAllForms(); } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaGameProfileTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaGameProfileTranslator.java index e7bde6a9d..47d1cff08 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaGameProfileTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaGameProfileTranslator.java @@ -25,7 +25,7 @@ package org.geysermc.geyser.translator.protocol.java; -import com.github.steveice10.mc.auth.data.GameProfile; +import org.geysermc.mcprotocollib.auth.GameProfile; import net.kyori.adventure.key.Key; import org.geysermc.geyser.api.network.AuthType; import org.geysermc.geyser.entity.type.player.PlayerEntity; diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerInfoUpdateTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerInfoUpdateTranslator.java index f5ea4c08d..19f34db74 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerInfoUpdateTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerInfoUpdateTranslator.java @@ -25,7 +25,7 @@ package org.geysermc.geyser.translator.protocol.java.entity.player; -import com.github.steveice10.mc.auth.data.GameProfile; +import org.geysermc.mcprotocollib.auth.GameProfile; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.packet.PlayerListPacket; diff --git a/core/src/main/java/org/geysermc/geyser/util/LoginEncryptionUtils.java b/core/src/main/java/org/geysermc/geyser/util/LoginEncryptionUtils.java index 478a6ef96..d3024be65 100644 --- a/core/src/main/java/org/geysermc/geyser/util/LoginEncryptionUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/LoginEncryptionUtils.java @@ -28,7 +28,7 @@ package org.geysermc.geyser.util; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.github.steveice10.mc.auth.service.MsaAuthenticationService; +import net.raphimc.minecraftauth.step.msa.StepMsaDeviceCode; import org.cloudburstmc.protocol.bedrock.packet.LoginPacket; import org.cloudburstmc.protocol.bedrock.packet.ServerToClientHandshakePacket; import org.cloudburstmc.protocol.bedrock.util.ChainValidationResult; @@ -203,7 +203,7 @@ public class LoginEncryptionUtils { /** * 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, StepMsaDeviceCode.MsaDeviceCode msCode) { String locale = session.locale(); StringBuilder message = new StringBuilder("%xbox.signin.website\n") @@ -212,7 +212,7 @@ public class LoginEncryptionUtils { .append(ChatColor.RESET) .append("\n%xbox.signin.enterCode\n") .append(ChatColor.GREEN) - .append(msCode.user_code); + .append(msCode.getUserCode()); int timeout = session.getGeyser().getConfig().getPendingAuthenticationTimeout(); if (timeout != 0) { message.append("\n\n") diff --git a/core/src/main/java/org/geysermc/geyser/util/MinecraftAuthLogger.java b/core/src/main/java/org/geysermc/geyser/util/MinecraftAuthLogger.java new file mode 100644 index 000000000..4e928d47e --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/util/MinecraftAuthLogger.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024 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.util; + +import net.raphimc.minecraftauth.util.logging.ILogger; +import org.geysermc.geyser.GeyserImpl; + +public class MinecraftAuthLogger implements ILogger { + + public static final MinecraftAuthLogger INSTANCE = new MinecraftAuthLogger(); + + @Override + public void info(String message) { + GeyserImpl.getInstance().getLogger().debug(message); + } + + @Override + public void warn(String message) { + GeyserImpl.getInstance().getLogger().warning(message); + } + + @Override + public void error(String message) { + GeyserImpl.getInstance().getLogger().error(message); + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 49c02d190..845589585 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,8 +12,8 @@ gson = "2.3.1" # Provided by Spigot 1.8.8 websocket = "1.5.1" protocol = "3.0.0.Beta2-20240704.153116-14" raknet = "1.0.0.CR3-20240416.144209-1" -mcauthlib = "e5b0bcc" -mcprotocollib = "1.21-20240616.154144-5" +minecraftauth = "4.1.0" +mcprotocollib = "1.21-20240718.102029-13" adventure = "4.14.0" adventure-platform = "4.3.0" junit = "5.9.2" @@ -107,7 +107,7 @@ commodore = { group = "me.lucko", name = "commodore", version.ref = "commodore" guava = { group = "com.google.guava", name = "guava", version.ref = "guava" } gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } junit = { group = "org.junit.jupiter", name = "junit-jupiter", version.ref = "junit" } -mcauthlib = { group = "com.github.GeyserMC", name = "MCAuthLib", version.ref = "mcauthlib" } +minecraftauth = { group = "net.raphimc", name = "MinecraftAuth", version.ref = "minecraftauth" } mcprotocollib = { group = "org.geysermc.mcprotocollib", name = "protocol", version.ref = "mcprotocollib" } raknet = { group = "org.cloudburstmc.netty", name = "netty-transport-raknet", version.ref = "raknet" } terminalconsoleappender = { group = "net.minecrell", name = "terminalconsoleappender", version.ref = "terminalconsoleappender" }