diff --git a/authme-core/src/main/java/fr/xephi/authme/api/v3/AuthMeApi.java b/authme-core/src/main/java/fr/xephi/authme/api/v3/AuthMeApi.java index 3d8ab194f8..764d6a24cd 100644 --- a/authme-core/src/main/java/fr/xephi/authme/api/v3/AuthMeApi.java +++ b/authme-core/src/main/java/fr/xephi/authme/api/v3/AuthMeApi.java @@ -265,6 +265,27 @@ public void forceLogin(Player player) { management.forceLogin(player); } + /** + * Force a player to login by name, bypassing password verification. + * This is intended for trusted proxy integrations where the proxy has already + * authenticated the player. + * + *

The request is queued for up to 15 seconds. If the player is already online, + * the existing quiet force-login path is used. If the player is in a pre-join + * login dialog, the dialog is closed and the player is marked for force-login on join. + * If the player is offline, the queued request is consumed when the player joins. + * + *

Authentication completion is asynchronous. Observe {@link LoginEvent}, + * {@link RestoreSessionEvent}, or {@link AuthMeApi#isAuthenticated(Player)} later; + * do not assume this method returns after authentication. + * + * @param playerName the player name (case-insensitive) + * @throws IllegalArgumentException if playerName is null + */ + public void forceLoginFromProxy(String playerName) { + management.forceLoginFromProxy(playerName); + } + /** * Force a player to logout. * diff --git a/authme-core/src/main/java/fr/xephi/authme/process/Management.java b/authme-core/src/main/java/fr/xephi/authme/process/Management.java index b87c759c53..603041eb52 100644 --- a/authme-core/src/main/java/fr/xephi/authme/process/Management.java +++ b/authme-core/src/main/java/fr/xephi/authme/process/Management.java @@ -19,6 +19,10 @@ import javax.inject.Inject; +import fr.xephi.authme.data.ProxySessionManager; +import fr.xephi.authme.service.PreJoinDialogService; +import java.util.Locale; + /** * Performs auth actions, e.g. when a player joins, registers or wants to change his password. */ @@ -27,6 +31,12 @@ public class Management { @Inject private BukkitService bukkitService; + @Inject + private ProxySessionManager proxySessionManager; + + @Inject + private PreJoinDialogService preJoinDialogService; + // Processes @Inject private AsyncAddEmail asyncAddEmail; @@ -65,6 +75,17 @@ public void forceLoginFromProxy(Player player) { runTask(() -> asynchronousLogin.forceLoginFromProxy(player)); } + public void forceLoginFromProxy(String playerName) { + String normalizedName = playerName.toLowerCase(Locale.ROOT); + proxySessionManager.processProxySessionMessage(playerName); + Player player = bukkitService.getPlayerExact(playerName); + if (player != null && player.isOnline()) { + forceLoginFromProxy(player); + } else { + preJoinDialogService.approvePreJoinForceLogin(normalizedName); + } + } + public void performLogout(Player player) { // Capture location on the main thread before handing off to the async task. Location quitLocation = player.getLocation(); diff --git a/authme-core/src/test/java/fr/xephi/authme/api/v3/AuthMeApiTest.java b/authme-core/src/test/java/fr/xephi/authme/api/v3/AuthMeApiTest.java index 938b8b22bd..e5c006b377 100644 --- a/authme-core/src/test/java/fr/xephi/authme/api/v3/AuthMeApiTest.java +++ b/authme-core/src/test/java/fr/xephi/authme/api/v3/AuthMeApiTest.java @@ -415,6 +415,18 @@ void shouldForceLogin() { verify(management).forceLogin(player); } + @Test + void shouldForceLoginFromProxyByName() { + // given + String playerName = "Connor"; + + // when + api.forceLoginFromProxy(playerName); + + // then + verify(management).forceLoginFromProxy(playerName); + } + @Test void shouldForceLogout() { // given diff --git a/authme-core/src/test/java/fr/xephi/authme/process/ManagementTest.java b/authme-core/src/test/java/fr/xephi/authme/process/ManagementTest.java new file mode 100644 index 0000000000..37432228f8 --- /dev/null +++ b/authme-core/src/test/java/fr/xephi/authme/process/ManagementTest.java @@ -0,0 +1,84 @@ +package fr.xephi.authme.process; + +import fr.xephi.authme.data.ProxySessionManager; +import fr.xephi.authme.process.login.AsynchronousLogin; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.PreJoinDialogService; +import org.bukkit.entity.Player; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Test for {@link Management}. + */ +@ExtendWith(MockitoExtension.class) +class ManagementTest { + + @InjectMocks + private Management management; + + @Mock + private BukkitService bukkitService; + @Mock + private AsynchronousLogin asynchronousLogin; + @Mock + private ProxySessionManager proxySessionManager; + @Mock + private PreJoinDialogService preJoinDialogService; + + @Test + void shouldForceLoginFromProxyByNameQueuesBeforeOnlineForceLogin() { + // given + String playerName = "Connor"; + Player player = mock(Player.class); + when(bukkitService.getPlayerExact(playerName)).thenReturn(player); + when(player.isOnline()).thenReturn(true); + + // when + management.forceLoginFromProxy(playerName); + + // then + verify(proxySessionManager).processProxySessionMessage(playerName); + verify(bukkitService).runTaskOptionallyAsync(any(Runnable.class)); + } + + @Test + void shouldForceLoginFromProxyByNameApprovesBlockingPreJoinDialog() { + // given + String playerName = "ConNor"; + when(bukkitService.getPlayerExact(playerName)).thenReturn(null); + + // when + management.forceLoginFromProxy(playerName); + + // then + verify(proxySessionManager).processProxySessionMessage(playerName); + verify(preJoinDialogService).approvePreJoinForceLogin("connor"); + verify(bukkitService, never()).runTaskOptionallyAsync(any()); + } + + @Test + void shouldForceLoginFromProxyByNameKeepsQueueWhenNoDialogFutureExists() { + // given + String playerName = "OfflinePlayer"; + when(bukkitService.getPlayerExact(playerName)).thenReturn(null); + when(preJoinDialogService.approvePreJoinForceLogin("offlineplayer")).thenReturn(false); + + // when + management.forceLoginFromProxy(playerName); + + // then + verify(proxySessionManager).processProxySessionMessage(playerName); + verify(preJoinDialogService).approvePreJoinForceLogin("offlineplayer"); + verify(bukkitService, never()).runTaskOptionallyAsync(any()); + } +} \ No newline at end of file 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 17a0256d67..d5de39460c 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 @@ -165,7 +165,7 @@ void shouldOnlyQueueSessionWhenPerformLoginReceivedForOfflinePlayer() { // then verify(proxySessionManager).processProxySessionMessage(playerName, null); - verify(management, never()).forceLoginFromProxy(any()); + verify(management, never()).forceLoginFromProxy(any(Player.class)); verify(bungeeSender, never()).sendAuthMeBungeecordMessage(any(), any()); } @@ -234,7 +234,7 @@ void shouldRemoveQueuedRequestWhenPremiumValidateRejects() { // then verify(proxySessionManager).removeLoginRequest(playerName); - verify(management, never()).forceLoginFromProxy(any()); + verify(management, never()).forceLoginFromProxy(any(Player.class)); verify(bungeeSender, never()).sendAuthMeBungeecordMessage(any(), any()); } @@ -261,7 +261,7 @@ void shouldValidateAndQueueConfigPhasePerformLogin() { // then assertThat(result, equalTo("bobby")); verify(proxySessionManager).processProxySessionMessage(playerName, null); - verify(management, never()).forceLoginFromProxy(any()); + verify(management, never()).forceLoginFromProxy(any(Player.class)); } @Test