Skip to content

feat: let AuthClient take identity and session storage - #149

Closed
sea-snake wants to merge 4 commits into
feat/auth-prompt-hintfrom
feat/auth-configurable-storage
Closed

feat: let AuthClient take identity and session storage#149
sea-snake wants to merge 4 commits into
feat/auth-prompt-hintfrom
feat/auth-configurable-storage

Conversation

@sea-snake

@sea-snake sea-snake commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

AuthClient takes identityStorage and sessionStorage instead of one combined store, which is what lets an application share sign-in state across sibling subdomains by handing it CookieSessionStorage. The client is key-type agnostic as a result: the keyType option, the pending-key machinery, key serialization, StoredKey and the localStorage migration are all gone, because the choice of algorithm belongs to the identity store.

The window flow persists the key only once a delegation is obtained, so a cancelled or failed ceremony can no longer overwrite the key of a session that is still valid.

The redirect flow has to persist on the outbound load, since the key must survive the navigation. To stop that corrupting a live session, #hydrate and #reconcile check that the stored key matches the delegation's leaf public key and discard the session on a mismatch, rather than assembling an identity that signs with a key the delegation does not authorize. Read that pair hardest.

subscribe() and dispose() expose the stores' change events and re-derive the identity on an external sign-in or sign-out. isAuthenticated() is synchronous, reading the session's expiry directly.

Breaking, and the commit carries the footer: keyType, AuthClientStorage, StoredKey, IdbStorage, LocalStorage and the KEY_STORAGE_* exports are removed, storage is renamed to identityStorage, and sessionStorage is added. Existing sessions are not migrated, so users re-authenticate.

Tests: auth-client.test.ts and auth-client-redirect.test.ts.

Two optional constructor options that shape the authorize URL, for silent
re-authentication against Internet Identity:

- `prompt: 'none'` asks II to answer from a delegation it already holds and
  return without rendering anything (or fail with interaction_required),
  rather than running a ceremony. Omitting it (or 'login') signs in normally.
- `hint` is the Principal to re-issue for, so II can pick which stored
  delegation a prompt=none request resolves to.

Both are II-specific extensions inspired by OpenID Connect's prompt/login_hint,
so they travel as query params on the authorize URL rather than in the ICRC
request. Baking them into the URL at construction means one client per authorize
intent; clients share storage, so whichever flow resolves populates the session.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors AuthClient session persistence into two swappable layers: an identity storage responsible for key generation/persistence (algorithm-owned by the store), and a delegation storage responsible for synchronously storing/observing DelegationChain updates (including cross-tab and optional cross-subdomain signaling).

Changes:

  • Introduces AuthClientIdentityStorage (IdbStorage default, plus LocalStorage Ed25519 fallback) and AuthClientDelegationStorage (SyncLocalStorage default, plus SyncCookieStorage hint-cookie variant).
  • Updates AuthClient to use the new storages, adds subscribe() / dispose(), and makes isAuthenticated() synchronous by reading delegation expiry directly.
  • Adds/updates tests for the new storage implementations and updated AuthClient behavior (including redirect flow).

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/client/sync-local-storage.test.ts Adds unit tests for delegation localStorage round-trip and storage-event notifications.
tests/client/sync-cookie-storage.test.ts Adds unit tests for hint-cookie gating, principal mismatch behavior, and notification triggers.
tests/client/storage.test.ts Updates tests to cover identity-store semantics (create/get/remove) for both IndexedDB and localStorage implementations.
tests/client/auth-client.test.ts Updates AuthClient tests to use new storages; adds coverage for external-change subscription reconciliation.
tests/client/auth-client-redirect.test.ts Updates redirect-flow tests to validate memoized identity persistence across loads without pending-key machinery.
src/client/sync-local-storage.ts Implements synchronous delegation storage in localStorage with cross-tab change notifications.
src/client/sync-cookie-storage.ts Implements cross-subdomain hint-cookie delegation storage layered on SyncLocalStorage.
src/client/storage.ts Replaces generic storage with AuthClientIdentityStorage; implements IdbStorage (ECDSA) + LocalStorage (Ed25519).
src/client/index.ts Exports new storage interfaces/implementations and delegation storage key.
src/client/delegation-storage.ts Introduces AuthClientDelegationStorage interface + DELEGATION_STORAGE_KEY.
src/client/db.ts Removes legacy localStorage migration/clearing and relocates DB_VERSION here.
src/client/auth-client.ts Refactors AuthClient to use identity/delegation storages; adds subscribe/dispose and synchronous isAuthenticated().

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/client/local-delegation-storage.ts
Comment thread src/client/storage.ts Outdated
Comment thread src/client/auth-client.ts Outdated
@sea-snake
sea-snake force-pushed the feat/auth-configurable-storage branch 6 times, most recently from 1e4f7b3 to 7dc655f Compare August 17, 2026 11:09
Splits the sign-in keypair out of AuthClient's combined storage into its own
swappable store, one class or interface per file.

IdentityStorage (create/set/get/remove -> SignIdentity) owns the key algorithm
rather than taking it as an option: IdbIdentityStorage keeps a non-extractable
ECDSA key pair in IndexedDB, LocalIdentityStorage an Ed25519 key in
localStorage for environments without CryptoKey. Switching algorithms means
switching the implementation.

create() only mints an identity and set() persists it, so a caller can decide
whether a ceremony that never completed should be allowed to overwrite what is
already stored.

Nothing selects a store yet; AuthClient keeps its own storage until the wiring
two PRs up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sea-snake
sea-snake force-pushed the feat/auth-configurable-storage branch from 7dc655f to 1b35a95 Compare August 23, 2026 10:50
@sea-snake
sea-snake changed the base branch from feat/auth-prompt-hint to feat/auth-delegation-storage August 23, 2026 10:51
@sea-snake sea-snake changed the title feat: configurable identity and delegation storage feat: let AuthClient take identity and delegation storage Aug 23, 2026
@sea-snake
sea-snake force-pushed the feat/auth-delegation-storage branch from 78e9443 to 27b1bdb Compare August 23, 2026 18:18
@sea-snake
sea-snake force-pushed the feat/auth-configurable-storage branch from 1b35a95 to 14d5efe Compare August 23, 2026 18:18
@sea-snake sea-snake changed the title feat: let AuthClient take identity and delegation storage feat: let AuthClient take identity and session storage Aug 23, 2026
@sea-snake
sea-snake force-pushed the feat/auth-delegation-storage branch from 27b1bdb to 6656c45 Compare August 23, 2026 22:17
@sea-snake
sea-snake force-pushed the feat/auth-configurable-storage branch from 14d5efe to 20ce1e9 Compare August 23, 2026 22:18
Adds the second half of AuthClient's persistence as its own swappable store,
holding a Session rather than a bare delegation chain. A session is what an
application is given at sign-in and what its access derives from, so the record
is named for what it is.

SessionStorage (get/set/remove/discard/subscribe over Session) holds only
non-secret material. LocalSessionStorage keeps it in localStorage and notifies
other tabs through the storage event.

CookieSessionStorage composes that and adds a domain-scoped hint cookie carrying
the signed-in principal and the session's expiry, so sibling subdomains of one
domain can see that a session exists and notice a sign-out. The session stays in
localStorage, per origin and too large for a cookie, so the cookie carries no key
material and no chain; each subdomain re-issues its own. readHint() is what lets
an application decide whether to acquire silently.

remove() and discard() exist because ending a session and finding out about one
are different acts. A session serves every sibling of a domain, so an origin that
discovers the chain it held is stale has learned something about itself: a sibling
may have signed in a moment ago and written a hint that is perfectly good.
remove() takes the hint with it, which is what signing out means; discard() leaves
it, so the sibling that signed in is not told its session has gone.

A domain the browser would refuse is rejected in the constructor. Setting such a
cookie fails silently, and the hint is what makes a stored session count as live,
so a session would appear to be forgotten the moment it was written.

Every change source, the storage event, the cookie watchers and the Cookie Store
API, routes through one snapshot comparison, so a single change notifies
subscribers once instead of once per source.

AuthClient starts using these in the PR above.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sea-snake
sea-snake force-pushed the feat/auth-delegation-storage branch from 6656c45 to 181d1c8 Compare August 23, 2026 23:00
AuthClient now takes identityStorage and sessionStorage instead of one combined
store, which is what lets an application share sign-in state across sibling
subdomains by handing it CookieSessionStorage. The client is key-type agnostic as
a result: the keyType option, the pending-key machinery, key (de)serialization,
StoredKey and the localStorage migration are all gone, because the choice of
algorithm now belongs to the identity store.

The window flow persists the key only once a delegation is obtained, so a
cancelled or failed ceremony can no longer overwrite the key of a session that is
still valid. The redirect flow has to persist on the outbound load, since the key
must survive the navigation, so #hydrate and #reconcile verify that the stored key
matches the delegation's leaf public key and discard the session on a mismatch
rather than assembling an identity that signs with an unauthorized key.

subscribe() and dispose() expose the stores' change events and re-derive the
identity on an external sign-in or sign-out. isAuthenticated() is synchronous,
reading the session's expiry directly. Each concrete store owns its own storage
slot, so no slot-key constants are exported.

BREAKING CHANGE: removes the keyType option and the AuthClientStorage and
StoredKey interfaces, along with the IdbStorage, LocalStorage and KEY_STORAGE_*
exports; renames the storage option to identityStorage and adds sessionStorage.
Existing sessions are not migrated and users re-authenticate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sea-snake
sea-snake force-pushed the feat/auth-configurable-storage branch from 20ce1e9 to 98fbafe Compare August 23, 2026 23:05
@sea-snake
sea-snake force-pushed the feat/auth-delegation-storage branch from 181d1c8 to b0b0c23 Compare August 24, 2026 05:42
Base automatically changed from feat/auth-delegation-storage to feat/auth-prompt-hint August 24, 2026 05:42
@sea-snake

Copy link
Copy Markdown
Contributor Author

Closing: this change is not separable the way the stack assumed. Making AuthClient take pluggable identity and session storage retires the combined AuthClientStorage in one move, and the session half cannot land before there is a session to store. It now lands as one layer in #159, whose BREAKING CHANGE footer carries what was here.

@sea-snake sea-snake closed this Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants