@asaidimu/iam
v6.0.0
Published
A lightweight, type-safe Identity and Access Management (IAM) system for client-side JavaScript/TypeScript applications.
Downloads
44
Maintainers
Readme
@asaidimu/iam - Identity & Access Management Toolkit
A comprehensive, extensible library for building robust identity and access management (IAM) into JavaScript/TypeScript applications, featuring advanced session management and first-class React integration.
🚀 Quick Links
- Overview & Features
- Installation & Setup
- Usage Documentation
- Project Architecture
- Development & Contributing
- Additional Information
✨ Overview & Features
@asaidimu/iam provides a powerful and flexible foundation for managing user identities and controlling access to resources within your application. Designed with modern web applications in mind, it separates concerns into distinct, extensible modules for identity, access control, and seamless React integration. This library empowers developers to implement complex authorization logic, manage secure user sessions, and reactively update UI based on permissions, all while maintaining a high degree of type safety and performance.
This library offers the tools to define granular permissions, enforce security policies, and deliver a robust user experience. Its modular design allows you to use only the parts you need, while its React hooks and components dramatically simplify integrating IAM into your frontend.
Key Features
- Flexible Identity Management:
IdentityProviderabstraction for integrating with any authentication backend (OAuth, JWT, custom API). - Robust Session Management: Built-in session TTL, automatic refresh, and pluggable persistence (e.g.,
localStorage,IndexedDB). - Advanced Security Features: Optional idle timeout, warning notifications, window blur/minimize lock, and activity tracking for high-security environments.
- Rule-Based Access Control: Define granular permissions using simple functions or complex
CompositeRulestructures with logicalAND/OR/NOT/XOR/NAND/NORoperators. - High-Performance Rule Evaluation: Memoized rule evaluator with configurable cache TTL for efficient permission checks.
- Comprehensive React Integration: Dedicated
IAMProvider, hooks (useIdentity,usePermissions,useCan,useEvaluate), Higher-Order Components (withPermission), and conditional rendering components (PermissionGate,RuleGate). - Type-Safe API: Fully written in TypeScript, providing excellent developer experience and compile-time safety.
- Extensible Design: Easily swap out persistence layers, integrate with different identity sources, and define custom rule logic.
📦 Installation & Setup
This library is available as an npm package.
Prerequisites
- Node.js (LTS version recommended)
- npm or yarn
Installation Steps
Install the package using your preferred package manager:
npm install @asaidimu/iam
# OR
yarn add @asaidimu/iamThe package includes TypeScript definitions by default.
Configuration
There are no global configuration files. All configurations are passed directly to the createAuthenticator and createAccessController functions, or via the IAMProvider component in React.
Verification
You can verify the installation by attempting to import a module in your project:
// For TypeScript projects
import { createAuthenticator, createAccessController } from '@asaidimu/iam';
// For JavaScript projects
const { createAuthenticator, createAccessController } = require('@asaidimu/iam');
console.log('IAM Toolkit imported successfully!');📖 Usage Documentation
Core Concepts
At the heart of @asaidimu/iam are a few key abstractions:
Identity: Represents an authenticated user, containingpermissions(array of strings) andproperties(generic object for user-specific data).IdentityProvider: Manages the authentication state, session lifecycle, and provides the currentIdentity. It handles login, logout, refresh, persistence, and notifies listeners of state changes.IAMRule: A function that takes anIAMRuleContext(containingidentityproperties and aresource) and returns abooleanindicating access.CompositeRule: Allows combining multipleIAMRules or otherCompositeRules using logical operators (AND,OR,NOT, etc.).IAMRuleSet: AMapthat associates permission names (strings) with their correspondingIAMRuleorCompositeRule.AccessController: EvaluatesIAMRules from anIAMRuleSetagainst the currentIdentity(from anIdentityProvider) and a givenresource.DataStorePersistence: An interface for plugging in various storage mechanisms to persist the identity and session state (e.g.,localStorage, cookies,IndexedDB).
Custom Persistence Adapter
Before setting up your IdentityProvider, you might want to create a persistence adapter. This allows the IAM system to save and load the session state across browser sessions or tabs.
Here's an example using localStorage:
import { DataStorePersistence, Identity } from '@asaidimu/iam';
// Define the shape of your identity properties
interface UserProps {
id: string;
name: string;
roles: string[];
}
// PersistedIdentity type is internal but good to know for context
interface PersistedIdentity<T> extends Identity<T, string> {
sessionExpires?: number;
lastActivity?: number;
}
const localStoragePersistence: DataStorePersistence<PersistedIdentity<UserProps>> = {
set(id: string, state: PersistedIdentity<UserProps>): boolean {
try {
localStorage.setItem(`iam_identity_${id}`, JSON.stringify(state));
return true;
} catch (error) {
console.error('Error saving identity to localStorage:', error);
return false;
}
},
get(): PersistedIdentity<UserProps> | null {
try {
const stored = localStorage.getItem(`iam_identity_identity`); // Default instanceId is 'identity'
return stored ? JSON.parse(stored) : null;
} catch (error) {
console.error('Error loading identity from localStorage:', error);
return null;
}
},
subscribe(id: string, callback: (state: PersistedIdentity<UserProps>) => void): () => void {
const handler = (event: StorageEvent) => {
if (event.key === `iam_identity_${id}`) {
const newState = event.newValue ? JSON.parse(event.newValue) : null;
callback(newState);
}
};
window.addEventListener('storage', handler);
return () => window.removeEventListener('storage', handler);
},
clear(): boolean {
try {
localStorage.removeItem(`iam_identity_identity`);
return true;
} catch (error) {
console.error('Error clearing identity from localStorage:', error);
return false;
}
},
};Setting up an Identity Provider
The createAuthenticator function is your primary way to manage user sessions. It takes handler functions for authentication, deauthentication, and optional session/security configurations.
import { createAuthenticator, type IdentityProvider } from '@asaidimu/iam';
// Assuming localStoragePersistence is defined as above
import { localStoragePersistence } from './persistence';
interface UserProps {
id: string;
name: string;
email: string;
roles: string[];
}
// Authentication parameters can be anything, here we use username and password
type AuthParams = [string, string];
const myIdentityProvider: IdentityProvider<UserProps> = createAuthenticator<UserProps, AuthParams>({
onAuthenticate: async (username, password) => {
// Simulate an API call to your backend
console.log(`Attempting to authenticate ${username}...`);
await new Promise(resolve => setTimeout(resolve, 500)); // Simulate network delay
if (username === 'admin' && password === 'adminpass') {
return {
permissions: ['dashboard:view', 'users:manage', 'posts:create', 'posts:edit', 'posts:publish'],
properties: { id: 'user-123', name: 'Admin User', email: '[email protected]', roles: ['admin', 'editor'] },
};
} else if (username === 'editor' && password === 'editorpass') {
return {
permissions: ['dashboard:view', 'posts:create', 'posts:edit'],
properties: { id: 'user-456', name: 'Editor User', email: '[email protected]', roles: ['editor'] },
};
}
return null; // Authentication failed
},
onDeauthenticate: async (identityProps) => {
// Simulate API call to log out on the backend, clear cookies/tokens
console.log(`Deauthenticating user: ${identityProps.name}`);
await new Promise(resolve => setTimeout(resolve, 300));
return true;
},
onRefresh: async (currentIdentity) => {
// Simulate refreshing the session (e.g., using a refresh token)
console.log(`Refreshing session for ${currentIdentity.properties.name}...`);
await new Promise(resolve => setTimeout(resolve, 200));
// In a real app, you'd make an API call here.
// If successful, return updated identity, otherwise return null to trigger re-auth.
return { ...currentIdentity, permissions: [...currentIdentity.permissions, 'refreshed:permission'] };
},
// Optional: Add persistence to save session across browser sessions/tabs
persistence: localStoragePersistence,
session: {
ttl: 60 * 60 * 1000, // Session expires in 1 hour
refreshInterval: 10 * 60 * 1000, // Attempt refresh 10 minutes before TTL
},
security: {
enabled: true,
maxIdleTime: 10 * 60 * 1000, // Auto-logout after 10 minutes of inactivity
warningTime: 30 * 1000, // Show warning 30 seconds before logout
lockOnWindowBlur: true, // Log out immediately if window loses focus
onIdleWarning: (timeRemaining) => console.warn(`Idle warning! Session expiring in ${timeRemaining / 1000}s.`),
onIdleTimeout: () => console.error('Session expired due to inactivity!'),
onSessionLocked: () => console.warn('Session locked due to window blur.'),
onSecurityEvent: (event, details) => console.log(`Security event: ${event}`, details),
},
});
// Example usage of IdentityProvider
async function runAuthDemo() {
await myIdentityProvider.authenticate('admin', 'adminpass');
console.log('Current identity after login:', myIdentityProvider.identity()?.properties.name);
// You can subscribe to changes
const unsubscribe = myIdentityProvider.onChange(identityProps => {
console.log('Identity changed:', identityProps ? identityProps.name : 'No user');
});
// Wait for a bit, simulate activity
await new Promise(resolve => setTimeout(resolve, 2000));
myIdentityProvider.resetIdleTimer?.(); // Reset idle timer if security is enabled
await myIdentityProvider.deauthenticate();
console.log('Current identity after logout:', myIdentityProvider.identity());
unsubscribe(); // Don't forget to unsubscribe
myIdentityProvider.destroy(); // Clean up listeners and timers when no longer needed
}
// runAuthDemo(); // Uncomment to run this demo in a non-React contextDefining Access Rules
Rules are the core of your authorization logic. They can be simple functions or complex CompositeRule objects.
import { IAMRuleSet, CompositeRule, type IAMRuleContext } from '@asaidimu/iam';
interface UserProps {
id: string;
name: string;
email: string;
roles: string[];
}
interface ResourceProps {
id: string;
ownerId: string;
status: 'draft' | 'published';
visibility: 'public' | 'private';
}
const myAccessRules: IAMRuleSet<UserProps, ResourceProps> = new Map();
// Simple rule: Only admin users can view the dashboard
myAccessRules.set('dashboard:view', ({ identity }) => {
return !!identity && identity.roles.includes('admin');
});
// Simple rule: Only admins or editors can create posts
myAccessRules.set('posts:create', ({ identity }) => {
return !!identity && (identity.roles.includes('admin') || identity.roles.includes('editor'));
});
// Rule with resource context: User can edit their own posts
myAccessRules.set('posts:edit', ({ identity, resource }) => {
if (!identity || !resource) return false;
return identity.id === resource.ownerId || identity.roles.includes('admin');
});
// Composite Rule: Admin can publish any post, OR editor can publish their own draft post
myAccessRules.set('posts:publish', {
operator: 'OR',
rules: [
// Condition 1: User is admin
({ identity }) => !!identity && identity.roles.includes('admin'),
// Condition 2: User is editor AND owns the resource AND resource is a draft
{
operator: 'AND',
rules: [
({ identity }) => !!identity && identity.roles.includes('editor'),
({ identity, resource }) => !!identity && !!resource && identity.id === resource.ownerId,
({ resource }) => !!resource && resource.status === 'draft',
],
},
],
});
// Another composite rule: Users can view private resources if they are admin OR owner
myAccessRules.set('resource:view-private', {
operator: 'OR',
rules: [
({ identity }) => !!identity && identity.roles.includes('admin'),
({ identity, resource }) => !!identity && !!resource && identity.id === resource.ownerId,
]
});Using the Access Controller (Standalone)
You can use the access controller directly without React if you're building a backend, a CLI, or a vanilla JavaScript application.
import { createAccessController } from '@asaidimu/iam';
// Assuming myIdentityProvider and myAccessRules are defined as above
import { myIdentityProvider } from './identitySetup';
import { myAccessRules } from './accessRules';
// Create an access controller instance
const accessController = createAccessController(myIdentityProvider, myAccessRules, {
defaultAllow: false, // If no rule is found for a permission, deny access by default
cacheTTL: 5000, // Cache rule evaluation results for 5 seconds
});
// Example resource
const samplePost = { id: 'post-abc', ownerId: 'user-456', status: 'draft', visibility: 'private' };
const anotherPost = { id: 'post-xyz', ownerId: 'user-789', status: 'published', visibility: 'public' };
async function runAccessDemo() {
// Simulate login
await myIdentityProvider.authenticate('editor', 'editorpass');
const currentUser = myIdentityProvider.identity();
console.log(`\n--- Access checks for ${currentUser?.properties.name} ---`);
// 1. `has(permission)`: Checks if the identity's `permissions` array *contains* the permission.
// Useful for simple permission checks where the rule logic is implicitly handled by the identity's granted permissions.
// If a rule exists, it's evaluated, but only if the identity already `has` the permission in its list.
console.log('Can view dashboard (has):', accessController.has('dashboard:view')); // True (editor has this)
console.log('Can manage users (has):', accessController.has('users:manage')); // False (editor does NOT have this)
// 2. `is(permission)`: Checks if the *rule* for the permission evaluates to true.
// Ignores the `identity.permissions` array. Useful if permissions are purely rule-based.
console.log('Can view dashboard (is):', accessController.is('dashboard:view')); // True (rule says editors can)
console.log('Can manage users (is):', accessController.is('users:manage')); // False (rule says only admins)
// 3. `can(permission, resource)`: Checks if the identity's `permissions` array *contains* the permission AND
// if the rule for the permission evaluates to true, potentially with resource context.
// This is often the most common method for resource-based authorization.
console.log('Can create posts (can):', accessController.can('posts:create')); // True (editor has permission + rule allows)
console.log('Can edit own post (can):', accessController.can('posts:edit', samplePost)); // True (editor owns samplePost)
console.log('Can edit other\'s post (can):', accessController.can('posts:edit', anotherPost)); // False (editor doesn't own anotherPost)
console.log('Can publish own draft post (can):', accessController.can('posts:publish', samplePost)); // True (editor owns draft samplePost)
console.log('Can publish published post (can):', accessController.can('posts:publish', { ...samplePost, status: 'published' })); // False (rule requires draft)
// 4. `evaluate(rule, resource)`: Directly evaluates a custom rule or CompositeRule, bypassing the permission map.
// Useful for one-off, dynamic rule evaluations not tied to a predefined permission name.
const customRule = ({ identity, resource }: IAMRuleContext<UserProps, ResourceProps>) =>
!!identity && identity.email.endsWith('@example.com') && resource?.visibility === 'public';
console.log('Can view public resource with custom rule:', accessController.evaluate(customRule, anotherPost)); // True
console.log('Can view private resource with custom rule:', accessController.evaluate(customRule, samplePost)); // False
await myIdentityProvider.deauthenticate();
}
// runAccessDemo(); // Uncomment to run this demoReact Integration
The React module (src/react.tsx) provides an IAMProvider component and a suite of hooks and components for seamless integration into your React application.
// App.tsx
import React, { useState, useEffect } from 'react';
import {
IAMProvider,
useIdentity,
usePermissions,
useCan,
useEvaluate,
PermissionGate,
RuleGate,
useAccessController,
useIdentityProvider,
useStableSetup,
} from '@asaidimu/iam';
// Assume myIdentityProvider and myAccessRules are imported from their setup files
import { myIdentityProvider } from './identitySetup';
import { myAccessRules } from './accessRules';
// Define the shape of your identity and resource properties
interface UserProps {
id: string;
name: string;
email: string;
roles: string[];
}
interface ResourceProps {
id: string;
ownerId: string;
status: 'draft' | 'published';
visibility: 'public' | 'private';
}
function App() {
// Use useStableSetup to ensure the setup function passed to IAMProvider is stable
// This prevents unnecessary re-initialization of the provider/controller.
const stableIAMSetup = useStableSetup(async () => {
// In a real app, you might fetch initial identity/rules from an API here
await new Promise(resolve => setTimeout(resolve, 100)); // Simulate async setup
return {
provider: myIdentityProvider,
rules: myAccessRules,
};
});
const customErrorComponent = (error: Error, retry: () => void) => (
<div style={{ color: 'red' }}>
<h2>Failed to load IAM</h2>
<p>{error.message}</p>
<button onClick={retry}>Try Again</button>
</div>
);
return (
<IAMProvider
setup={stableIAMSetup}
loadingComponent={<div style={{ padding: '20px', textAlign: 'center' }}>Loading identity and access control...</div>}
errorComponent={customErrorComponent}
onRetry={() => console.log('Retrying IAM setup...')}
>
<div style={{ fontFamily: 'Arial, sans-serif', maxWidth: '800px', margin: '20px auto', padding: '20px', border: '1px solid #eee', borderRadius: '8px', boxShadow: '0 2px 4px rgba(0,0,0,0.1)' }}>
<h1>IAM Demo Application</h1>
<AuthenticationSection />
<DashboardContent />
</div>
</IAMProvider>
);
}
function AuthenticationSection() {
const user = useIdentity<UserProps>();
const { authenticate, deauthenticate } = useIdentityProvider();
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [authError, setAuthError] = useState<string | null>(null);
const handleLogin = async () => {
setAuthError(null);
try {
const success = await authenticate(username, password);
if (!success) {
setAuthError('Invalid credentials');
}
} catch (e: any) {
setAuthError(e.message || 'Login failed');
}
};
if (user) {
return (
<div style={{ marginBottom: '20px', padding: '15px', border: '1px solid #ccc', borderRadius: '4px', backgroundColor: '#e8f5e9' }}>
<p>Logged in as: <strong>{user.name}</strong> ({user.roles.join(', ')})</p>
<button onClick={() => deauthenticate()}>Logout</button>
</div>
);
}
return (
<div style={{ marginBottom: '20px', padding: '15px', border: '1px solid #ccc', borderRadius: '4px', backgroundColor: '#ffe0b2' }}>
<h2>Login</h2>
<input
type="text"
placeholder="Username (e.g., admin, editor)"
value={username}
onChange={(e) => setUsername(e.target.value)}
style={{ marginRight: '10px', padding: '8px' }}
/>
<input
type="password"
placeholder="Password (e.g., adminpass, editorpass)"
value={password}
onChange={(e) => setPassword(e.target.value)}
style={{ marginRight: '10px', padding: '8px' }}
/>
<button onClick={handleLogin}>Login</button>
{authError && <p style={{ color: 'red', marginTop: '10px' }}>{authError}</p>}
</div>
);
}
function DashboardContent() {
const user = useIdentity<UserProps>(); // Get current user properties
const canViewDashboard = usePermissions('dashboard:view'); // Check simple permission
const canManageUsers = usePermissions('users:manage'); // Check another simple permission
const canCreatePosts = useCan('posts:create'); // Check permission (no resource needed for this rule)
// Example resource to check against
const myPost: ResourceProps = { id: 'post-1', ownerId: user?.id || 'unknown', status: 'draft', visibility: 'private' };
const othersPost: ResourceProps = { id: 'post-2', ownerId: 'some-other-id', status: 'published', visibility: 'public' };
const canEditMyPost = useCan('posts:edit', myPost); // Check permission on a specific resource
const canPublishMyDraftPost = useCan('posts:publish', myPost);
const canPublishOthersPost = useCan('posts:publish', othersPost);
const canViewOthersPrivatePost = useEvaluate<UserProps, ResourceProps>(
({ identity, resource }) => {
// Custom inline rule: can view private if admin or owner
if (!identity || !resource) return false;
return resource.visibility === 'private' && (identity.roles.includes('admin') || identity.id === resource.ownerId);
},
othersPost
);
const { resetIdleTimer } = useIdentityProvider();
useEffect(() => {
if (user && resetIdleTimer) {
// Simulate user activity to reset the idle timer
const interval = setInterval(() => {
console.log('User active, resetting idle timer...');
resetIdleTimer();
}, 60 * 1000); // Reset every minute
return () => clearInterval(interval);
}
}, [user, resetIdleTimer]);
return (
<div style={{ borderTop: '1px solid #eee', paddingTop: '20px', marginTop: '20px' }}>
<h2>Application Content</h2>
<p>Current User ID: {user?.id || 'N/A'}</p>
<p>Can View Dashboard: <strong>{canViewDashboard ? 'Yes' : 'No'}</strong></p>
<p>Can Manage Users: <strong>{canManageUsers ? 'Yes' : 'No'}</strong></p>
<p>Can Create Posts: <strong>{canCreatePosts ? 'Yes' : 'No'}</strong></p>
<p>Can Edit My Post (ID: {myPost.id}): <strong>{canEditMyPost ? 'Yes' : 'No'}</strong></p>
<p>Can Publish My Draft Post (ID: {myPost.id}): <strong>{canPublishMyDraftPost ? 'Yes' : 'No'}</strong></p>
<p>Can Publish Others Post (ID: {othersPost.id}): <strong>{canPublishOthersPost ? 'Yes' : 'No'}</strong></p>
<p>Can View Others Private Post (Custom Rule): <strong>{canViewOthersPrivatePost ? 'Yes' : 'No'}</strong></p>
<h3>Conditional Rendering Examples</h3>
<PermissionGate permission="dashboard:view" fallback={<p style={{ color: '#d32f2f' }}>Access Denied: You need 'dashboard:view' permission to see the admin section.</p>}>
<div style={{ border: '1px dashed #4caf50', padding: '10px', marginTop: '10px', backgroundColor: '#e8f5e9' }}>
<h4>Admin Dashboard Section</h4>
<p>This content is only visible to users with the 'dashboard:view' permission.</p>
{canManageUsers && <button>Manage Users</button>}
{!canManageUsers && <p style={{ opacity: 0.6 }}>You cannot manage users.</p>}
</div>
</PermissionGate>
<RuleGate rule={myAccessRules.get('posts:publish')!} resource={myPost} fallback={<p style={{ color: '#d32f2f' }}>Access Denied: You cannot publish this specific post.</p>}>
<div style={{ border: '1px dashed #2196f3', padding: '10px', marginTop: '10px', backgroundColor: '#e3f2fd' }}>
<h4>Publish Post Section</h4>
<p>This content is visible if the `posts:publish` rule passes for your specific post ({myPost.id}).</p>
<button>Publish My Draft Post</button>
</div>
</RuleGate>
<RuleGate
rule={{
operator: 'AND',
rules: [
({ identity }) => !!identity && identity.roles.includes('editor'),
({ resource }) => !!resource && resource.status === 'draft',
],
}}
resource={myPost}
fallback={<p style={{ color: '#d32f2f' }}>Access Denied: You need to be an editor and the post must be a draft to see this rule-based content.</p>}
>
<div style={{ border: '1px dashed #ff9800', padding: '10px', marginTop: '10px', backgroundColor: '#fff3e0' }}>
<h4>Editor Draft Tools</h4>
<p>This content is visible only if you are an editor AND the current post (`myPost`) is a draft.</p>
<button>Edit Draft</button>
</div>
</RuleGate>
</div>
);
}
export default App;🏗️ Project Architecture
@asaidimu/iam is designed with a clear separation of concerns, making it modular, testable, and extensible.
Identity Module (
identity.ts):- Purpose: Manages user authentication state, session lifecycle, and optional security features.
- Components:
createAuthenticator(the main factory function),IdentityProviderinterface,SecurityConfig. - Functionality: Handles login (
onAuthenticate), logout (onDeauthenticate), session refreshing (onRefresh), identity verification (onVerify), change notifications (onChange), persistence integration, and security features like idle timeouts, window blur locking, and activity tracking.
Access Module (
access.ts):- Purpose: Provides the core logic for evaluating access rules against the current identity and resources.
- Components:
createAccessController(factory function),AccessControllertype,createMemoizedEvaluator. - Functionality: Offers methods like
has,is,can, andevaluatefor checking permissions. It utilizes memoization for performance and supports both simple and composite rule structures.
React Integration Module (
react.tsx):- Purpose: Offers a user-friendly way to integrate IAM into React applications.
- Components:
IAMProvider(context provider),useIdentity,usePermissions,useCan,useEvaluate,useAccessController,useIdentityProvider(hooks),PermissionGate,RuleGate(components),withPermission(HOC). - Functionality: Provides a declarative way to wrap your application with IAM context, and reactive hooks/components to consume identity and access control data. Includes utilities for stable setup functions and robust loading/error handling.
Types & Interfaces (
types.ts):- Purpose: Defines the foundational data structures and contracts used across all modules.
- Components:
Identity,IAMRuleContext,IAMRule,IAMRuleSet,IAMBooleanOperator,CompositeRule,DataStorePersistence. - Functionality: Ensures type safety, clarity, and consistency throughout the library, making it easier to understand, use, and extend.
Error Handling (
error.ts):- Purpose: Provides a standardized custom error class for IAM-specific failures.
- Components:
IAMError. - Functionality: Allows for structured error reporting with specific codes and optional context, aiding in debugging and user feedback.
Data Flow: The IAMProvider (or createAuthenticator directly) establishes an IdentityProvider which manages the current user's Identity. This IdentityProvider is then passed to createAccessController along with a IAMRuleSet to create an AccessController. In React applications, the IAMContext makes both the IdentityProvider and AccessController available to child components via hooks like useIdentity and usePermissions, which react to changes in the identity provider's state.
Extension Points:
- Custom
IdentityProviderLogic: OverrideonAuthenticate,onDeauthenticate,onRefresh,onVerify,onAuthChangecallbacks increateAuthenticator. - Custom
DataStorePersistence: Implement theDataStorePersistenceinterface to integrate with any storage mechanism (e.g., cookies, server-side sessions, custom browser storage). - Custom
IAMRules: Define any arbitrary function logic for your authorization rules, including asynchronous checks if needed (thoughIAMRuleis synchronous by default; for async, you'd typically pre-fetch data or use another mechanism). - Security Event Handlers: Hook into
onIdleWarning,onIdleTimeout,onSessionLocked,onWindowBlur,onSecurityEventfor custom notifications or actions in response to security events.
🛠️ Development & Contributing
Contributions are welcome! If you'd like to contribute, please follow these guidelines.
Development Setup
- Clone the repository:
git clone https://github.com/asaidimu/iam.git # Replace with actual repo URL cd iam - Install dependencies:
npm install # OR yarn install - Build the project:
npm run build
Scripts
npm run build: Compiles the TypeScript source code into JavaScript.npm test: Runs the test suite (e.g., Jest).npm run lint: Lints the codebase (e.g., ESLint).npm run format: Formats the codebase (e.g., Prettier).
Testing
Tests are crucial for maintaining the quality and stability of the library.
- To run all tests:
npm test - Ensure high test coverage for any new features or bug fixes.
Contributing Guidelines
- Fork the repository.
- Create a new branch for your feature or bug fix:
git checkout -b feature/your-feature-nameorgit checkout -b bugfix/issue-description. - Make your changes, ensuring they adhere to the project's coding standards (linting and formatting checks).
- Write tests for your changes.
- Ensure all tests pass:
npm test. - Commit your changes with clear and descriptive commit messages.
- Push your branch to your forked repository.
- Open a Pull Request against the
mainbranch of the original repository.- Provide a clear description of your changes.
- Reference any related issues.
Issue Reporting
If you find a bug or have a feature request, please open an issue on the GitHub Issues page.
📚 Additional Information
Troubleshooting
useIAMContext must be used within an IAMProvider:- Cause: You are attempting to use one of the IAM React hooks (e.g.,
useIdentity,usePermissions) outside of anIAMProvidercomponent's tree. - Solution: Ensure that the component where you're using the hook is a child of
IAMProvider. Typically, you'd wrap your entire application or the relevant part with<IAMProvider />.
- Cause: You are attempting to use one of the IAM React hooks (e.g.,
Permissions or Identity not updating in React components:
- Cause: The
IdentityProvider'sonChangemechanism might not be properly implemented or the component isn't reacting correctly. Ensure yourcreateAuthenticatorsetup correctly callsnotifyListeners()when the identity state changes. - Solution: Verify your
IdentityProvider'sonChangecallback is correctly subscribed and publishing updates. Check if youronAuthenticate,onDeauthenticate,onRefreshfunctions are correctly callingupdateIdentity.
- Cause: The
createAuthenticatoroptions not working as expected (e.g., persistence not saving):- Cause: Incorrect implementation of the
DataStorePersistenceadapter or issues with the chosen storage mechanism. - Solution: Double-check your
DataStorePersistenceimplementation (especiallysetandgetmethods). EnsureinstanceIdis consistent if you have multiple IAM instances. ForlocalStorage, ensure browser storage isn't blocked or full.
- Cause: Incorrect implementation of the
Rules are not evaluated or always return
defaultAllow:- Cause: The permission string passed to
accessController.has(),is(), orcan()does not match any key in yourIAMRuleSet. - Solution: Ensure the permission string precisely matches a key in the
Mapyou passed tocreateAccessController.
- Cause: The permission string passed to
FAQ
Why do I need
useStableSetupforIAMProvider? TheIAMProvidertakes asetupfunction that returns a Promise. If thissetupfunction is recreated on every render (which happens if it's defined directly inside your functional component), React might continuously re-run the setup process.useStableSetupmemoizes this function, ensuring it remains the same across renders unless its dependencies explicitly change, preventing unnecessary re-initializations of yourIdentityProviderandAccessController.Can I implement my own
IdentityProviderfrom scratch? Yes! TheIdentityProvideris an interface. WhilecreateAuthenticatorprovides a robust, pre-built solution with many features, you can implement theIdentityProviderinterface yourself if you have highly custom needs thatcreateAuthenticatorcannot accommodate.How do
has,is, andcandiffer?has(permission): Checks if the identity'spermissionsarray includes the given permission string. If a rule exists for this permission, it will also be evaluated, buthasimplies an explicit permission granted to the identity.is(permission): Evaluates the rule associated with the given permission string, completely ignoring theidentity.permissionsarray. This is for purely rule-based authorization.can(permission, resource): This is the most common for resource-based checks. It acts likehas(requires the permission inidentity.permissions) and evaluates the associated rule, providing theresourcecontext to the rule.
How can I handle Server-Side Rendering (SSR) with
@asaidimu/iam? The React hooks utilizeuseSyncExternalStorewhich supports SSR by providing agetServerSnapshotfunction. By default,useIdentity,usePermissions, etc., will returnnull(orfalse) on the server, as a real authenticated identity typically doesn't exist in that context unless explicitly hydrated. You would need to hydrate theIdentityProviderstate on the server (e.g., from request headers/cookies) before passing it toIAMProviderorcreateAccessController.
Changelog
- See CHANGELOG.md for a history of changes. (Placeholder - create this file in your project)
License
This project is licensed under the MIT License - see the LICENSE file for details. (Placeholder - create this file in your project)
Acknowledgments
- Inspired by robust authentication and authorization patterns found in various frameworks and libraries.
- Built with TypeScript and React.
