use-identity
v0.2.4
Published
React hooks and utilities for identity management with OIDC-compliant backends
Maintainers
Readme
use-identity
A lightweight, TypeScript-first identity management library for frontend applications. Designed to work with OIDC-compliant identity providers behind a backend-for-frontend (BFF) pattern.
Features
- 🎯 Framework Agnostic Core - Use with any JavaScript framework or vanilla JS/TS
- ⚛️ React Integration - First-class React hooks and context provider
- 🔄 Automatic Caching - Smart caching with configurable TTL
- 🔁 Auto Refresh - Optional automatic identity refresh at intervals
- 📡 Event System - Subscribe to identity state changes
- 🔒 Type Safe - Full TypeScript support with strict typing
- 🪶 Lightweight - Zero runtime dependencies (React is optional peer dep)
- 🌐 SSR Ready - Server-side rendering support for Next.js/Remix
Installation
npm install use-identity
# or
yarn add use-identity
# or
pnpm add use-identityQuick Start
React Usage
import { IdentityProvider, useIdentity } from "use-identity";
// Wrap your app with the provider
function App() {
return (
<IdentityProvider config={{ endpoint: "/api/identity" }}>
<UserProfile />
</IdentityProvider>
);
}
// Use the hook in any component
function UserProfile() {
const { identity, isLoading, isAuthenticated, refresh } = useIdentity();
if (isLoading) {
return <div>Loading...</div>;
}
if (!isAuthenticated) {
return <div>Please log in</div>;
}
return (
<div>
<img src={identity.picture ?? "/default-avatar.png"} alt="Avatar" />
<h1>Hello, {identity.displayName}!</h1>
<p>Email: {identity.email}</p>
<button onClick={() => refresh()}>Refresh</button>
</div>
);
}Non-React Usage
Use the /core entry point for non-React applications:
import { useIdentity, onIdentityChange, IdentityService } from "use-identity/core";
// Simple one-time load
const identity = await useIdentity({ endpoint: "/api/identity" });
if (identity) {
console.log(`Hello, ${identity.displayName}!`);
} else {
console.log("Not authenticated");
}
// Subscribe to changes
const unsubscribe = onIdentityChange((event) => {
console.log("Identity state changed:", event.state);
});
// Full control with IdentityService class
const service = new IdentityService({
endpoint: "/api/identity",
cacheTTL: 60000, // 1 minute cache
refreshInterval: 30000 // Auto-refresh every 30 seconds
});
service.subscribe((event) => {
if (event.type === "identityLoaded") {
console.log("User:", event.state.identity?.displayName);
}
});Configuration
| Option | Type | Default | Description |
| ----------------- | --------- | ------------------ | ---------------------------------------------- |
| endpoint | string | '/api/identity' | URL to fetch identity from |
| cacheTTL | number | 300000 (5 min) | Cache time-to-live in milliseconds |
| refreshInterval | number | 0 (disabled) | Auto-refresh interval in milliseconds |
| fetchOnInit | boolean | true | Whether to fetch immediately on initialization |
| timeout | number | 10000 (10s) | Request timeout in milliseconds |
| retryAttempts | number | 1 | Number of retry attempts for failed requests |
| retryDelay | number | 1000 | Delay between retry attempts |
| fetcher | Fetcher | globalThis.fetch | Custom fetch function |
API Reference
React API
<IdentityProvider>
Context provider that manages identity state for your application.
<IdentityProvider
config={{
endpoint: "/api/identity",
cacheTTL: 60000,
refreshInterval: 30000
}}
>
{children}
</IdentityProvider>useIdentity()
Main hook for accessing identity state and operations.
const {
identity, // Identity | null - The current user identity
status, // 'idle' | 'loading' | 'authenticated' | 'unauthenticated' | 'error'
error, // IdentityError | null - Error information
isLoading, // boolean - True during fetch
isAuthenticated, // boolean - True when authenticated
lastFetchedAt, // number | null - Timestamp of last fetch
refresh, // () => Promise<Identity | null> - Refresh identity
clearIdentity // () => void - Clear cached identity
} = useIdentity();useIdentitySelector(selector, options?)
Select specific parts of identity state to prevent unnecessary re-renders.
// Only re-renders when displayName changes
const displayName = useIdentitySelector((state) => state.identity?.displayName ?? "Guest");
// With custom equality
const user = useIdentitySelector((state) => ({ name: state.identity?.displayName, email: state.identity?.email }), {
isEqual: (a, b) => a.name === b.name && a.email === b.email
});useAuthenticatedIdentity()
Convenience hook that provides proper typing for authenticated users.
const { identity, isAuthenticated, isLoading } = useAuthenticatedIdentity();
if (!isAuthenticated) {
return <Redirect to="/login" />;
}
// TypeScript knows identity is not null here
return <div>Welcome, {identity.displayName}!</div>;useIdentitySSR()
Hook for server-side rendering that handles hydration.
const { identity, isHydrated, isLoading } = useIdentitySSR();
if (!isHydrated || isLoading) {
return <Skeleton />;
}
return identity ? <UserMenu user={identity} /> : <LoginButton />;Core API (Non-React)
IdentityService
The core service class for identity management.
const service = new IdentityService({
endpoint: "/api/identity",
cacheTTL: 60000
});
// Fetch identity
const identity = await service.fetchIdentity();
// Get cached identity (returns null if expired)
const cached = service.getCachedIdentity();
// Subscribe to changes
const unsubscribe = service.subscribe((event) => {
console.log(event.type, event.state);
});
// Clear identity
service.clearIdentity();
// Start/stop auto-refresh
service.startAutoRefresh();
service.stopAutoRefresh();
// Clean up
service.dispose();Convenience Functions
import {
useIdentity,
getCachedIdentity,
clearIdentity,
onIdentityChange,
getIdentityService,
resetIdentityService
} from "use-identity/core";
// Load identity (uses cached if available)
const identity = await useIdentity();
// Get cached without fetching
const cached = getCachedIdentity();
// Clear cached identity
clearIdentity();
// Subscribe to changes
const unsubscribe = onIdentityChange((event) => {
console.log("State changed:", event.state);
});
// Get/create default service instance
const service = getIdentityService();
// Reset default service (useful for testing)
resetIdentityService();Types
Identity
The normalized identity object:
interface Identity {
id: string; // Unique identifier (from 'sub' claim)
displayName: string; // Computed display name
email: string | null;
emailVerified: boolean;
picture: string | null;
claims: OIDCUserInfo; // Full OIDC claims
}IdentityState
The full state object:
interface IdentityState {
identity: Identity | null;
status: "idle" | "loading" | "authenticated" | "unauthenticated" | "error";
error: IdentityError | null;
isLoading: boolean;
isAuthenticated: boolean;
lastFetchedAt: number | null;
}IdentityError
Error information:
interface IdentityError {
code: "NETWORK_ERROR" | "UNAUTHORIZED" | "FORBIDDEN" | "NOT_FOUND" | "SERVER_ERROR" | "INVALID_RESPONSE" | "TIMEOUT" | "UNKNOWN";
message: string;
statusCode?: number;
cause?: Error;
}Backend Setup
This library expects your backend to expose an identity endpoint that returns OIDC UserInfo claims. A typical setup with APISIX:
# APISIX route configuration
routes:
- uri: /api/identity
upstream:
nodes:
- host: 127.0.0.1
port: 8080
plugins:
openid-connect:
# ... OIDC configuration
proxy-rewrite:
# Rewrite to userinfo endpointThe endpoint should return a JSON object with at least a sub claim:
{
"sub": "user-123",
"name": "John Doe",
"email": "[email protected]",
"email_verified": true,
"picture": "https://example.com/avatar.jpg"
}Advanced Usage
Custom Fetcher
const customFetcher = async (url: string, init?: RequestInit) => {
const response = await fetch(url, {
...init,
headers: {
...init?.headers,
"X-Custom-Header": "value"
}
});
return response;
};
const service = new IdentityService({
endpoint: "/api/identity",
fetcher: customFetcher
});Sharing State Between React and Non-React Code
// Create a shared service instance
const sharedService = new IdentityService({
endpoint: '/api/identity',
fetchOnInit: false,
});
// Use in React
function App() {
return (
<IdentityProvider service={sharedService}>
<MyApp />
</IdentityProvider>
);
}
// Use in non-React code (e.g., API client)
sharedService.subscribe((event) => {
if (event.state.isAuthenticated) {
apiClient.setUser(event.state.identity);
}
});Testing
import { vi } from 'vitest';
import { render } from '@testing-library/react';
import { IdentityProvider } from 'use-identity';
const mockUserInfo = {
sub: 'test-user',
name: 'Test User',
email: '[email protected]',
};
const mockFetcher = vi.fn().mockResolvedValue(
new Response(JSON.stringify(mockUserInfo), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})
);
render(
<IdentityProvider config={{ fetcher: mockFetcher }}>
<YourComponent />
</IdentityProvider>
);License
MIT
