@goiam/react
Version:
React SDK for Go-IAM - A lightweight Identity and Access Management server
815 lines (622 loc) • 20.9 kB
Markdown
# GoIAM React SDK
<div className="flex gap-2 mb-4">
<a href="https://badge.fury.io/js/@goiam%2Freact" target="_blank" rel="noopener noreferrer" className="mr-4">
<img src="https://badge.fury.io/js/@goiam%2Freact.svg" alt="npm version" />
</a>
<a href="https://www.npmjs.com/package/@goiam/react" target="_blank" rel="noopener noreferrer">
<img src="https://img.shields.io/npm/dm/@goiam/react.svg" alt="npm downloads" />
</a>
</div>
A React SDK for integrating with Go-IAM (Identity and Access Management) server. This package provides React hooks and utilities for handling authentication in your React applications using [Hookstate](https://hookstate.js.org/) for optimal performance and minimal re-renders.
## Installation
```bash
npm install /react
```
or
```bash
yarn add /react
```
## Features
- 🔐 **Easy Authentication**: Simple integration with Go-IAM server
- � **High Performance**: Built with Hookstate for minimal re-renders
- 🎯 **Resource-Based Access Control**: Support for resource-based authorization
- 🪝 **Simple Hook API**: Easy access to authentication state and methods
- 💾 **Persistent Storage**: Automatic user data storage and retrieval
- 🔄 **Auto Refresh**: Automatic user profile updates with smart caching
- 📱 **TypeScript Support**: Full TypeScript support with comprehensive types
- ⚡ **PKCE Support**: Secure OAuth2 PKCE flow implementation
## Quick Start
### 1. Basic Setup
No provider needed! Just start using the hook:
```tsx
import React from 'react';
import { useGoIam } from '/react';
function LoginButton() {
const { user, login, logout, loadedState } = useGoIam();
if (!loadedState) {
return <div>Loading...</div>;
}
if (user) {
return (
<div>
<p>Welcome, {user.name || user.email}!</p>
<button onClick={logout}>Logout</button>
</div>
);
}
return <button onClick={login}>Login</button>;
}
```
### 2. Configure Your App
Set up the base URL and client ID:
```tsx
import React, { useEffect } from 'react';
import { useGoIam } from '/react';
function App() {
const { setBaseUrl, setClientId, dashboardMe } = useGoIam();
useEffect(() => {
// Configure the SDK
setBaseUrl('https://your-goiam-server.com');
setClientId('your-client-id');
// Load user data on app start
dashboardMe();
}, [setBaseUrl, setClientId, dashboardMe]);
return <div>Your App Content</div>;
}
```
### 3. Handle Authentication Callback
Complete authentication after redirect from GoIAM server:
```tsx
import React, { useEffect } from 'react';
import { useGoIam } from '/react';
function CallbackScreen() {
const { user, verify, loadedState, verifying } = useGoIam();
useEffect(() => {
// Extract parameters from URL query string
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const codeChallenge = localStorage.getItem('code_challenge'); // Stored during login
if (code && codeChallenge && !user && !verifying) {
// Complete authentication
verify(codeChallenge, code);
}
}, [verify, user, verifying]);
if (verifying) {
return <div>Completing authentication...</div>;
}
if (!loadedState) {
return <div>Loading...</div>;
}
if (user) {
return (
<div>
<p>Welcome back, {user.name || user.email}!</p>
<a href="/">Go to Dashboard</a>
</div>
);
}
return <div>Authentication failed. Please try again.</div>;
}
```
### 4. Resource-Based Access Control
Check user permissions:
```tsx
import React from 'react';
import { useGoIam } from '/react';
function AdminPanel() {
const { user, hasRequiredResources } = useGoIam();
if (!user) {
return <div>Please log in to access this area.</div>;
}
if (!hasRequiredResources(['admin', 'user_management'])) {
return <div>You don't have permission to access this area.</div>;
}
return (
<div>
<h2>Admin Panel</h2>
<p>Welcome to the admin area!</p>
</div>
);
}
```
### 5. Using AuthGuard for Route Protection
Protect your routes with the AuthGuard component:
```tsx
import React from 'react';
import { AuthGuard } from '/react';
// Basic route protection
function ProtectedRoute() {
return (
<AuthGuard>
<div>This content is only visible to authenticated users!</div>
</AuthGuard>
);
}
// Auto-redirect to login
function AutoRedirectRoute() {
return (
<AuthGuard redirectToLogin={true}>
<div>Protected content with auto redirect</div>
</AuthGuard>
);
}
// Resource-based protection
function AdminRoute() {
return (
<AuthGuard requiredResources={['admin']}>
<div>Admin-only content</div>
</AuthGuard>
);
}
// Custom fallback components
function CustomFallbackRoute() {
const CustomLogin = () => (
<div style={{ textAlign: 'center', padding: '2rem' }}>
<h3>🔒 Login Required</h3>
<p>Please sign in to continue</p>
</div>
);
const CustomUnauthorized = () => (
<div style={{ textAlign: 'center', padding: '2rem' }}>
<h3>⛔ Access Denied</h3>
<p>You need admin permissions</p>
</div>
);
return (
<AuthGuard
requiredResources={['admin']}
fallback={CustomLogin}
unauthorizedComponent={CustomUnauthorized}
>
<div>Protected admin content</div>
</AuthGuard>
);
}
```
#### Using the withAuthGuard HOC
For component-based protection:
```tsx
import React from 'react';
import { withAuthGuard } from '/react';
// Basic component protection
const ProtectedComponent = () => <div>This component is protected!</div>;
export const GuardedComponent = withAuthGuard(ProtectedComponent);
// With guard options
const AdminComponent = () => <div>Admin dashboard content</div>;
export const GuardedAdminComponent = withAuthGuard(AdminComponent, {
requiredResources: ['admin'],
redirectToLogin: true,
});
// Usage in your app
function App() {
return (
<div>
<GuardedComponent />
<GuardedAdminComponent />
</div>
);
}
```
#### AuthGuard Props
| Prop | Type | Default | Description |
| ----------------------- | --------------- | --------------------------------- | --------------------------------------------------------- |
| `children` | `ReactNode` | - | Content to render when authenticated and authorized |
| `fallback` | `ComponentType` | `DefaultUnauthenticatedComponent` | Component to show when user is not authenticated |
| `redirectToLogin` | `boolean` | `false` | Automatically redirect to login page if not authenticated |
| `requiredResources` | `string[]` | `[]` | Array of required resource permissions |
| `unauthorizedComponent` | `ComponentType` | `DefaultUnauthorizedComponent` | Component to show when user lacks required resources |
## API Reference
### useGoIam Hook
The main hook to access authentication state and methods. Built with Hookstate for optimal performance.
```tsx
const {
// State
user, // User | undefined: Current user data
loadedState, // boolean: Whether initial data load is complete
clientAvailable, // boolean: Whether client is properly configured
verifying, // boolean: Whether verification is in progress
loadingMe, // boolean: Whether user data is being fetched
verified, // boolean: Whether user has verified their account
err, // string: Any error message
baseUrl, // string: Current base URL
clientId, // string: Current client ID
loginPageUrl, // string: Login page URL
callbackPageUrl, // string: Callback page URL
// Actions
login, // () => void: Redirect to login
logout, // () => void: Clear user data and logout
verify, // (codeChallenge: string, code: string) => Promise<void>: Complete authentication
dashboardMe, // (dontUpdateTime?: boolean) => Promise<void>: Fetch user dashboard data
me, // () => Promise<void>: Fetch user profile
hasRequiredResources, // (resources: string[]) => boolean: Check user permissions
// Configuration
setBaseUrl, // (url: string) => void: Set API base URL
setClientId, // (id: string) => void: Set client ID
setLoginPageUrl, // (url: string) => void: Set login page URL
setCallbackPageUrl, // (url: string) => void: Set callback page URL
setLoadingMe, // (loading: boolean) => void: Set loading state
} = useGoIam();
```
### Key Methods
#### `dashboardMe(dontUpdateTime?: boolean)`
Fetches user dashboard data including setup information and user profile. Includes smart caching - skips API call if data was fetched recently (within 5 minutes) unless `dontUpdateTime` is true.
```tsx
// Standard fetch with caching
await dashboardMe();
// Force fetch, bypass cache
await dashboardMe(true);
```
#### `verify(codeChallenge: string, code: string)`
Completes the OAuth2 PKCE authentication flow with the authorization code and code challenge.
```tsx
const codeChallenge = localStorage.getItem('code_challenge');
const code = new URLSearchParams(window.location.search).get('code');
await verify(codeChallenge, code);
```
#### `hasRequiredResources(resources: string[])`
Checks if the current user has all the specified resources/permissions.
```tsx
// Check single resource
const canAdmin = hasRequiredResources(['admin']);
// Check multiple resources (user must have ALL)
const canManageUsers = hasRequiredResources(['admin', 'user_management']);
```
### User Data Structure
```tsx
interface User {
id: string;
name: string;
email: string;
profile_pic: string;
created_at: string;
updated_at: string;
created_by: string;
updated_by: string;
enabled: boolean;
expiry: string;
resources: Record<string, Resource>; // Resource permissions
roles: Record<string, Role>; // User roles
}
interface Resource {
id: string;
key: string;
name: string;
}
interface Role {
id: string;
name: string;
}
```
## Authentication Flow
1. **Initialization**: Hook checks for stored user data and validates with API
2. **Configuration**: Set base URL and client ID using `setBaseUrl()` and `setClientId()`
3. **Login**: User clicks login → generates PKCE challenge → redirects to GoIAM auth URL
4. **Callback**: After successful authentication, user returns to your app with authorization code
5. **Verification**: Call `verify()` with code challenge and authorization code
6. **Profile Fetch**: SDK automatically fetches and stores user profile data
7. **State Management**: Authentication state is maintained globally with Hookstate
## Smart Caching
The SDK includes intelligent caching to reduce API calls:
- **Dashboard data**: Cached for 5 minutes, automatically skips refetch if recently updated
- **Local storage sync**: User data persisted automatically and synced on app load
- **Timestamp tracking**: Tracks when data was last updated to make caching decisions
```tsx
// Will use cache if data fetched within last 5 minutes
await dashboardMe();
// Forces fresh fetch, ignores cache
await dashboardMe(true);
```
## Error Handling
The SDK provides comprehensive error handling through the `err` state:
```tsx
function MyComponent() {
const { err, loadedState } = useGoIam();
if (err) {
return <div>Authentication error: {err}</div>;
}
if (!loadedState) {
return <div>Loading...</div>;
}
// Render authenticated content
}
```
## Advanced Usage
### Custom Configuration
Set different URLs for login and callback pages:
```tsx
function App() {
const { setLoginPageUrl, setCallbackPageUrl } = useGoIam();
useEffect(() => {
setLoginPageUrl('/custom-login');
setCallbackPageUrl('/auth/callback');
}, [setLoginPageUrl, setCallbackPageUrl]);
}
```
### Manual State Management
Control loading states manually if needed:
```tsx
function CustomLoader() {
const { setLoadingMe, loadingMe } = useGoIam();
const handleCustomLoad = async () => {
setLoadingMe(true);
try {
// Custom loading logic
} finally {
setLoadingMe(false);
}
};
return (
<div>
{loadingMe && <div>Loading...</div>}
<button onClick={handleCustomLoad}>Load Data</button>
</div>
);
}
```
### Resource-Based Conditional Rendering
```tsx
function ConditionalContent() {
const { hasRequiredResources } = useGoIam();
return (
<div>
{hasRequiredResources(['view_dashboard']) && <DashboardWidget />}
{hasRequiredResources(['admin', 'user_management']) && <AdminTools />}
{hasRequiredResources(['billing']) ? (
<BillingSection />
) : (
<div>Upgrade to access billing features</div>
)}
</div>
);
}
```
## TypeScript Support
The SDK is built with TypeScript and provides comprehensive type definitions:
```tsx
import { useGoIam } from '/react';
import type { User } from '/react';
// Type-safe user handling
const handleUser = (user: User) => {
console.log(`Welcome ${user.name}!`);
console.log('User resources:', Object.keys(user.resources));
};
// Type-safe hook usage
const MyComponent = () => {
const {
user,
hasRequiredResources,
setBaseUrl,
}: {
user: User | undefined;
hasRequiredResources: (resources: string[]) => boolean;
setBaseUrl: (url: string) => void;
} = useGoIam();
return <div>Typed component</div>;
};
```
## Examples
### Complete App Setup
```tsx
import React, { useEffect } from 'react';
import { useGoIam } from '/react';
function App() {
return (
<div>
<AuthSetup />
<Router />
</div>
);
}
function AuthSetup() {
const { setBaseUrl, setClientId, dashboardMe } = useGoIam();
useEffect(() => {
// Configure SDK
setBaseUrl(process.env.REACT_APP_GOIAM_URL!);
setClientId(process.env.REACT_APP_CLIENT_ID!);
// Load initial user data
dashboardMe();
}, [setBaseUrl, setClientId, dashboardMe]);
return null;
}
function Router() {
const { user, loadedState } = useGoIam();
if (!loadedState) {
return <div>Loading app...</div>;
}
return <div>{user ? <Dashboard /> : <LoginPage />}</div>;
}
function Dashboard() {
const { user, logout, hasRequiredResources } = useGoIam();
return (
<div>
<header>
<h1>Welcome, {user?.name}!</h1>
<button onClick={logout}>Logout</button>
</header>
<main>
{hasRequiredResources(['admin']) && <AdminPanel />}
{hasRequiredResources(['billing']) && <BillingSection />}
<UserProfile />
</main>
</div>
);
}
function LoginPage() {
const { login } = useGoIam();
return (
<div>
<h1>Please log in</h1>
<button onClick={login}>Login with GoIAM</button>
</div>
);
}
```
### Callback Handler
```tsx
import React, { useEffect, useState } from 'react';
import { useGoIam } from '/react';
function CallbackPage() {
const { verify, verifying, user } = useGoIam();
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const handleCallback = async () => {
try {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const codeChallenge = localStorage.getItem('code_challenge');
if (!code) {
setError('No authorization code received');
return;
}
if (!codeChallenge) {
setError('No code challenge found in storage');
return;
}
await verify(codeChallenge, code);
// Clean up
localStorage.removeItem('code_challenge');
// Redirect to app
window.location.href = '/dashboard';
} catch (err) {
setError(err instanceof Error ? err.message : 'Authentication failed');
}
};
handleCallback();
}, [verify]);
if (error) {
return (
<div>
<h2>Authentication Error</h2>
<p>{error}</p>
<a href="/login">Try Again</a>
</div>
);
}
if (verifying) {
return <div>Completing authentication...</div>;
}
if (user) {
return <div>Authentication successful! Redirecting...</div>;
}
return <div>Processing...</div>;
}
```
## Performance Benefits
### Why Hookstate?
This SDK uses [Hookstate](https://hookstate.js.org/) instead of React Context for several performance advantages:
- **Minimal Re-renders**: Only components that use specific state properties re-render when those properties change
- **Global State**: No provider wrapper needed - state is globally accessible
- **Selective Subscriptions**: Components automatically subscribe only to the state they actually use
- **Optimized Updates**: Fine-grained reactivity ensures optimal performance even with complex state
### Performance Comparison
```tsx
// ❌ With React Context - entire context re-renders
const { user, isLoading, error, config, methods } = useGoIam(); // Re-renders on ANY change
// ✅ With Hookstate - only re-renders when `user` changes
const { user } = useGoIam(); // Only re-renders when user changes
// ✅ Multiple selective subscriptions
const UserName = () => {
const { user } = useGoIam(); // Only subscribes to user
return <span>{user?.name}</span>;
};
const LoadingSpinner = () => {
const { loadingMe } = useGoIam(); // Only subscribes to loadingMe
return loadingMe ? <Spinner /> : null;
};
```
## Upgrade Guide
### From React Context Version
If you're upgrading from a previous React Context-based version:
**Before (Context):**
```tsx
// Old provider setup
<GoIamProvider config={config}>
<App />
</GoIamProvider>;
// Old hook usage
const { isAuthenticated, isLoading } = useGoIam();
```
**After (Hookstate):**
```tsx
// No provider needed!
<App />;
// New hook usage with different property names
const { user, loadedState } = useGoIam();
const isAuthenticated = !!user;
const isLoading = !loadedState;
```
**Key Changes:**
- Remove `GoIamProvider` wrapper
- `isAuthenticated` → check `!!user`
- `isLoading` → check `!loadedState`
- `refreshUser()` → `dashboardMe()` or `me()`
- `getAccessToken()` → `verify()`
- `requiredRoles` → `hasRequiredResources()`
## Development
### Building
```bash
npm run build
```
### Testing
```bash
npm test
npm run test:coverage
```
### Linting
```bash
npm run lint
npm run lint:fix
```
## Troubleshooting
### Common Issues
**Q: User data not persisting across browser sessions**
A: The SDK automatically saves user data to localStorage. Ensure your app domain allows localStorage access.
**Q: Authentication redirects not working**
A: Verify your `callbackPageUrl` matches the redirect URI configured in your GoIAM server.
**Q: Resource checks always return false**
A: Ensure you've called `dashboardMe()` to load user data with resources after authentication.
**Q: App stuck in loading state**
A: Check browser console for errors and ensure `setBaseUrl()` and `setClientId()` are called before `dashboardMe()`.
### Debug Mode
Enable debug logging in development:
```tsx
// The SDK logs debug information to console.debug()
// Open browser DevTools and enable "Verbose" log level to see:
// - API call timing
// - Cache hit/miss decisions
// - User data updates
// - Authentication flow steps
```
## Contributing
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## Support
- **Documentation**: [GitHub Repository](https://github.com/melvinodsa/go-iam-sdk)
- **Issues**: [GitHub Issues](https://github.com/melvinodsa/go-iam-sdk/issues)
- **Email**: melvinodsa .com
## Changelog
### 0.3.0 (Latest)
- **BREAKING**: Migrated from React Context to Hookstate for better performance
- **BREAKING**: Removed `GoIamProvider` - no provider wrapper needed
- **BREAKING**: API changes: `isAuthenticated` → `!!user`, `isLoading` → `!loadedState`
- **NEW**: Smart caching with 5-minute API call throttling
- **NEW**: Resource-based access control with `hasRequiredResources()`
- **NEW**: PKCE OAuth2 flow support
- **NEW**: Comprehensive TypeScript types
- **IMPROVED**: Minimal re-renders with selective state subscriptions
- **IMPROVED**: Better error handling and debug logging
- **IMPROVED**: Automatic localStorage persistence and sync
### 0.2.0
- Added AuthGuard component
- Enhanced TypeScript support
- Improved error handling
### 0.1.0
- Initial release
- Basic authentication flow
- Context provider and hook
- TypeScript support