Skip to content

Commit 01fbf68

Browse files
committed
refactor: the session identity resolves its own account key
1 parent 57e3002 commit 01fbf68

2 files changed

Lines changed: 35 additions & 17 deletions

File tree

src/client/auth-client.ts

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -804,17 +804,12 @@ export class AuthClient {
804804
): Promise<SessionIdentity> {
805805
const minter = source ?? (await this.#minterFor(key, sessionChain));
806806

807-
const stored = await this.#credentialStorage.get(this.#slots.app);
808-
let accountKey = stored?.chain?.publicKey;
809-
if (accountKey === undefined) {
810-
const appKey = await this.#credentialStorage.create();
811-
const appChain = await minter.mint(appKey.getPublicKey().toDer());
812-
await this.#credentialStorage.set(this.#slots.app, { identity: appKey, chain: appChain });
813-
accountKey = appChain.publicKey;
814-
}
815-
816-
const identity = new SessionIdentity({
817-
accountKey,
807+
// The identity resolves its own account key, through the same locked
808+
// read-or-mint every rotation uses. Doing it here instead meant a second
809+
// writer of the app slot that took no lock: a load could overwrite a
810+
// credential a peer tab had just minted, and would not see the credential
811+
// that peer had left for it to adopt.
812+
return SessionIdentity.create({
818813
sessionExpiresAtMs: earliestExpiryMs(sessionChain),
819814
source: minter,
820815
storage: this.#credentialStorage,
@@ -826,10 +821,6 @@ export class AuthClient {
826821
this.#identity = new AnonymousIdentity();
827822
},
828823
});
829-
// Adopts what is stored, so the identity a caller is handed already holds a
830-
// delegation rather than minting on its first request.
831-
await identity.refresh();
832-
return identity;
833824
}
834825

835826
// Memoized — only runs #hydrate once, returns the same promise on repeat calls.

tests/client/auth-client.test.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
} from '@icp-sdk/core/identity';
77
import { Principal } from '@icp-sdk/core/principal';
88
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
9+
import { SessionGoneError } from '../../src/client/app-delegation-source.ts';
910
import { AuthClient } from '../../src/client/auth-client.ts';
1011
import {
1112
APP_PENDING_SLOT,
@@ -604,17 +605,43 @@ describe('AuthClient signIn', () => {
604605
await expect(client.signIn()).rejects.toThrow('connection failed');
605606
});
606607

608+
it('fails the sign-in when the session granted has nothing left to mint against', async () => {
609+
const credentialStorage = new MemoryCredentialStorage();
610+
const stateStorage = new MemoryStateStorage();
611+
const client = new AuthClient({
612+
credentialStorage,
613+
stateStorage,
614+
idleOptions: { disableIdle: true },
615+
});
616+
handleSignIn(FakeTransport.last());
617+
618+
// One millisecond of session: the fixture takes the chain's life from the
619+
// requested ceiling. Nothing can be minted against it, so an identity built
620+
// around it could never sign.
621+
await expect(client.signIn({ maxTimeToLive: 1_000_000n })).rejects.toThrow(SessionGoneError);
622+
623+
// And nothing is recorded. Reporting a sign-in that cannot make a call would
624+
// have the application render a signed-in page and fail on its first request,
625+
// which is the outcome this refusal exists to avoid.
626+
expect(stateStorage.get()).toBeNull();
627+
expect(await credentialStorage.get(APP_SLOT)).toBeNull();
628+
});
629+
607630
it('asks for a session, carrying maxTimeToLive and no targets', async () => {
608631
const client = new AuthClient();
609632
const transport = FakeTransport.last();
610633
handleSignIn(transport);
611634

612-
await client.signIn({ maxTimeToLive: 1_000_000n });
635+
// An hour, in nanoseconds. The value only has to survive onto the wire, but
636+
// the fixture derives the session's own life from it — so a token number here
637+
// would build a session already too short to mint against, which is a
638+
// different outcome and not the one this test is about.
639+
await client.signIn({ maxTimeToLive: 3_600_000_000_000n });
613640

614641
const req = transport.requests[0];
615642
expect(req.method).toBe('ii_session_delegation');
616643
expect(req.params?.sessionPublicKey).toEqual(expect.any(String));
617-
expect(req.params?.maxTimeToLive).toBe('1000000');
644+
expect(req.params?.maxTimeToLive).toBe('3600000000000');
618645
// A session chain is restricted to Internet Identity, so an application has
619646
// no targets to ask for: what it may call is decided by the delegations
620647
// minted from the session, not by the session itself.

0 commit comments

Comments
 (0)