Authentication
IC Reactor provides seamless integration with Internet Identity for authentication. This guide covers everything you need to know about user authentication.
Authentication hooks are created by passing an AuthenticationManager to
createAuthHooks. Identity-attribute hooks come from a separate
createIdentityAttributeHooks factory:
import { AuthenticationManager, IdentityAttributesManager, createAuthHooks, createIdentityAttributeHooks,} from "@ic-reactor/react"import { clientManager } from "./index"
export const authentication = new AuthenticationManager({ clientManager })export const identityAttributes = new IdentityAttributesManager(authentication)
export const { useAuth, useUserPrincipal, useAgentState } = createAuthHooks(authentication)
export const { useIdentityAttributes } = createIdentityAttributeHooks(identityAttributes)useAuth
Section titled “useAuth”The primary hook for managing authentication. Provides login, logout, and access to the current identity and principal:
import { useAuth } from "../reactor/hooks"
function AuthButton() { const { login, logout, isAuthenticated, isAuthenticating, principal } = useAuth()
if (isAuthenticating) { return <button disabled>Connecting...</button> }
return isAuthenticated ? ( <button onClick={() => logout()}> Logout {principal?.toText().slice(0, 8)}... </button> ) : ( <button onClick={() => login()}>Login with Internet Identity</button> )}Login Options
Section titled “Login Options”Customize the login flow with options:
import { useAuth } from "../reactor/hooks"
function LoginButton() { const { login } = useAuth()
return ( <button onClick={() => login({ // Identity provider URL (auto-detected based on network) identityProvider: "https://id.ai/authorize",
// Session duration (default: 8 hours) maxTimeToLive: BigInt(7 * 24 * 60 * 60 * 1_000_000_000), // 7 days
// Callbacks — onError receives an optional error string onSuccess: () => { console.log("Logged in!") navigate("/dashboard") }, onError: (error) => { console.error("Login failed:", error) toast.error("Authentication failed") }, }) } > Login </button> )}Client Options
Section titled “Client Options”Every option below can be passed either once, on the AuthenticationManager
constructor, or per call to login() — the per-call value wins.
| Option | Type | Default | Description |
|---|---|---|---|
identityProvider |
string | URL |
auto-detected | Identity provider URL |
derivationOrigin |
string | URL |
– | Origin the principal is derived from — see the callout below |
idleOptions |
AuthClientIdleOptions |
10 min → sign out + reload | @icp-sdk/auth v8 only. Idle detection and what happens on timeout |
storage |
AuthClientStorageLike |
IndexedDB | Persistent storage backend for the session key and delegation |
keyType |
"ECDSA" | "Ed25519" |
"ECDSA" |
Session key algorithm; use "Ed25519" when the storage backend cannot hold a CryptoKey |
windowOpenerFeatures |
string |
– | window.open features string for the identity provider popup |
openIdProvider |
"google" | "apple" | "microsoft" |
– | One-click sign-in through that provider |
transport |
"window" | "redirect" |
"window" |
How the client talks to the provider; "redirect" needs @icp-sdk/auth v8 or v10 |
identity |
SignIdentity | PartialIdentity |
– | An existing identity to authenticate via delegation |
disableBrowserActivity |
boolean |
false |
@icp-sdk/auth v10 only. Stop the client watching the page for activity, so only requests keep the session in use |
login() additionally accepts maxTimeToLive, maxTimeToIdle, targets,
onSuccess, and onError.
@icp-sdk/auth v10 has no equivalent for idleOptions, storage, keyType,
identity or targets, so IC Reactor drops them on v10 with a one-time
warning. maxTimeToIdle and disableBrowserActivity exist only on v10 and are
dropped on v8 the same way. On v10 a custom identityProvider also needs
internetIdentityId, the canister that mints its delegations, because v10 names
a provider by both values. The AuthenticationManager constructor additionally accepts
clientManager (required), authClient (bring your own client instance), and
internetIdentityId (canister ID of a locally deployed Internet Identity).
Auto-Detected Identity Provider
Section titled “Auto-Detected Identity Provider”The AuthenticationManager automatically selects the correct Internet Identity provider based on your network:
| Network | Identity Provider |
|---|---|
| IC (mainnet) | https://id.ai/authorize |
| Local | http://rdmx6-jaaaa-aaaaa-aaadq-cai.localhost:4943/authorize |
The local URL uses the replica port from the agent host and the
internet_identity canister ID from the ic_env cookie when one is present.
A provider named by that cookie is honoured only where the cookie is trusted —
a local replica, or a host you opted in with allowEnvConfig — since any
sibling subdomain can write it; the mainnet provider is fixed regardless. You
can override the whole thing by passing identityProvider.
Identity Attributes
Section titled “Identity Attributes”IC Reactor can request signed identity attributes from @icp-sdk/auth v8 or
v10 for OpenID email and profile flows. requestAttributes is unchanged between
the two majors, so this works identically on either. Use useIdentityAttributes() when a React
component needs to request attributes and send the signed payload to a backend or
canister for verification.
import { useEffect } from "react"import { hexToUint8Array } from "@ic-reactor/react"import { backend } from "../reactor"import { authentication, useIdentityAttributes } from "../reactor/hooks"
function RegisterWithOpenIdProvider() { const { requestOpenIdAttributes, attributes, isRequestingAttributes, attributeError, } = useIdentityAttributes()
// Preload the auth module so the identity provider window can be opened // synchronously from the click handler below. useEffect(() => { authentication.prepareClient() }, [])
async function handleRegister() { const result = await requestOpenIdAttributes({ // Pass the nonce as a callback — see the caution below // A DisplayReactor returns a blob as hex, and the nonce is bytes nonce: async () => hexToUint8Array( await backend.callMethod({ functionName: "register_begin" }) ), openIdProvider: "microsoft", keys: ["email", "name"], })
await backend.callMethod({ functionName: "register_finish", args: [ { data: result.signedAttributes.data, signature: result.signedAttributes.signature, }, ], }) }
return ( <button disabled={isRequestingAttributes} onClick={handleRegister}> {attributes?.decodedAttributes.email ?? attributeError?.message ?? "Continue with provider"} </button> )}Pass a documented auth provider alias ("google", "apple", or
"microsoft") or an issuer URL such as "https://issuer.example.com" for
custom OpenID providers. Only the three aliases change how the user signs in; a
raw issuer URL just scopes the requested keys to openid:<issuer>:<key>.
For non-React code, use identityAttributes.requestOpenId() — or
identityAttributes.request() for non-OpenID keys — on the
IdentityAttributesManager directly.
useUserPrincipal
Section titled “useUserPrincipal”A convenience hook to get the current user’s principal:
import { useUserPrincipal } from "../reactor/hooks"
function UserBadge() { const principal = useUserPrincipal()
if (!principal) { return <span>Anonymous User</span> }
return ( <span className="principal-badge">{principal.toText().slice(0, 8)}...</span> )}useAgentState
Section titled “useAgentState”Access the IC agent’s initialization state. This is useful for showing a global loading screen while the agent (and potential session) is warming up:
import { useAgentState } from "../reactor/hooks"
function AppRoot({ children }: { children: React.ReactNode }) { const { isInitializing, error } = useAgentState()
if (isInitializing) { return <LoadingScreen message="Connecting to IC..." /> }
if (error) { return <ErrorScreen error={error} /> }
return children}Protected Routes
Section titled “Protected Routes”Create a wrapper component for routes that require authentication:
import { useAuth } from "../reactor/hooks"import { Navigate, useLocation } from "react-router-dom"
function ProtectedRoute({ children }: { children: React.ReactNode }) { const { isAuthenticated, isAuthenticating } = useAuth() const location = useLocation()
// Show loading while the session is restored, so a reload does not send a // signed-in user to the login page if (isAuthenticating) { return <LoadingSpinner /> }
// Redirect to login if not authenticated if (!isAuthenticated) { return <Navigate to="/login" state={{ from: location }} replace /> }
return <>{children}</>}Session Persistence
Section titled “Session Persistence”Sessions are automatically persisted in IndexedDB. When users return to your app, the useAuth hook automatically triggers session restoration. clientManager.initialize() only initializes the agent — outside React, call authentication.authenticate() to restore a session.
useAuth() reports isAuthenticating: true until that restore has settled,
from the first render on and in a server render too. So the login button in
useAuth shows “Connecting…” rather than “Login” to a user who is
already signed in, and the protected route waits instead of
redirecting. A restore that fails settles it as well, and the hooks then report
the user signed out.
Several Tabs
Section titled “Several Tabs”Every tab of an origin shares the stored session. With @icp-sdk/auth v10 the
AuthenticationManager follows it: when the user signs out in another tab,
this tab’s manager signs out too and puts the anonymous identity on the agent,
and when they sign in there as another account, this tab adopts that account.
Neither needs a call in this tab. The hooks re-render with the new state, and
the queries of every connected canister are swept as on any sign-in or
sign-out. With v8, which tells a tab nothing, a tab notices only when it checks
the session again, as authenticate() or a newly mounted useAuth() does.
Session Duration
Section titled “Session Duration”By default, Internet Identity sessions last 8 hours. You can customize this:
import { useAuth } from "../reactor/hooks"
function LoginButton() { const { login } = useAuth()
return ( <button onClick={() => login({ maxTimeToLive: BigInt(7 * 24 * 60 * 60 * 1_000_000_000), // 7 days in nanoseconds }) } > Login </button> )}Discarding a Manager
Section titled “Discarding a Manager”An AuthenticationManager built once at module scope lives as long as the page
and needs nothing more. One built per mount does not: a provider in a
server-rendered app builds one per render tree, and a widget may build its own
reactor stack each time it opens. The manager builds its Internet Identity
client on first use, and a @icp-sdk/auth v10 client listens to the page
(activity, focus and the session record every tab shares) until it is disposed.
A provider from createReactorProvider
disposes every manager built for its value when its tree unmounts, and leaves
alone one built elsewhere and passed in. A manager built per mount any other
way is released from the cleanup of whatever built it:
import { useEffect, useState } from "react"import { AuthenticationManager } from "@ic-reactor/react"
function Widget() { const [authentication] = useState( () => new AuthenticationManager({ clientManager }) ) useEffect(() => () => authentication.dispose(), [authentication]) // ...}dispose() disposes the client the manager built and forgets it, so a later
login(), logout() or prepareClient() builds a new one. That keeps it safe
under StrictMode, which runs the cleanup and the effect again on the same
manager. It leaves the auth state and the agent’s identity as they are, and it
does not sign anyone out. A client you passed in as authClient is never
disposed. With v8 there is nothing to release, and the manager only drops its
reference.
A tree can unmount before the session restore its useAuth() started has
finished, as a widget closed right after it opened does. That restore then
stops without building a client, and if the same manager is mounted again, as
<Activity> mounts a tree it shows again, its useAuth() restores the session
from the start.
Automatic Query Invalidation
Section titled “Automatic Query Invalidation”When the user’s principal changes — on login, logout, and session restore — IC Reactor sweeps the cache of every connected canister: in-flight queries are cancelled, inactive cached entries are removed, and active queries are invalidated so they refetch as the new identity.
Removal matters because query keys carry no principal. An entry that was only invalidated would still be readable through getQueryData or fetchQuery under the next identity — so the previous principal’s inactive data (a balance, a profile, a deposit address) is dropped outright, and only the queries a mounted component is actively watching live on through a refetch.
The sweep is scoped by canister ID (every reactor query key starts with one), so sharing a QueryClient with the rest of your app does not cause your unrelated REST or GraphQL queries to refetch on every sign-in.
A sign-in that returns the principal already signed in — signing in again to extend a session, or an identity attribute request, which signs in by default — is not an identity change: the cache is kept, and only queries whose last fetch failed refetch.
Without Authentication
Section titled “Without Authentication”If your app doesn’t need authentication, you can skip the @icp-sdk/auth package entirely. The ClientManager will work with an anonymous identity. Queries will work, but update calls requiring authentication will fail.
Further Reading
Section titled “Further Reading”- useAuth — Main auth hook reference
- useAgentState — Agent state reference
- React Setup — Complete React configuration
- Error Handling — Handle auth and query errors
- Query Caching — Understand query invalidation