feat(dpop): client now aware if session if bound

this prevent client to try binding and creating
- unnecessary call
- unnecessary warning in server log
This commit is contained in:
Edouard Vanbelle
2026-08-09 12:04:45 +02:00
parent df9a6babbb
commit 69c57e1e24
4 changed files with 55 additions and 8 deletions
+14
View File
@@ -256,6 +256,20 @@ export interface User {
* card).
*/
has_password?: boolean;
/**
* TRUE when the caller's current session is DPoP-bound (row's
* `dpop_jkt IS NOT NULL`). Populated only by `/api/auth/me`; other
* User-emitting endpoints leave it unset.
*
* The session store reads this to skip a redundant
* `POST /api/auth/dpop/bind` call — the endpoint returns 409
* `already_bound` on repeated attempts (anti-downgrade invariant)
* and each rejection logs at audit INFO, so a naive "bind on
* every load" pattern was cluttering the audit stream. We only
* fire bind now when there's actual work to do (fresh OIDC /
* magic-link session that landed unbound).
*/
is_dpop_bound?: boolean;
}
/** Fields rendered by the paginated admin table. Full account details remain
+8 -6
View File
@@ -50,12 +50,14 @@ class SessionStore {
// Post-redirect DPoP bind — catches OIDC / magic-link
// flows whose server-side callback creates the session
// UNBOUND (no way for the redirect to carry the JKT in
// the callback body). One-shot per SPA lifetime because
// `this.loaded` guard makes `load()` a singleton;
// server returns 409 if the session is already bound
// (harmless — result is swallowed). Fire-and-forget so
// a slow IndexedDB open doesn't stall the app boot.
void bindDpopIfPossible();
// the callback body). Gate on `is_dpop_bound` so we
// don't call the endpoint on every SPA load: password
// login already binds at session-mint time, so `/me`
// reports `true` on the very first request and skip
// avoids the 409 `already_bound` reject that would
// otherwise clutter the audit stream. Fire-and-forget
// so a slow IndexedDB open doesn't stall app boot.
if (me.is_dpop_bound === false) void bindDpopIfPossible();
} else this.user = null;
} catch {
this.user = null;