From fa33351f8d2a2144f389e9d7696dc78849cae9fb Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Tue, 22 Sep 2026 21:20:41 +0200 Subject: [PATCH] fix: stay signed in after changing the password in settings After a password change the session kept authenticating with the old password: the JMAP client's Basic header, the server-side context the Stalwart management calls use, and the remembered login. The next request was refused and the user was sent back to the login page. A successful password change now switches all three to the new password for Basic-auth sessions. OAuth sessions are unaffected by this change. --- stores/__tests__/account-security-store.test.ts | 4 ++++ stores/account-security-store.ts | 7 +++++++ stores/auth-store.ts | 17 +++++++++++++++++ 3 files changed, 28 insertions(+) diff --git a/stores/__tests__/account-security-store.test.ts b/stores/__tests__/account-security-store.test.ts index 359d553f9..7e8454dde 100644 --- a/stores/__tests__/account-security-store.test.ts +++ b/stores/__tests__/account-security-store.test.ts @@ -9,9 +9,12 @@ vi.mock('@/lib/stalwart/jmap-passthrough', () => ({ }, })); +const updateBasicPassword = vi.fn(async (_password: string) => {}); + vi.mock('@/stores/auth-store', () => ({ useAuthStore: { getState: () => ({ + updateBasicPassword, client: { getAccountId: () => 'acc-primary', hasAccountCapability: (cap: string) => cap === 'urn:stalwart:jmap', @@ -270,6 +273,7 @@ describe('account-security-store', () => { ]); await useAccountSecurityStore.getState().changePassword('old', 'new', ' 123456 '); + expect(updateBasicPassword).toHaveBeenCalledWith('new'); const args = mockedJmap.mock.calls[0][0][0][1]; expect(args.update.singleton).toEqual({ currentSecret: 'old', secret: 'new', 'otpAuth/otpCode': '123456' }); diff --git a/stores/account-security-store.ts b/stores/account-security-store.ts index 2fd566c46..4346d732c 100644 --- a/stores/account-security-store.ts +++ b/stores/account-security-store.ts @@ -579,6 +579,13 @@ export const useAccountSecurityStore = create()((set, get) ], ]); requireAccountPasswordUpdate(responses, 'Failed to change password'); + // The session still authenticates with the old password; switch it over + // so the next request does not bounce the user to the login page. + try { + await useAuthStore.getState().updateBasicPassword(newPassword); + } catch (error) { + debug.warn('auth', 'Password changed, but the session could not switch to it:', error); + } set({ isSaving: false }); } catch (error) { set({ diff --git a/stores/auth-store.ts b/stores/auth-store.ts index 7a519e913..3f24e54ac 100644 --- a/stores/auth-store.ts +++ b/stores/auth-store.ts @@ -80,6 +80,12 @@ interface AuthState { checkAuth: () => Promise; clearError: () => void; syncIdentities: () => void; + /** + * Keep a Basic-auth session working after the user changed the password: + * the client, the server-side management context and the remembered + * session still hold the old one, which the server now refuses. + */ + updateBasicPassword: (newPassword: string) => Promise; refreshIdentities: () => Promise; getClientForAccount: (accountId: string) => JMAPClient | undefined; getAllConnectedClients: () => Map; @@ -2378,6 +2384,17 @@ export const useAuthStore = create()( clearError: () => set({ error: null }), + updateBasicPassword: async (newPassword) => { + const { client, activeAccountId } = get(); + const account = activeAccountId ? useAccountStore.getState().getAccountById(activeAccountId) : undefined; + if (!client || !account || account.authMode !== 'basic') return; + client.updateBasicAuth(newPassword); + await syncStalwartAuthContext(account.serverUrl, account.username, client.getAuthHeader(), account.cookieSlot); + if (account.rememberMe || IS_LITE) { + await persistBasicSession(account.cookieSlot, account.serverUrl, account.username, newPassword); + } + }, + syncIdentities: () => { const identityState = useIdentityStore.getState(); const identities = identityState.identities;