Skip to content
IC Reactor

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:

src/reactor/hooks.ts
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)

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>
)
}

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>
)
}

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).

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.

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.

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>
)
}

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
}

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}</>
}

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.

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.

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>
)
}

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.

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.

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.