GoIAM React SDK

August 29, 2025 ยท View on GitHub

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

npm install @goiam/react

or

yarn add @goiam/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:

import React from 'react';
import { useGoIam } from '@goiam/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:

import React, { useEffect } from 'react';
import { useGoIam } from '@goiam/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:

import React, { useEffect } from 'react';
import { useGoIam } from '@goiam/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:

import React from 'react';
import { useGoIam } from '@goiam/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:

import React from 'react';
import { AuthGuard } from '@goiam/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:

import React from 'react';
import { withAuthGuard } from '@goiam/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

PropTypeDefaultDescription
childrenReactNode-Content to render when authenticated and authorized
fallbackComponentTypeDefaultUnauthenticatedComponentComponent to show when user is not authenticated
redirectToLoginbooleanfalseAutomatically redirect to login page if not authenticated
requiredResourcesstring[][]Array of required resource permissions
unauthorizedComponentComponentTypeDefaultUnauthorizedComponentComponent 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.

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.

// 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.

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.

// Check single resource
const canAdmin = hasRequiredResources(['admin']);

// Check multiple resources (user must have ALL)
const canManageUsers = hasRequiredResources(['admin', 'user_management']);

User Data Structure

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
// 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:

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:

function App() {
  const { setLoginPageUrl, setCallbackPageUrl } = useGoIam();

  useEffect(() => {
    setLoginPageUrl('/custom-login');
    setCallbackPageUrl('/auth/callback');
  }, [setLoginPageUrl, setCallbackPageUrl]);
}

Manual State Management

Control loading states manually if needed:

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

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:

import { useGoIam } from '@goiam/react';
import type { User } from '@goiam/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

import React, { useEffect } from 'react';
import { useGoIam } from '@goiam/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

import React, { useEffect, useState } from 'react';
import { useGoIam } from '@goiam/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 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

// โŒ 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):

// Old provider setup
<GoIamProvider config={config}>
  <App />
</GoIamProvider>;

// Old hook usage
const { isAuthenticated, isLoading } = useGoIam();

After (Hookstate):

// 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

npm run build

Testing

npm test
npm run test:coverage

Linting

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:

// 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 file for details.

Support

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