Merge branch 'main' of https://github.com/bulwarkmail/webmail
This commit is contained in:
@@ -7,6 +7,7 @@ import { useTranslations } from "next-intl";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useAuthStore } from "@/stores/auth-store";
|
||||
import { useAccountStore } from "@/stores/account-store";
|
||||
import { useThemeStore } from "@/stores/theme-store";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import { useConfig } from "@/hooks/use-config";
|
||||
@@ -460,6 +461,16 @@ export default function LoginPage() {
|
||||
sessionStorage.setItem("oauth_add_account_mode", "true");
|
||||
}
|
||||
|
||||
// Persist the next-free cookie slot so loginWithOAuth (in stores/auth-store.ts)
|
||||
// writes the refresh token to the correct per-account jmap_rt_<slot> cookie.
|
||||
// loginWithOAuth reads this key but it was previously never written, so every
|
||||
// OAuth account collapsed onto slot 0 and clobbered earlier accounts' refresh
|
||||
// tokens. getNextCookieSlot() returns 0 when no accounts exist (correct for
|
||||
// first sign-in) and the lowest unused slot otherwise (correct for "+ Add
|
||||
// Account").
|
||||
const nextSlot = useAccountStore.getState().getNextCookieSlot();
|
||||
sessionStorage.setItem("oauth_cookie_slot", nextSlot.toString());
|
||||
|
||||
const authUrl = new URL(oauthMetadata.authorization_endpoint);
|
||||
authUrl.searchParams.set("response_type", "code");
|
||||
authUrl.searchParams.set("client_id", oauthClientId);
|
||||
|
||||
@@ -13,12 +13,18 @@ export async function POST(request: NextRequest) {
|
||||
const cookieStore = await cookies();
|
||||
|
||||
try {
|
||||
const { code, state } = await request.json();
|
||||
const { code, state, slot: bodySlot } = await request.json();
|
||||
|
||||
if (!code || !state) {
|
||||
return NextResponse.json({ error: 'Missing code or state' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Per-account refresh-token cookie slot. Without this the route hardcoded
|
||||
// slot 0, so the "+ Add Account" flow overwrote the first account's
|
||||
// refresh-token cookie. Default to 0 for back-compat with any caller that
|
||||
// omits slot. Mirrors the validation in /api/auth/token POST.
|
||||
const slot = typeof bodySlot === 'number' && bodySlot >= 0 && bodySlot <= 4 ? bodySlot : 0;
|
||||
|
||||
// Read and decrypt the pending SSO cookie
|
||||
const pendingCookie = cookieStore.get(SSO_PENDING_COOKIE)?.value;
|
||||
if (!pendingCookie) {
|
||||
@@ -58,9 +64,9 @@ export async function POST(request: NextRequest) {
|
||||
// Exchange code for tokens
|
||||
const tokens = await exchangeCodeForTokens(code, codeVerifier, redirectUri);
|
||||
|
||||
// Store refresh token
|
||||
// Store refresh token in the per-account cookie slot.
|
||||
if (tokens.refresh_token) {
|
||||
const cookieName = refreshTokenCookieName(0);
|
||||
const cookieName = refreshTokenCookieName(slot);
|
||||
cookieStore.set(cookieName, tokens.refresh_token, getCookieOptions());
|
||||
}
|
||||
|
||||
|
||||
+23
-9
@@ -601,12 +601,21 @@ export const useAuthStore = create<AuthState>()(
|
||||
set({ isLoading: true, error: null, isRateLimited: false, rateLimitUntil: null });
|
||||
|
||||
try {
|
||||
// Determine slot for this account (use slot from sessionStorage if re-adding)
|
||||
// Determine slot for this account (use slot from sessionStorage if re-adding).
|
||||
// Note: `parseInt(getItem(...) || '0')` collapses "no value set" and
|
||||
// "value is 0" into the same case, so the fallback to getNextCookieSlot()
|
||||
// never fired for the common "+ Add Account" path — every OAuth account
|
||||
// ended up on slot 0 and overwrote earlier accounts' refresh-token cookies.
|
||||
// Distinguishing rawSlot === null from a parsed 0 fixes that. The page
|
||||
// also writes oauth_cookie_slot before redirecting to the IdP.
|
||||
const accountStore = useAccountStore.getState();
|
||||
const pendingSlot = typeof window !== 'undefined'
|
||||
? parseInt(sessionStorage.getItem('oauth_cookie_slot') || '0', 10)
|
||||
: 0;
|
||||
const slot = pendingSlot >= 0 && pendingSlot <= 4 ? pendingSlot : accountStore.getNextCookieSlot();
|
||||
const rawSlot = typeof window !== 'undefined'
|
||||
? sessionStorage.getItem('oauth_cookie_slot')
|
||||
: null;
|
||||
const pendingSlot = rawSlot !== null ? parseInt(rawSlot, 10) : NaN;
|
||||
const slot = !isNaN(pendingSlot) && pendingSlot >= 0 && pendingSlot <= 4
|
||||
? pendingSlot
|
||||
: accountStore.getNextCookieSlot();
|
||||
|
||||
const tokenRes = await apiFetch(`/api/auth/token?slot=${slot}`, {
|
||||
method: 'POST',
|
||||
@@ -718,12 +727,19 @@ export const useAuthStore = create<AuthState>()(
|
||||
set({ isLoading: true, error: null, isRateLimited: false, rateLimitUntil: null });
|
||||
|
||||
try {
|
||||
// Server-side SSO: the server holds the PKCE verifier in an encrypted cookie
|
||||
// Server-side SSO: the server holds the PKCE verifier in an encrypted cookie.
|
||||
// Pass the next-free cookie slot so /api/auth/sso/complete writes the refresh
|
||||
// token to the correct per-account jmap_rt_<slot> cookie. Without this the
|
||||
// route hardcoded slot 0, which broke "+ Add Account" by overwriting the
|
||||
// first account's refresh-token cookie.
|
||||
const accountStore = useAccountStore.getState();
|
||||
const slot = accountStore.getNextCookieSlot();
|
||||
|
||||
const ssoRes = await apiFetch('/api/auth/sso/complete', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ code, state }),
|
||||
body: JSON.stringify({ code, state, slot }),
|
||||
});
|
||||
|
||||
if (!ssoRes.ok) {
|
||||
@@ -741,8 +757,6 @@ export const useAuthStore = create<AuthState>()(
|
||||
throw new Error('Server URL not configured');
|
||||
}
|
||||
|
||||
const accountStore = useAccountStore.getState();
|
||||
|
||||
const refreshFn = get().refreshAccessToken;
|
||||
const client = JMAPClient.withBearer(ssoServerUrl, access_token, '', () => refreshFn());
|
||||
await client.connect();
|
||||
|
||||
Reference in New Issue
Block a user