From 6173a9ad13ebbe8321b75ef5033f30b58b18b08d Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Thu, 2 Apr 2026 10:54:35 +0200 Subject: [PATCH] fix: resolve settings sync Identity mismatch for OAuth/SSO sessions #127 --- app/[locale]/login/page.tsx | 2 +- app/api/settings/route.ts | 26 +++++++++++++++++++++----- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/app/[locale]/login/page.tsx b/app/[locale]/login/page.tsx index 4ec6a56d..b2c229fb 100644 --- a/app/[locale]/login/page.tsx +++ b/app/[locale]/login/page.tsx @@ -16,7 +16,7 @@ import { discoverOAuth, type OAuthMetadata } from "@/lib/oauth/discovery"; import { generateCodeVerifier, generateCodeChallenge, generateState } from "@/lib/oauth/pkce"; import { OAUTH_SCOPES } from "@/lib/oauth/tokens"; -const APP_VERSION = "1.4.10"; +const APP_VERSION = "1.4.11"; const THEME_OPTIONS = [ { value: "light" as const, icon: Sun, label: "Light" }, diff --git a/app/api/settings/route.ts b/app/api/settings/route.ts index 3e0c966e..72a34ca4 100644 --- a/app/api/settings/route.ts +++ b/app/api/settings/route.ts @@ -3,6 +3,7 @@ import { cookies } from 'next/headers'; import { logger } from '@/lib/logger'; import { decryptSession } from '@/lib/auth/crypto'; import { sessionCookieName } from '@/lib/auth/session-cookie'; +import { readStalwartAuthContextFromStore } from '@/lib/stalwart/auth-context'; import { saveUserSettings, loadUserSettings, deleteUserSettings } from '@/lib/settings-sync'; import { configManager } from '@/lib/admin/config-manager'; @@ -50,21 +51,36 @@ function isEnabled(): boolean { return process.env.SETTINGS_SYNC_ENABLED === 'true' && !!process.env.SESSION_SECRET; } +/** Strip trailing slashes so differently-formatted URLs still match. */ +function normalizeUrl(url: string): string { + return url.replace(/\/+$/, ''); +} + /** * Verify identity against session cookies across all account slots. * With multi-account, the requesting account may be on any slot (0-4). - * Returns true only if a matching session cookie is found. + * Checks both basic-auth session cookies and stalwart auth context cookies + * (used by OAuth/SSO and TOTP-upgraded sessions). + * Returns true only if a matching cookie is found. */ async function verifyIdentity(username: string, serverUrl: string): Promise { const cookieStore = await cookies(); + const normalizedServerUrl = normalizeUrl(serverUrl); for (let slot = 0; slot <= 4; slot++) { + // Check basic-auth session cookie const token = cookieStore.get(sessionCookieName(slot))?.value; - if (!token) continue; + if (token) { + const session = decryptSession(token); + if (session && session.username === username && normalizeUrl(session.serverUrl) === normalizedServerUrl) { + return true; + } + } - const session = decryptSession(token); - if (session && session.username === username && session.serverUrl === serverUrl) { - return true; // Found a matching slot + // Check stalwart auth context cookie (set for all auth modes) + const ctx = readStalwartAuthContextFromStore(cookieStore, slot); + if (ctx && ctx.username === username && normalizeUrl(ctx.serverUrl) === normalizedServerUrl) { + return true; } }