From de5cc874920354d1b1a74705244a97222900f140 Mon Sep 17 00:00:00 2001 From: "Gabriele C." Date: Thu, 25 Jun 2026 20:53:24 +0200 Subject: [PATCH] suppress pre-join login dialog via config-phase auto-login --- .../service/bungeecord/BungeeReceiver.java | 124 ++++++++++++++---- .../bungeecord/BungeeReceiverTest.java | 60 +++++++++ .../authme/platform/FoliaPlatformAdapter.java | 2 + .../platform/FoliaPlatformAdapterTest.java | 2 + .../listener/PaperDialogFlowListener.java | 16 ++- .../listener/PaperProxyAutoLoginListener.java | 91 +++++++++++++ .../authme/platform/PaperPlatformAdapter.java | 10 +- .../platform/PaperPlatformAdapterTest.java | 2 + .../authme/velocity/AuthMeVelocityPlugin.java | 6 + .../authme/velocity/VelocityProxyBridge.java | 40 ++++++ .../velocity/VelocityProxyBridgeTest.java | 50 +++++++ 11 files changed, 366 insertions(+), 37 deletions(-) create mode 100644 authme-paper-common/src/main/java/fr/xephi/authme/listener/PaperProxyAutoLoginListener.java diff --git a/authme-core/src/main/java/fr/xephi/authme/service/bungeecord/BungeeReceiver.java b/authme-core/src/main/java/fr/xephi/authme/service/bungeecord/BungeeReceiver.java index c89ea6a34..654023868 100644 --- a/authme-core/src/main/java/fr/xephi/authme/service/bungeecord/BungeeReceiver.java +++ b/authme-core/src/main/java/fr/xephi/authme/service/bungeecord/BungeeReceiver.java @@ -21,6 +21,7 @@ import javax.inject.Inject; import java.util.List; +import java.util.Locale; import java.util.Optional; import java.util.UUID; @@ -42,6 +43,7 @@ public class BungeeReceiver implements PluginMessageListener, SettingsDependent private boolean isEnabled; private String proxySharedSecret; + private boolean channelRegistered; @Inject BungeeReceiver(AuthMe plugin, BukkitService bukkitService, ProxySessionManager proxySessionManager, @@ -62,12 +64,17 @@ public void reload(Settings settings) { this.proxySharedSecret = settings.getProperty(HooksSettings.PROXY_SHARED_SECRET); this.isEnabled = settings.getProperty(HooksSettings.BUNGEECORD); final Messenger messenger = plugin.getServer().getMessenger(); - if (this.isEnabled && messenger != null) { - if (!messenger.isIncomingChannelRegistered(plugin, AUTHME_CHANNEL)) { - messenger.registerIncomingPluginChannel(plugin, AUTHME_CHANNEL, this); - } - } else if (messenger != null && messenger.isIncomingChannelRegistered(plugin, AUTHME_CHANNEL)) { + if (messenger == null) { + return; + } + // Track our own registration rather than querying the channel: other listeners + // (e.g. the Paper configuration-phase receiver) may register the same channel. + if (this.isEnabled && !channelRegistered) { + messenger.registerIncomingPluginChannel(plugin, AUTHME_CHANNEL, this); + channelRegistered = true; + } else if (!this.isEnabled && channelRegistered) { messenger.unregisterIncomingPluginChannel(plugin, AUTHME_CHANNEL, this); + channelRegistered = false; } } @@ -126,33 +133,83 @@ public void onPluginMessageReceived(String channel, Player player, byte[] data) } if (type.get() == MessageType.PERFORM_LOGIN) { - long timestamp; - String uuidOrHmac; - UUID verifiedPremiumUuid = null; - String hmac; - try { - timestamp = in.readLong(); - uuidOrHmac = in.readUTF(); - } catch (IllegalStateException e) { - logger.warning("Received perform.login without HMAC — update your proxy plugin"); + VerifiedProxyLogin verified = parseAndVerifyPerformLogin(in, argument); + if (verified == null) { return; } - try { - UUID parsedUuid = UuidUtils.parseUuidSafely(uuidOrHmac); - if (parsedUuid != null || uuidOrHmac.isEmpty()) { - verifiedPremiumUuid = parsedUuid; - hmac = in.readUTF(); - } else { - hmac = uuidOrHmac; - } - } catch (IllegalStateException e) { + performLogin(verified.name, verified.verifiedPremiumUuid); + } + } + + /** + * Parses and HMAC-verifies the remainder of a {@code perform.login} message (everything after the + * player-name argument). + * + * @param in the data input positioned right after the player-name argument + * @param playerName the player-name argument that was already read + * @return the verified login data, or {@code null} if the message was malformed or failed verification + */ + private VerifiedProxyLogin parseAndVerifyPerformLogin(ByteArrayDataInput in, String playerName) { + long timestamp; + String uuidOrHmac; + UUID verifiedPremiumUuid = null; + String hmac; + try { + timestamp = in.readLong(); + uuidOrHmac = in.readUTF(); + } catch (IllegalStateException e) { + logger.warning("Received perform.login without HMAC — update your proxy plugin"); + return null; + } + try { + UUID parsedUuid = UuidUtils.parseUuidSafely(uuidOrHmac); + if (parsedUuid != null || uuidOrHmac.isEmpty()) { + verifiedPremiumUuid = parsedUuid; + hmac = in.readUTF(); + } else { hmac = uuidOrHmac; } - if (!verifyHmac(argument, timestamp, verifiedPremiumUuid, hmac)) { - return; + } catch (IllegalStateException e) { + hmac = uuidOrHmac; + } + if (!verifyHmac(playerName, timestamp, verifiedPremiumUuid, hmac)) { + return null; + } + return new VerifiedProxyLogin(playerName, verifiedPremiumUuid); + } + + /** + * Validates and queues a {@code perform.login} received during Paper/Folia's connection + * configuration phase, when the player does not yet exist as a {@link Player}. Queuing it in the + * {@link ProxySessionManager} lets the blocking pre-join login dialog be skipped (and + * {@code processJoin} auto-login the player) instead of waiting for the post-join + * {@code perform.login}, which arrives only after the configuration phase has completed. + * + * @param data the raw plugin-message payload + * @return the normalized player name if this was a valid {@code perform.login}, otherwise {@code null} + */ + public String handleConfigPhasePerformLogin(byte[] data) { + if (!isEnabled) { + return null; + } + ByteArrayDataInput in = ByteStreams.newDataInput(data); + String argument; + try { + String typeId = in.readUTF(); + if (!MessageType.PERFORM_LOGIN.getId().equals(typeId)) { + return null; } - performLogin(argument, verifiedPremiumUuid); + argument = in.readUTF(); + } catch (IllegalStateException e) { + return null; + } + VerifiedProxyLogin verified = parseAndVerifyPerformLogin(in, argument); + if (verified == null) { + return null; } + proxySessionManager.processProxySessionMessage(verified.name, verified.verifiedPremiumUuid); + logger.debug("Config-phase perform.login validated and queued for {0}", verified.name); + return verified.name.toLowerCase(Locale.ROOT); } private boolean verifyHmac(String playerName, long timestamp, UUID verifiedPremiumUuid, String providedHmac) { @@ -219,4 +276,19 @@ private void completeProxyLogin(Player player) { logger.info(player.getName() + " has been automatically logged in via proxy request."); } + /** + * Holder for a validated {@code perform.login}: the player name and the proxy-verified premium UUID + * (or {@code null} if the player is not a verified premium player). + */ + private static final class VerifiedProxyLogin { + + private final String name; + private final UUID verifiedPremiumUuid; + + private VerifiedProxyLogin(String name, UUID verifiedPremiumUuid) { + this.name = name; + this.verifiedPremiumUuid = verifiedPremiumUuid; + } + } + } diff --git a/authme-core/src/test/java/fr/xephi/authme/service/bungeecord/BungeeReceiverTest.java b/authme-core/src/test/java/fr/xephi/authme/service/bungeecord/BungeeReceiverTest.java index 3b1f0467b..aee27609b 100644 --- a/authme-core/src/test/java/fr/xephi/authme/service/bungeecord/BungeeReceiverTest.java +++ b/authme-core/src/test/java/fr/xephi/authme/service/bungeecord/BungeeReceiverTest.java @@ -3,6 +3,7 @@ import com.google.common.io.ByteArrayDataOutput; import com.google.common.io.ByteStreams; import fr.xephi.authme.AuthMe; +import fr.xephi.authme.TestHelper; import fr.xephi.authme.data.ProxySessionManager; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.process.Management; @@ -14,6 +15,7 @@ import org.bukkit.Server; import org.bukkit.entity.Player; import org.bukkit.plugin.messaging.Messenger; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -26,6 +28,9 @@ import static fr.xephi.authme.service.BukkitServiceTestHelper.setBukkitServiceToRunTaskAsynchronously; import static fr.xephi.authme.service.BukkitServiceTestHelper.setBukkitServiceToScheduleSyncTaskFromOptionallyAsyncTask; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; @@ -67,6 +72,11 @@ class BungeeReceiverTest { @Mock private Messenger messenger; + @BeforeAll + static void initLogger() { + TestHelper.setupLogger(); + } + @BeforeEach void setUp() { given(plugin.getServer()).willReturn(server); @@ -228,6 +238,56 @@ void shouldRemoveQueuedRequestWhenPremiumValidateRejects() { verify(bungeeSender, never()).sendAuthMeBungeecordMessage(any(), any()); } + @Test + void shouldValidateAndQueueConfigPhasePerformLogin() { + // given + String sharedSecret = "test-secret"; + String playerName = "Bobby"; + long timestamp = System.currentTimeMillis(); + String hmac = HashUtils.hmacSha256(sharedSecret, playerName + ":" + timestamp + ":"); + + given(settings.getProperty(HooksSettings.BUNGEECORD)).willReturn(true); + given(settings.getProperty(HooksSettings.PROXY_SHARED_SECRET)).willReturn(sharedSecret); + + BungeeReceiver receiver = + new BungeeReceiver(plugin, bukkitService, proxySessionManager, management, bungeeSender, dataSource, + proxyLoginRequestValidator, settings); + + byte[] payload = buildPerformLoginPayload(playerName, timestamp, hmac); + + // when + String result = receiver.handleConfigPhasePerformLogin(payload); + + // then + assertThat(result, equalTo("bobby")); + verify(proxySessionManager).processProxySessionMessage(playerName, null); + verify(management, never()).forceLoginFromProxy(any()); + } + + @Test + void shouldRejectConfigPhasePerformLoginWithInvalidHmac() { + // given + String sharedSecret = "test-secret"; + String playerName = "Bobby"; + long timestamp = System.currentTimeMillis(); + + given(settings.getProperty(HooksSettings.BUNGEECORD)).willReturn(true); + given(settings.getProperty(HooksSettings.PROXY_SHARED_SECRET)).willReturn(sharedSecret); + + BungeeReceiver receiver = + new BungeeReceiver(plugin, bukkitService, proxySessionManager, management, bungeeSender, dataSource, + proxyLoginRequestValidator, settings); + + byte[] payload = buildPerformLoginPayload(playerName, timestamp, "not-a-valid-hmac"); + + // when + String result = receiver.handleConfigPhasePerformLogin(payload); + + // then + assertThat(result, nullValue()); + verify(proxySessionManager, never()).processProxySessionMessage(any(), any()); + } + private static byte[] buildPerformLoginPayload(String playerName, long timestamp, String hmac) { return buildPerformLoginPayload(playerName, timestamp, "", hmac); } diff --git a/authme-folia/src/main/java/fr/xephi/authme/platform/FoliaPlatformAdapter.java b/authme-folia/src/main/java/fr/xephi/authme/platform/FoliaPlatformAdapter.java index 7e10632dd..5c5685481 100644 --- a/authme-folia/src/main/java/fr/xephi/authme/platform/FoliaPlatformAdapter.java +++ b/authme-folia/src/main/java/fr/xephi/authme/platform/FoliaPlatformAdapter.java @@ -5,6 +5,7 @@ import fr.xephi.authme.listener.PaperLoginValidationListener; import fr.xephi.authme.listener.FoliaPlayerSpawnLocationListener; import fr.xephi.authme.listener.PaperDialogFlowListener; +import fr.xephi.authme.listener.PaperProxyAutoLoginListener; import fr.xephi.authme.listener.PlayerOpenSignListener; import fr.xephi.authme.service.CancellableTask; import io.papermc.paper.threadedregions.scheduler.ScheduledTask; @@ -108,6 +109,7 @@ public List> getListeners() { Arrays.asList( FoliaChatListener.class, PaperDialogFlowListener.class, + PaperProxyAutoLoginListener.class, FoliaPlayerSpawnLocationListener.class, PaperLoginValidationListener.class, PlayerOpenSignListener.class)); diff --git a/authme-folia/src/test/java/fr/xephi/authme/platform/FoliaPlatformAdapterTest.java b/authme-folia/src/test/java/fr/xephi/authme/platform/FoliaPlatformAdapterTest.java index ad27bbe26..dbf74cbc8 100644 --- a/authme-folia/src/test/java/fr/xephi/authme/platform/FoliaPlatformAdapterTest.java +++ b/authme-folia/src/test/java/fr/xephi/authme/platform/FoliaPlatformAdapterTest.java @@ -5,6 +5,7 @@ import fr.xephi.authme.listener.FoliaChatListener; import fr.xephi.authme.listener.FoliaPlayerSpawnLocationListener; import fr.xephi.authme.listener.PaperDialogFlowListener; +import fr.xephi.authme.listener.PaperProxyAutoLoginListener; import fr.xephi.authme.listener.PaperLoginValidationListener; import fr.xephi.authme.listener.PlayerListener; import fr.xephi.authme.listener.PlayerOpenSignListener; @@ -76,6 +77,7 @@ public void getListenersContainsCoreAndFoliaListeners() { ServerListener.class, FoliaChatListener.class, PaperDialogFlowListener.class, + PaperProxyAutoLoginListener.class, FoliaPlayerSpawnLocationListener.class, PaperLoginValidationListener.class, PlayerOpenSignListener.class)); diff --git a/authme-paper-common/src/main/java/fr/xephi/authme/listener/PaperDialogFlowListener.java b/authme-paper-common/src/main/java/fr/xephi/authme/listener/PaperDialogFlowListener.java index 480cd7e6d..0be188a0c 100644 --- a/authme-paper-common/src/main/java/fr/xephi/authme/listener/PaperDialogFlowListener.java +++ b/authme-paper-common/src/main/java/fr/xephi/authme/listener/PaperDialogFlowListener.java @@ -199,11 +199,21 @@ private void handleBlockingLoginDialog(PlayerConfigurationConnection connection, long timeoutSeconds = Math.max(commonService.getProperty(RestrictionSettings.LOGIN_TIMEOUT), 1); loginResponse.completeOnTimeout( messages.retrieveSingle(playerName, MessageKey.LOGIN_TIMEOUT_ERROR), timeoutSeconds, TimeUnit.SECONDS); + String normalizedName = playerName.toLowerCase(Locale.ROOT); pendingLoginResponses.put(playerId, loginResponse); - preJoinDialogService.registerPreJoinFuture(playerName.toLowerCase(java.util.Locale.ROOT), playerId, loginResponse); + preJoinDialogService.registerPreJoinFuture(normalizedName, playerId, loginResponse); + + // Close the race with a proxy auto-login (perform.login) that arrived during the configuration + // phase between the shouldSkipDialogs() check and now: if a proxy session has been queued, + // force-login instead of showing the dialog. + if (proxySessionManager.shouldResumeSession(normalizedName)) { + preJoinDialogService.approvePreJoinForceLogin(normalizedName); + } - connection.getAudience().showDialog( - PaperDialogHelper.createPreJoinLoginDialog(dialogWindowService.createPreJoinLoginDialog(playerName))); + if (!loginResponse.isDone()) { + connection.getAudience().showDialog( + PaperDialogHelper.createPreJoinLoginDialog(dialogWindowService.createPreJoinLoginDialog(playerName))); + } String kickMessage = loginResponse.join(); pendingLoginResponses.remove(playerId); preJoinDialogService.unregisterPreJoinFuture(playerId); diff --git a/authme-paper-common/src/main/java/fr/xephi/authme/listener/PaperProxyAutoLoginListener.java b/authme-paper-common/src/main/java/fr/xephi/authme/listener/PaperProxyAutoLoginListener.java new file mode 100644 index 000000000..d807a9a86 --- /dev/null +++ b/authme-paper-common/src/main/java/fr/xephi/authme/listener/PaperProxyAutoLoginListener.java @@ -0,0 +1,91 @@ +package fr.xephi.authme.listener; + +import fr.xephi.authme.AuthMe; +import fr.xephi.authme.initialization.SettingsDependent; +import fr.xephi.authme.service.PreJoinDialogService; +import fr.xephi.authme.service.bungeecord.BungeeReceiver; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.HooksSettings; +import io.papermc.paper.connection.PlayerConfigurationConnection; +import io.papermc.paper.connection.PlayerConnection; +import org.bukkit.entity.Player; +import org.bukkit.event.Listener; +import org.bukkit.plugin.messaging.Messenger; +import org.bukkit.plugin.messaging.PluginMessageListener; + +import javax.inject.Inject; + +/** + * Receives proxy {@code perform.login} messages that arrive during Paper/Folia's connection + * configuration phase — before the connecting player exists as a {@link Player} — and uses them to + * dismiss the blocking pre-join login dialog. This lets an already-authenticated player who switches + * back to an auth server be auto-logged in instead of being prompted to log in again. + * + *

The standard {@link BungeeReceiver} only handles play-phase messages (its listener callback + * requires a {@link Player}). This listener implements the configuration-phase overload of + * {@code PluginMessageListener.onPluginMessageReceived(String, PlayerConnection, byte[])}, which is + * the only point at which the proxy signal can reach the backend before the dialog decision is made + * (the post-join {@code perform.login} arrives only after the configuration phase completes). + */ +public class PaperProxyAutoLoginListener implements Listener, PluginMessageListener, SettingsDependent { + + private static final String AUTHME_CHANNEL = "authme:main"; + + private final AuthMe plugin; + private final BungeeReceiver bungeeReceiver; + private final PreJoinDialogService preJoinDialogService; + + private boolean enabled; + private boolean channelRegistered; + + @Inject + PaperProxyAutoLoginListener(AuthMe plugin, BungeeReceiver bungeeReceiver, + PreJoinDialogService preJoinDialogService, Settings settings) { + this.plugin = plugin; + this.bungeeReceiver = bungeeReceiver; + this.preJoinDialogService = preJoinDialogService; + reload(settings); + } + + @Override + public void reload(Settings settings) { + this.enabled = settings.getProperty(HooksSettings.BUNGEECORD); + Messenger messenger = plugin.getServer().getMessenger(); + if (messenger == null) { + return; + } + if (enabled && !channelRegistered) { + messenger.registerIncomingPluginChannel(plugin, AUTHME_CHANNEL, this); + channelRegistered = true; + } else if (!enabled && channelRegistered) { + messenger.unregisterIncomingPluginChannel(plugin, AUTHME_CHANNEL, this); + channelRegistered = false; + } + } + + /** + * Play-phase messages are handled by {@link BungeeReceiver}; nothing to do here. + */ + @Override + public void onPluginMessageReceived(String channel, Player player, byte[] message) { + // no-op + } + + /** + * Handles AuthMe messages that arrive while the player is still in the configuration phase. + */ + @Override + public void onPluginMessageReceived(String channel, PlayerConnection connection, byte[] message) { + if (!enabled || !AUTHME_CHANNEL.equals(channel) + || !(connection instanceof PlayerConfigurationConnection)) { + return; + } + String name = bungeeReceiver.handleConfigPhasePerformLogin(message); + if (name != null) { + // If the player is already blocked in the pre-join dialog, dismiss it (force-login on join). + // If the dialog has not been shown yet, the queued proxy session makes shouldSkipDialogs() + // skip it; PaperDialogFlowListener also re-checks that queue right after registering. + preJoinDialogService.approvePreJoinForceLogin(name); + } + } +} diff --git a/authme-paper/src/main/java/fr/xephi/authme/platform/PaperPlatformAdapter.java b/authme-paper/src/main/java/fr/xephi/authme/platform/PaperPlatformAdapter.java index 660178f7e..6afe61206 100644 --- a/authme-paper/src/main/java/fr/xephi/authme/platform/PaperPlatformAdapter.java +++ b/authme-paper/src/main/java/fr/xephi/authme/platform/PaperPlatformAdapter.java @@ -1,18 +1,11 @@ package fr.xephi.authme.platform; -import fr.xephi.authme.AuthMe; -import fr.xephi.authme.command.CommandDescription; -import fr.xephi.authme.command.CommandHandler; import fr.xephi.authme.listener.PaperChatListener; import fr.xephi.authme.listener.PaperDialogFlowListener; import fr.xephi.authme.listener.PaperLoginValidationListener; +import fr.xephi.authme.listener.PaperProxyAutoLoginListener; import fr.xephi.authme.listener.PlayerOpenSignListener; import fr.xephi.authme.listener.PaperPlayerSpawnLocationListener; -import fr.xephi.authme.process.register.RegisterSecondaryArgument; -import fr.xephi.authme.process.register.RegistrationType; -import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; -import org.bukkit.Location; -import org.bukkit.entity.Player; import org.bukkit.event.Listener; import java.util.Arrays; @@ -53,6 +46,7 @@ public List> getListeners() { Arrays.asList( PaperChatListener.class, PaperDialogFlowListener.class, + PaperProxyAutoLoginListener.class, PaperPlayerSpawnLocationListener.class, PaperLoginValidationListener.class, PlayerOpenSignListener.class)); diff --git a/authme-paper/src/test/java/fr/xephi/authme/platform/PaperPlatformAdapterTest.java b/authme-paper/src/test/java/fr/xephi/authme/platform/PaperPlatformAdapterTest.java index ea6075bcf..9d0b0e7b1 100644 --- a/authme-paper/src/test/java/fr/xephi/authme/platform/PaperPlatformAdapterTest.java +++ b/authme-paper/src/test/java/fr/xephi/authme/platform/PaperPlatformAdapterTest.java @@ -4,6 +4,7 @@ import fr.xephi.authme.listener.EntityListener; import fr.xephi.authme.listener.PaperChatListener; import fr.xephi.authme.listener.PaperDialogFlowListener; +import fr.xephi.authme.listener.PaperProxyAutoLoginListener; import fr.xephi.authme.listener.PaperLoginValidationListener; import fr.xephi.authme.listener.PaperPlayerSpawnLocationListener; import fr.xephi.authme.listener.PlayerListener; @@ -69,6 +70,7 @@ public void getListenersContainsCoreAndPaperListeners() { ServerListener.class, PaperChatListener.class, PaperDialogFlowListener.class, + PaperProxyAutoLoginListener.class, PaperPlayerSpawnLocationListener.class, PaperLoginValidationListener.class, PlayerOpenSignListener.class)); diff --git a/authme-velocity/src/main/java/fr/xephi/authme/velocity/AuthMeVelocityPlugin.java b/authme-velocity/src/main/java/fr/xephi/authme/velocity/AuthMeVelocityPlugin.java index cac2b9733..068494a00 100644 --- a/authme-velocity/src/main/java/fr/xephi/authme/velocity/AuthMeVelocityPlugin.java +++ b/authme-velocity/src/main/java/fr/xephi/authme/velocity/AuthMeVelocityPlugin.java @@ -11,6 +11,7 @@ import com.velocitypowered.api.event.player.PlayerChatEvent; import com.velocitypowered.api.event.player.ServerConnectedEvent; import com.velocitypowered.api.event.player.ServerPreConnectEvent; +import com.velocitypowered.api.event.player.configuration.PlayerEnteredConfigurationEvent; import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; import com.velocitypowered.api.event.proxy.ProxyShutdownEvent; import com.velocitypowered.api.plugin.Plugin; @@ -71,6 +72,11 @@ public void onGameProfileRequest(GameProfileRequestEvent event) { proxyBridge.onGameProfileRequest(event); } + @Subscribe + public void onPlayerEnteredConfiguration(PlayerEnteredConfigurationEvent event) { + proxyBridge.onPlayerEnteredConfiguration(event); + } + @Subscribe public void onServerConnected(ServerConnectedEvent event) { proxyBridge.onServerConnected(event); diff --git a/authme-velocity/src/main/java/fr/xephi/authme/velocity/VelocityProxyBridge.java b/authme-velocity/src/main/java/fr/xephi/authme/velocity/VelocityProxyBridge.java index e2fc5ec6f..89da15f6e 100644 --- a/authme-velocity/src/main/java/fr/xephi/authme/velocity/VelocityProxyBridge.java +++ b/authme-velocity/src/main/java/fr/xephi/authme/velocity/VelocityProxyBridge.java @@ -11,6 +11,7 @@ import com.velocitypowered.api.event.player.GameProfileRequestEvent; import com.velocitypowered.api.event.player.ServerConnectedEvent; import com.velocitypowered.api.event.player.ServerPreConnectEvent; +import com.velocitypowered.api.event.player.configuration.PlayerEnteredConfigurationEvent; import com.velocitypowered.api.proxy.ConnectionRequestBuilder; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ProxyServer; @@ -321,6 +322,45 @@ void onPluginMessage(PluginMessageEvent event) { } } + /** + * Sends the auto-login {@code perform.login} as soon as an already-authenticated player enters the + * configuration phase on an auth server, before the backend decides whether to show its blocking + * pre-join login dialog. The regular {@link #onServerConnected(ServerConnectedEvent)} path fires only + * after the configuration phase completes — too late to suppress that dialog on a server switch. + */ + void onPlayerEnteredConfiguration(PlayerEnteredConfigurationEvent event) { + if (!configuration.autoLoginEnabled()) { + return; + } + ServerConnection server = event.server(); + if (server == null) { + return; + } + RegisteredServer target = server.getServer(); + // The pre-join dialog only appears on backends running AuthMe, i.e. auth servers. + if (!configuration.isAuthServer(target)) { + return; + } + + String normalizedName = normalizeName(event.player().getUsername()); + UUID verifiedPremiumUuid = premiumVerificationManager.getVerifiedPremiumUuid(normalizedName); + boolean isPremiumJoin = verifiedPremiumUuid != null; + if (!authenticationStore.isAuthenticated(normalizedName) && !isPremiumJoin) { + return; + } + + boolean sent = server.sendPluginMessage( + AUTHME_CHANNEL, createPerformLoginMessage(normalizedName, verifiedPremiumUuid)); + if (sent) { + logger.info("Sent config-phase auto-login to auth server '{}' for player {} (verifiedPremiumUuid={})", + target.getServerInfo().getName(), normalizedName, verifiedPremiumUuid); + initiatePendingLogin(normalizedName); + } else { + logger.debug("Config-phase auto-login send to '{}' for {} returned false; onServerConnected will retry", + target.getServerInfo().getName(), normalizedName); + } + } + void onServerConnected(ServerConnectedEvent event) { String playerName = event.getPlayer().getUsername(); String newServer = event.getServer().getServerInfo().getName(); diff --git a/authme-velocity/src/test/java/fr/xephi/authme/velocity/VelocityProxyBridgeTest.java b/authme-velocity/src/test/java/fr/xephi/authme/velocity/VelocityProxyBridgeTest.java index 975869161..7dbd88ecb 100644 --- a/authme-velocity/src/test/java/fr/xephi/authme/velocity/VelocityProxyBridgeTest.java +++ b/authme-velocity/src/test/java/fr/xephi/authme/velocity/VelocityProxyBridgeTest.java @@ -10,6 +10,7 @@ import com.velocitypowered.api.event.player.PlayerChatEvent; import com.velocitypowered.api.event.player.ServerConnectedEvent; import com.velocitypowered.api.event.player.ServerPreConnectEvent; +import com.velocitypowered.api.event.player.configuration.PlayerEnteredConfigurationEvent; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ProxyServer; import com.velocitypowered.api.proxy.ConnectionRequestBuilder; @@ -364,6 +365,55 @@ void shouldForwardPerformLoginWhenSwitchingFromAuthServerToNonAuthServer() { assertPerformLoginPayload(payloadCaptor.getValue(), "alice", "test-secret"); } + @Test + void shouldSendConfigPhaseAutoLoginForAuthenticatedPlayerEnteringAuthServer() { + given(player.getUsername()).willReturn("Alice"); + given(currentServer.getServer()).willReturn(authServer); + given(authServer.getServerInfo()).willReturn(authServerInfo); + given(authServerInfo.getName()).willReturn("lobby"); + given(currentServer.sendPluginMessage(eq(VelocityProxyBridge.AUTHME_CHANNEL), any(byte[].class))) + .willReturn(true); + + VelocityAuthenticationStore store = new VelocityAuthenticationStore(); + store.markAuthenticated("alice"); + + VelocityProxyBridge bridge = new VelocityProxyBridge(proxyServer, logger, createConfiguration(), store, null); + bridge.onPlayerEnteredConfiguration(new PlayerEnteredConfigurationEvent(player, currentServer)); + + verify(currentServer).sendPluginMessage(eq(VelocityProxyBridge.AUTHME_CHANNEL), payloadCaptor.capture()); + assertPerformLoginPayload(payloadCaptor.getValue(), "alice", "test-secret"); + } + + @Test + void shouldNotSendConfigPhaseAutoLoginForUnauthenticatedPlayer() { + given(player.getUsername()).willReturn("Alice"); + given(currentServer.getServer()).willReturn(authServer); + given(authServer.getServerInfo()).willReturn(authServerInfo); + given(authServerInfo.getName()).willReturn("lobby"); + + VelocityProxyBridge bridge = new VelocityProxyBridge(proxyServer, logger, createConfiguration(), + new VelocityAuthenticationStore(), null); + bridge.onPlayerEnteredConfiguration(new PlayerEnteredConfigurationEvent(player, currentServer)); + + verify(currentServer, never()).sendPluginMessage(any(), any(byte[].class)); + } + + @Test + void shouldNotSendConfigPhaseAutoLoginWhenEnteringNonAuthServer() { + given(player.getUsername()).willReturn("Alice"); + given(currentServer.getServer()).willReturn(nonAuthServer); + given(nonAuthServer.getServerInfo()).willReturn(nonAuthServerInfo); + given(nonAuthServerInfo.getName()).willReturn("survival"); + + VelocityAuthenticationStore store = new VelocityAuthenticationStore(); + store.markAuthenticated("alice"); + + VelocityProxyBridge bridge = new VelocityProxyBridge(proxyServer, logger, createConfiguration(), store, null); + bridge.onPlayerEnteredConfiguration(new PlayerEnteredConfigurationEvent(player, currentServer)); + + verify(currentServer, never()).sendPluginMessage(any(), any(byte[].class)); + } + // --- Command blocking tests --- @Test