feat(dpop): provide nonce on immediate login

provide the DPoP nonce via cookie on login, this reduce the amount of API call
and prevent having any first call returning in 401
This commit is contained in:
Edouard Vanbelle
2026-08-09 14:40:10 +02:00
parent 2eb1e8a1d5
commit 950c8c0f38
9 changed files with 155 additions and 1 deletions
+7
View File
@@ -301,6 +301,13 @@ export function setLogoutInProgress(value: boolean): void {
logoutInProgress = value;
}
/** Read-only view of the gate — used by cross-tab handlers to distinguish
* OUR logout (already handled by AppShell.onLogout with source=logged_out)
* from ANOTHER tab's logout (which needs a bare redirect). */
export function isLogoutInProgress(): boolean {
return logoutInProgress;
}
// Same shape as `sessionExpiredHandler` — mutable so the app can install
// the real behaviour post-mount, and a fallback for the (rare) case
// where no handler is wired yet (bootstrap, tests). The fallback does
+27
View File
@@ -55,6 +55,33 @@ export function updateNonceFromHeader(fresh: string | null): void {
}
}
/**
* Read the one-shot `oxicloud_dpop_nonce` cookie the backend stamps on
* every login-success response (POST OPAQUE/legacy AND 302 OIDC/magic-
* link), seed the local nonce cache with it, then clear the cookie so a
* later flow can't reuse a stale value.
*
* Call at SPA boot AND from any client-side login-success path
* (`session.setUser()`). The cookie is set by the server unconditionally
* on login when `dpop_mode != off`; if the client lacks DPoP support this
* call is a harmless no-op (the seeded nonce is never used).
*
* SameSite=Strict + non-HttpOnly on the server side — see
* `cookie_auth::maybe_append_dpop_nonce_cookie` in the backend.
*/
export function seedNonceFromCookie(): void {
if (typeof document === 'undefined') return;
const match = document.cookie.split('; ').find((row) => row.startsWith('oxicloud_dpop_nonce='));
if (!match) return;
const value = match.split('=')[1] ?? '';
if (value) updateNonceFromHeader(value);
// Single-shot: expire the cookie so a stale value can't confuse a
// later flow (or, worse, land in the DPoP proof after the nonce has
// rotated server-side past its pool TTL). Path + SameSite must match
// the set-cookie for the browser to accept the deletion.
document.cookie = 'oxicloud_dpop_nonce=; SameSite=Strict; Path=/; Max-Age=0';
}
/** Wipe the current nonce — called on logout so a new session bootstraps fresh. */
export function clearNonce(): void {
currentNonce = null;
@@ -9,6 +9,7 @@
import { bindDpopIfPossible, fetchMe, tryRefresh } from '$lib/api/endpoints/auth';
import { setLogoutInProgress } from '$lib/api/client';
import { hasSessionHint } from '$lib/api/csrf';
import { seedNonceFromCookie } from '$lib/auth/dpop-proof';
import { drives } from '$lib/stores/drives.svelte';
import type { User } from '$lib/api/types';
import { ensureActiveUser } from '$lib/utils/localStoragePrefs';
@@ -95,6 +96,13 @@ class SessionStore {
// `AUTH_PRIMITIVES`, but the /me + /drives + … fetches the app
// fires post-login would all abort with "Session terminated".
setLogoutInProgress(false);
// Consume the one-shot `oxicloud_dpop_nonce` cookie the login
// response set. For POST logins (OPAQUE, legacy, magic-link
// SPA-side, OIDC exchange) this is where the seed lands — the
// hooks.client boot pass fires too early (before any login).
// Redirect-flow logins are seeded at boot; both paths are safe
// to double-run (idempotent, cookie is single-shot).
seedNonceFromCookie();
}
/**