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:

const { login } = useAuth()
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")
},
})

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 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
identity SignIdentity | PartialIdentity An existing identity to authenticate via delegation

login() additionally accepts maxTimeToLive, targets, onSuccess, and onError. 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 for OpenID email and profile flows. 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 { 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
nonce: () => 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 || principal.isAnonymous()) {
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 }) {
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 checking auth state
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.

By default, Internet Identity sessions last 8 hours. You can customize this:

login({
maxTimeToLive: BigInt(7 * 24 * 60 * 60 * 1_000_000_000), // 7 days in nanoseconds
})

When the user’s identity 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.

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.