From 5295b7762ea0fd90ebd6176aa54ddf4b953125eb Mon Sep 17 00:00:00 2001 From: M57 <41645758+Hy4ri@users.noreply.github.com> Date: Tue, 23 Jun 2026 12:40:33 +0300 Subject: [PATCH] Fix multi-profile token leakage fallback bug (#337) This commit modifies `AuthManager.getToken()` to strictly return the chosen profile's token or `null` if the profile has no token, removing the previous logic that incorrectly fell back to the global token (`KEY_TOKEN`). Why this was necessary: Before this change, if a user switched to a profile that lacked a saved token (Profile B), the system would fall back to the global token, effectively leaking Profile A's credentials to Profile B's configured host. This was an unintended and critical cross-profile authentication leak. Impact: This ensures strong isolation between user profiles. If a profile doesn't have its own credentials, the system will correctly register it as unauthenticated rather than trying to reuse potentially sensitive global tokens. Unit tests have also been updated to ensure the fallback logic is tested as strictly returning `null`. Fixes #258 --- .../hermescontrol/data/local/AuthManager.kt | 3 +-- .../data/local/AuthManagerTest.kt | 18 +++++++++--------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/m57/hermescontrol/data/local/AuthManager.kt b/app/src/main/java/com/m57/hermescontrol/data/local/AuthManager.kt index eccf6a65..ee53fc13 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/local/AuthManager.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/local/AuthManager.kt @@ -147,8 +147,7 @@ object AuthManager { fun getToken(): String? { val selectedId = getSelectedProfileId() if (selectedId != null) { - val token = getProfileToken(selectedId) - if (token != null) return token + return getProfileToken(selectedId) } return requirePrefs().getString(KEY_TOKEN, null) } diff --git a/app/src/test/java/com/m57/hermescontrol/data/local/AuthManagerTest.kt b/app/src/test/java/com/m57/hermescontrol/data/local/AuthManagerTest.kt index 96cea770..72233057 100644 --- a/app/src/test/java/com/m57/hermescontrol/data/local/AuthManagerTest.kt +++ b/app/src/test/java/com/m57/hermescontrol/data/local/AuthManagerTest.kt @@ -139,11 +139,11 @@ class AuthManagerTest { } @Test - fun testGetToken_fallsBackToGlobalWhenProfileTokenMissing() { + fun testGetToken_returnsNullWhenProfileTokenMissing() { every { mockPrefs.getString("selected_profile_id", null) } returns "prof-1" every { mockPrefs.getString("token_prof-1", null) } returns null // no profile token every { mockPrefs.getString("auth_token", null) } returns "global-token" - assertEquals("global-token", AuthManager.getToken()) + assertNull(AuthManager.getToken()) } @Test @@ -293,18 +293,18 @@ class AuthManagerTest { } @Test - fun testGetToken_fallsBackToGlobalWhenSelectedProfileLacksToken() { - // Profile A is selected but has no token → falls back to global + fun testGetToken_returnsNullWhenSelectedProfileLacksToken() { + // Profile A is selected but has no token → should return null, not global every { mockPrefs.getString("selected_profile_id", null) } returns "prof-a" every { mockPrefs.getString("token_prof-a", null) } returns null every { mockPrefs.getString("auth_token", null) } returns "shared-global-token" - assertEquals("shared-global-token", AuthManager.getToken()) + assertNull(AuthManager.getToken()) - // Switch to Profile B, which also has no token → same global fallback + // Switch to Profile B, which also has no token → should return null every { mockPrefs.getString("selected_profile_id", null) } returns "prof-b" every { mockPrefs.getString("token_prof-b", null) } returns null - assertEquals("shared-global-token", AuthManager.getToken()) + assertNull(AuthManager.getToken()) } @Test @@ -314,11 +314,11 @@ class AuthManagerTest { every { mockPrefs.getString("token_prof-a", null) } returns "token-a" assertEquals("token-a", AuthManager.getToken()) - // Profile B has no token, but global exists + // Profile B has no token, but global exists → should return null every { mockPrefs.getString("selected_profile_id", null) } returns "prof-b" every { mockPrefs.getString("token_prof-b", null) } returns null every { mockPrefs.getString("auth_token", null) } returns "fallback" - assertEquals("fallback", AuthManager.getToken()) + assertNull(AuthManager.getToken()) // Profile C has its own token too every { mockPrefs.getString("selected_profile_id", null) } returns "prof-c"