npm package: @mpofusindie/iam-react
Description: React hooks and guard components for IAM service
Latest version: 1.3.1

# @mpofusindie/iam-react

React hooks and guard components for the IAM (Identity & Access Management) service. Provides declarative, permission-based UI rendering with OAuth2/OIDC authentication, two-factor authentication, password reset, and session management.

## Install

```bash
npm install @mpofusindie/iam-react
```

Peer dependencies: `react >= 18.0.0`, `react-dom >= 18.0.0`, `react-router-dom >= 6.0.0`

## Quick Start

### 1. Wrap your app with IAMProvider

```tsx
import { IAMProvider } from "@mpofusindie/iam-react";

function App() {
  return (
    <IAMProvider
      config={{
        apiUrl: "/api/iam/v1",
        token: jwtToken,
        tenantId: "optional-tenant-uuid",     // for header-based multi-tenancy
        refreshMode: "body",                   // "body" | "cookie" | "both"
      }}
      loadingFallback={<Spinner />}
    >
      <YourApp />
    </IAMProvider>
  );
}
```

### 2. Guard your routes

```tsx
import { ProtectedRoute } from "@mpofusindie/iam-react";

<Route path="/loans" element={
  <ProtectedRoute resource="loans">
    <LoansPage />
  </ProtectedRoute>
} />
```

### 3. Guard features, actions, and fields

```tsx
import { FeatureGuard, ActionGuard, FieldGuard } from "@mpofusindie/iam-react";

{/* Show nav item only if user has access to loans */}
<FeatureGuard resource="loans">
  <NavLink to="/loans">Loans</NavLink>
</FeatureGuard>

{/* Show button only if user can create loans */}
<ActionGuard resource="loans" action="create">
  <button>Create Loan</button>
</ActionGuard>

{/* Show button but disabled if no permission */}
<ActionGuard resource="loans" action="approve" mode="disable">
  <button>Approve</button>
</ActionGuard>

{/* Mask sensitive fields */}
<FieldGuard resource="loans" field="ssn" maskChar="***-**-****">
  {loan.ssn}
</FieldGuard>
```

### 4. OAuth2 Login

```tsx
import { useOAuth2, OAuth2LoginButton, OAuth2Callback } from "@mpofusindie/iam-react";

{/* Show provider buttons */}
<OAuth2LoginButton providerKey="google" label="Sign in with Google" />

{/* Handle callback */}
<Route path="/auth/callback" element={
  <OAuth2Callback onSuccess={handleTokens} onError={handleError} />
} />
```

### 5. Two-Factor Authentication

```tsx
import { useTwoFactor, TwoFactorSetup, TwoFactorVerify } from "@mpofusindie/iam-react";

function TwoFactorPage() {
  const { setup, confirmSetup, verify, disable } = useTwoFactor();

  // Setup: generates QR code + backup codes
  const setupData = await setup();

  return (
    <TwoFactorSetup
      setupData={setupData}
      onConfirm={(code) => confirmSetup(code)}
    />
  );
}

// During login challenge (requires2fa === true):
<TwoFactorVerify
  onSubmit={(code) => verify(challengeToken, code)}
  onVerified={(tokenPair) => handleLogin(tokenPair)}
/>
```

### 6. Password Reset

```tsx
import { usePasswordReset, PasswordResetForm } from "@mpofusindie/iam-react";

const { forgotPassword, resetPassword } = usePasswordReset();

<PasswordResetForm
  onForgotPassword={(email) => forgotPassword(email)}
  onResetPassword={(token, password) => resetPassword(token, password)}
  onResetComplete={() => navigate("/login")}
/>
```

### 7. Session Management

```tsx
import { useSessions } from "@mpofusindie/iam-react";

const { revokeUserSessions, revokeTenantSessions } = useSessions();

// Revoke all sessions for a specific user
await revokeUserSessions(userId);

// Emergency: revoke all sessions for the entire tenant
await revokeTenantSessions();
```

### 8. Programmatic Permission Checks

```tsx
import { usePermissions } from "@mpofusindie/iam-react";

function LoansTable() {
  const { canDo, isFieldVisible, hasRole } = usePermissions();

  const columns = baseColumns.filter(
    col => !col.field || isFieldVisible("loans", col.field)
  );

  return (
    <div>
      {canDo("loans", "export") && <ExportButton />}
      <Table columns={columns} data={loans} />
    </div>
  );
}
```

### 9. Admin Management API

```tsx
import { useIAMAdmin } from "@mpofusindie/iam-react";

const admin = useIAMAdmin();

// CRUD operations
const users = await admin.users.list({ page: 0, size: 20 });
const role = await admin.roles.create({ name: "MANAGER", description: "..." });

// OAuth2 providers
const providers = await admin.oauth2Providers.list();
await admin.oauth2Providers.create({ providerKey: "google", ... });

// Audit logs
const logs = await admin.auditLogs.list({ entityType: "USER", from: "2026-01-01" });
const csvBlob = await admin.auditLogs.export("csv", { entityType: "USER" });

// Session management
await admin.sessions.revokeUser(userId);
await admin.sessions.revokeTenant();
```

## API Reference

### Components

| Component | Props | Description |
|-----------|-------|-------------|
| `IAMProvider` | `config`, `loadingFallback?`, `onError?` | Permission context provider |
| `FeatureGuard` | `resource`, `fallback?` | Show children if resource access granted |
| `ActionGuard` | `resource`, `action`, `mode?`, `fallback?` | Show/disable based on action permission |
| `FieldGuard` | `resource`, `field`, `fallback?`, `maskChar?` | Show/mask based on field visibility |
| `ProtectedRoute` | `resource`, `redirectTo?` | Redirect if access denied |
| `OAuth2LoginButton` | `providerKey`, `label?`, `icon?`, `className?` | OAuth2 provider login button |
| `OAuth2Callback` | `onSuccess`, `onError`, `loadingComponent?` | OAuth2 callback handler |
| `TwoFactorSetup` | `setupData`, `onConfirm`, `onError?`, `className?` | 2FA QR code + backup codes + confirm |
| `TwoFactorVerify` | `onSubmit`, `onVerified`, `onError?`, `className?` | TOTP code input for login challenge |
| `PasswordResetForm` | `onForgotPassword`, `onResetPassword`, `onResetComplete?`, `className?` | Two-step password reset form |

### Hooks

| Hook | Returns | Description |
|------|---------|-------------|
| `usePermissions()` | `{ permissions, loading, canAccess, canDo, isFieldVisible, hasRole, ... }` | Permission state and helpers |
| `useIAMAdmin()` | `{ users, roles, groups, resources, actions, permissions, oauth2Providers, auditLogs, sessions }` | Typed CRUD + admin operations |
| `useOAuth2()` | `{ providers, loading, loginWith, completeAuth, exchangeToken }` | OAuth2 authentication flow |
| `useTwoFactor()` | `{ setup, confirmSetup, verify, disable, regenerateBackupCodes }` | 2FA lifecycle management |
| `usePasswordReset()` | `{ forgotPassword, resetPassword }` | Password reset flow |
| `useSessions()` | `{ revokeUserSessions, revokeTenantSessions }` | Session revocation |

### Types

```typescript
import type {
  ResolvedPermissions,
  FeatureAccess,
  FieldAccess,
  ActionPermissions,
  IAMConfig,
  RefreshMode,
  TokenPair,
  OAuth2Provider,
  UserAuthProviderInfo,
  TwoFactorSetupResponse,
  AuditLogEntry,
} from "@mpofusindie/iam-react";
```

### IAMConfig

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `apiUrl` | `string` | required | IAM backend base URL |
| `token` | `string` | required | JWT access token |
| `refreshInterval?` | `number` | — | Auto-refresh interval (ms) |
| `refreshMode?` | `RefreshMode` | `"body"` | Refresh token delivery: `"body"`, `"cookie"`, or `"both"` |
| `tenantId?` | `string` | — | Tenant UUID for header-based multi-tenancy |

## How It Works

1. `IAMProvider` fetches `GET /me/permissions` from your IAM backend on mount
2. Permissions are cached in React state with TTL-based auto-refresh
3. Guard components read permissions from context and conditionally render children
4. 2FA/OAuth2/password reset hooks call the corresponding backend endpoints
5. No access logic in your components — guards handle everything declaratively

## Design Principles

- **Server is the source of truth** — frontend never determines access, only renders what the backend permits
- **Zero access logic in components** — use guards, not `if (role === "admin")`
- **Three-tier permissions** — feature-level, action-level, field-level
- **DENY wins** — if any role denies access, it's denied regardless of other roles
- **Implicit deny** — no permission = no access
- **Components accept callbacks** — 2FA and password reset components are decoupled from transport

## Backend

This library works with the [IAM Spring Boot Starter](https://iam.mpofusindie.dev):

```kotlin
implementation("dev.mpofusindie:iam-spring-boot-starter:1.2.0")
```

The backend provides:
- JWT authentication + OAuth2/OIDC (Google, Microsoft, any OIDC provider)
- TOTP two-factor authentication with backup codes
- Permission resolution with three-tier access control
- Password reset with session invalidation
- Multi-tenant support (JWT, header, subdomain resolution)
- Management APIs for users, roles, groups, resources, permissions, OAuth2 providers
- Audit logging with export and retention
- Rate limiting (in-memory or Redis)

## Troubleshooting

### Blank page after login

`IAMProvider` calls `GET /me/permissions` on mount. If the request fails, no permissions are loaded and guard components render nothing. Check:

- Is the JWT token valid and not expired? Look for `401` errors in the browser console.
- Is `apiUrl` in `IAMConfig` pointing to the correct backend URL?
- Is the backend running and reachable from the browser?

### 401 after token expiry

Access tokens expire after 15 minutes by default. After expiry, permission fetches return `401` and guards stop rendering. Use the refresh token flow (`refreshMode` in `IAMConfig`) to obtain new tokens automatically, or redirect the user to re-authenticate.

### Guards not rendering children

If a guard component renders nothing even though the user should have access:

- Check `usePermissions().loading` — guards return `null` while permissions are still loading.
- Verify the `resource` prop matches the resource code configured in the backend exactly (case-sensitive).
- For `ActionGuard`, verify the `action` prop matches the backend action code.
- For `FieldGuard`, verify the `field` prop matches the backend field name.

### OAuth2 callback blank page

React `StrictMode` double-mounts components in development, which can cause the OAuth2 callback to fire twice. Ensure your `onSuccess` handler in `OAuth2Callback` is idempotent — exchanging the same authorization code twice will fail on the second attempt.

### CORS errors

If the browser console shows CORS errors when calling the IAM backend, the backend must include your frontend origin in its allowed origins configuration:

```yaml
iam:
  security:
    cors-allowed-origins:
      - http://localhost:3000
      - https://your-frontend-domain.com
```

## License

MIT