Skip to content

Upgrading to v9

v9 moves the sign-in out of the client and into storage the whole origin reads, and optionally the whole domain. A client is now a cheap view onto that state rather than the thing that holds it, and the rest of these changes follow from that.

In v8 an AuthClient was the sign-in: it held the identity, owned the idle timer, and had to be created once and threaded through every page and component that needed it. In v9 the sign-in lives in storage, so construct a client where you need one and let it go when that view does:

const authClient = new AuthClient();
// whatever your framework calls when the page or component goes away
onUnmount(() => authClient.dispose());

dispose() releases what the client hooked up — its browser listeners, its subscription to the stored state, and the delegation refresh it has scheduled — and leaves the sign-in itself untouched. Two clients on a page, or fifty over a visit, all read the same sign-in, so nothing is lost when one is discarded. Skipping dispose() leaks those listeners, which is the one thing to get right when you move off a shared instance.

isAuthenticated() still works and is still a boolean. getStatus() answers the same question with enough detail to render on:

const status = authClient.getStatus();
// { state: 'signed-in', principal, expiresAtMs }

signed-in means this origin holds a credential and can act as the user, and signed-out means nobody is. The other two are worth handling: expired says the session ended — it ran out, or the provider ended it — and still names whose it was, so you can say “your session ended” rather than showing the bare signed-out page a deliberate sign-out gives you; signed-in-elsewhere says somebody is signed in on this domain while this origin holds nothing for them yet, which you only see once state is shared across subdomains.

Because the state is stored rather than held, it changes for reasons this client had nothing to do with — another tab signing out, the session expiring, a sibling subdomain publishing a sign-in. subscribe() is how those reach you:

const unsubscribe = authClient.subscribe(() => render(authClient.getStatus()));

The listener says that something changed, not what; read getStatus() for the answer. Changes in other tabs arrive the same way, so a sign-out in one tab signs the user out in all of them with no work on your part.

Two different things are stored now: credentials, which are secret and belong to one origin, and the record of who is signed in, which is not secret and is what other tabs converge on.

import { AuthClient, IdbStorage } from '@icp-sdk/auth/client';
import { AuthClient, IdbCredentialStorage } from '@icp-sdk/auth/client';
const authClient = new AuthClient({ storage: new IdbStorage() });
const authClient = new AuthClient({ credentialStorage: new IdbCredentialStorage() });

AuthClientStorage, IdbStorage, LocalStorage, KEY_STORAGE_KEY and KEY_STORAGE_DELEGATION are gone. In their place CredentialStorage has four implementations: IdbCredentialStorage (the default), LocalCredentialStorage, MemoryCredentialStorage and SharedMemoryCredentialStorage. The keyType option went with them, because the key a store can keep is a property of the store: IndexedDB holds a non-extractable ECDSA key, while localStorage needs an Ed25519 key whose bytes serialise.

The new stateStorage option says where the record of the sign-in goes. It defaults to LocalStateStorage, which is what makes tabs of one origin converge. CookieStateStorage widens that to sibling subdomains. If two clients must share an origin without sharing a sign-in, give each one a namespace.

The idle manager is gone, and the session has its own clock

Section titled “The idle manager is gone, and the session has its own clock”

IdleManager has been removed along with everything that configured it: idleOptions, onIdle, idleTimeout, captureScroll and disableIdle. A timer watching the mouse in one tab was only ever a guess at whether the user was still there, and it could not end a session that other tabs, or other subdomains, were still using.

The session is now bounded where it is issued. The identity provider ends it once it has gone unused for maxTimeToIdle, and it cannot outlive maxTimeToLive; both are sign-in options, and both are optional. Leaving them unset means the provider’s own defaults, currently seven days of idleness and thirty days in total — where v8 quietly capped every sign-in at eight hours, so expect sessions to last considerably longer unless you ask for a shorter one.

An idle-out then arrives like any other change, as an expired status. Whatever onIdle did for you belongs there:

new AuthClient({ idleOptions: { onIdle: () => location.reload() } });
authClient.subscribe(() => {
if (authClient.getStatus().state === 'expired') showSessionEnded();
});

identityProvider names a canister as well as a page

Section titled “identityProvider names a canister as well as a page”

The client mints its own delegations by calling the Internet Identity canister, so it has to know which canister, not only which page to open:

new AuthClient({ identityProvider: process.env.II_AUTHORIZE_URL });
new AuthClient({
identityProvider: {
authorizeUrl: process.env.II_AUTHORIZE_URL,
canisterId: process.env.II_CANISTER_ID,
},
});

The option as a whole is optional and omitting it means mainnet Internet Identity, but naming a deployment means naming both halves: nothing is derived from the URL, so a custom authorizeUrl with no canisterId would render the ceremony at your deployment and mint against mainnet. A string or a URL throws a TypeError naming the new shape rather than being quietly ignored, for the same reason.

Two more options went the same way. identity, which let you hand the client a signing identity to delegate to, is gone: the client mints towards the credential it stores, so a passed-in identity had nowhere to live. And targets, which restricted a delegation to named canisters, is no longer a sign-in option.

The delegation the client acts with is short-lived, and the client replaces it shortly before it expires by calling the canister. It also refreshes when a tab returns to the foreground and when the user interacts with the page, so a tab left in the background is usable the moment it is looked at again rather than after the first failed call. disableBrowserActivity turns those hooks off where there is no DOM, and watchForeground and watchActivity are exported if you would rather drive the refresh yourself.

Signing out ends the session at the canister. Every tab and every sibling subdomain that shared it is signed out, and it cannot be resumed — a new sign-in is a new session.

Four errors are worth naming in that flow: SessionGoneError when the session has ended or been revoked, InteractionRequiredError when a silent request needs a real ceremony after all, SessionNotHeldError when a sign-in exists for the domain but this origin holds no credential for it, and SupersededError when a later sign-in or sign-out took over from the one you were waiting on.

Sibling subdomains can share a single sign-in: put the record in a CookieStateStorage, and an origin that finds itself signed-in-elsewhere can acquire its own credential without a ceremony using prompt: 'none' with a hint. The Shared sessions across subdomains guide walks through it end to end.

One-click sign-in also covers organizations now. ssoDomain sends the user straight to their own identity provider, isValidSsoDomain checks a domain they typed before you try, and scopedKeys takes an ssoDomain alongside the existing openIdProvider. See the One-Click Sign-In guide.

Finally, getPrincipal() returns the principal this client acts as, undefined unless the status is signed-in, and agentOptions configures the agent the client builds for its canister calls.