npm package: @micahgoodman/auth
Description: Complete authentication package for React with Supabase and Keycloak support. Includes email/password auth and embedded SDK mode for federated identity.
Author: Your Name
Latest version: 2.0.0

# @micahgoodman/auth

Complete authentication package for React with Supabase and Keycloak support. Includes email/password auth, OAuth with Keycloak, and embedded SDK mode for federated identity.

## Features

🔐 **Multiple Auth Strategies** - Email/password, Keycloak OAuth, embedded SDK mode  
🔗 **Single Sign-On (SSO)** - Keycloak integration for federated identity  
📦 **Embedded SDK Mode** - Use as library with remote backend authentication  
🎨 **Ready-to-Use UI** - Pre-built modals and auth button  
⚛️ **React Context** - Easy state management across your app  
📱 **TypeScript** - Full type safety  
🔌 **Supabase Integration** - Built for Supabase Auth with Keycloak provider

## Installation

### Basic Installation (Email/Password Auth)
```bash
npm install @micahgoodman/auth @supabase/supabase-js
```

### With Keycloak Support
```bash
npm install @micahgoodman/auth @supabase/supabase-js keycloak-js
```

## Three Authentication Modes

This package supports three authentication modes:

1. **Email/Password Auth** (default) - Traditional auth with Supabase
2. **Keycloak Auth** - For standalone apps using Keycloak OAuth
3. **Embedded SDK Mode** - For components embedded in parent apps with SSO

---

## Mode 1: Email/Password Auth (Default)

**Use this for:** Traditional email/password authentication

```tsx
import { createSupabaseClient, AuthProvider, AuthButton } from '@micahgoodman/auth';

// 1. Create Supabase client
const supabase = createSupabaseClient({
  supabaseUrl: import.meta.env.VITE_SUPABASE_URL,
  supabaseKey: import.meta.env.VITE_SUPABASE_ANON_KEY,
});

// 2. Wrap your app with AuthProvider
function App() {
  return (
    <AuthProvider supabaseClient={supabase}>
      <YourApp />
    </AuthProvider>
  );
}

// 3. Use AuthButton or useAuth hook
function Header() {
  return (
    <header>
      <h1>My App</h1>
      <AuthButton />
    </header>
  );
}
```

---

## Mode 2: Keycloak Auth (Standalone)

**Use this for:** Standalone apps using Keycloak for OAuth authentication

### Setup

```tsx
import Keycloak from 'keycloak-js';
import { 
  createSupabaseClient, 
  KeycloakAuthProvider, 
  useKeycloakAuth 
} from '@micahgoodman/auth';

// 1. Initialize Keycloak
const keycloak = new Keycloak({
  url: 'https://keycloak.example.com',
  realm: 'my-realm',
  clientId: 'my-app'
});

// 2. Create Supabase client
const supabase = createSupabaseClient({
  supabaseUrl: import.meta.env.VITE_SUPABASE_URL,
  supabaseKey: import.meta.env.VITE_SUPABASE_ANON_KEY,
});

// 3. Wrap app with KeycloakAuthProvider
function App() {
  return (
    <KeycloakAuthProvider
      supabaseClient={supabase}
      keycloakInstance={keycloak}
      onLoad="login-required"
    >
      <YourApp />
    </KeycloakAuthProvider>
  );
}

// 4. Use in components
function MyComponent() {
  const { user, signOut, keycloakAuthenticated } = useKeycloakAuth();
  
  return (
    <div>
      <p>Welcome, {user?.email}</p>
      <button onClick={signOut}>Sign Out</button>
    </div>
  );
}
```

### Supabase Configuration

Configure Keycloak as auth provider in Supabase dashboard:

```tsx
import { getSupabaseKeycloakConfig } from '@micahgoodman/auth';

const config = getSupabaseKeycloakConfig({
  url: 'https://keycloak.example.com',
  realm: 'my-realm',
  clientId: 'my-app'
});

// Use in Supabase Dashboard:
// Authentication > Providers > Keycloak
// Keycloak URL: config.keycloakUrl
// Client ID: config.clientId
```

---

## Mode 3: Embedded SDK Mode (Recommended for Embedded Apps)

**Use this for:** Embedding components in a parent app with existing Keycloak authentication

**Key Benefits:**
- ✅ User logs in ONCE in parent app
- ✅ No additional login UI for embedded components
- ✅ Silent token exchange (user sees nothing)
- ✅ Automatic token refresh
- ✅ Works across multiple embedded modules

### Parent App Setup

```tsx
import Keycloak from 'keycloak-js';
import { RemoteAuthProvider } from '@micahgoodman/auth';
import { NotesComponents } from '@company/notes';

// 1. Initialize Keycloak (parent app)
const keycloak = new Keycloak({
  url: 'https://keycloak.example.com',
  realm: 'shared-realm',
  clientId: 'parent-app'
});

function ParentApp() {
  const [token, setToken] = React.useState<string>();

  React.useEffect(() => {
    // User logs in ONCE here
    keycloak.init({ onLoad: 'login-required' }).then((authenticated) => {
      if (authenticated) {
        setToken(keycloak.token);
      }
    });

    // Handle token refresh
    keycloak.onTokenExpired = () => {
      keycloak.updateToken(30).then(() => {
        setToken(keycloak.token);
      });
    };
  }, []);

  if (!token) return <div>Loading...</div>;

  return (
    <div className="app">
      <h1>Productivity Suite</h1>
      
      {/* Embedded notes - no additional login! */}
      <RemoteAuthProvider
        supabaseClient={notesSupabase}
        keycloakToken={token}
        onTokenExpired={async () => {
          await keycloak.updateToken(30);
          return keycloak.token!;
        }}
      >
        <NotesComponents />
      </RemoteAuthProvider>
    </div>
  );
}
```

### Embedded Module Setup

In your embedded npm package:

```tsx
import { createSupabaseClient, configureNotesApi } from '@company/notes';

// Point to the remote Supabase backend
const notesSupabase = createSupabaseClient({
  supabaseUrl: 'https://notes-backend.supabase.co',
  supabaseKey: 'notes-anon-key'
});

// Components automatically authenticated via RemoteAuthProvider
export function NotesComponents() {
  const { user } = useRemoteAuth();
  // user is authenticated - no login needed!
  return <div>Notes for {user?.email}</div>;
}
```

### How It Works

1. User logs into parent app with Keycloak (sees login UI ONCE)
2. Parent app gets Keycloak token
3. Parent passes token to `RemoteAuthProvider`
4. Provider silently exchanges token for Supabase session (backend-to-backend)
5. Embedded components are now authenticated - user never sees another login!

This is **federated identity** - one login, multiple services.

---

## Unified Auth Hook

Use `useUnifiedAuth()` to write components that work with ANY auth provider:

```tsx
import { useUnifiedAuth } from '@micahgoodman/auth';

function MyComponent() {
  const { user, signOut, loading } = useUnifiedAuth();
  // Works with AuthProvider, KeycloakAuthProvider, or RemoteAuthProvider!
  
  if (loading) return <div>Loading...</div>;
  
  return (
    <div>
      {user && (
        <>
          <p>Welcome, {user.email}</p>
          <button onClick={signOut}>Sign Out</button>
        </>
      )}
    </div>
  );
}
```

## Components

### `<AuthButton />`

All-in-one auth UI - shows sign-in button or profile button based on auth state.

```tsx
<AuthButton />
```

### `<AuthModal />`

Sign in/sign up modal.

```tsx
const [showAuth, setShowAuth] = useState(false);
<AuthModal onClose={() => setShowAuth(false)} />
```

### `<ProfileModal />`

User profile management modal.

```tsx
const [showProfile, setShowProfile] = useState(false);
<ProfileModal onClose={() => setShowProfile(false)} />
```

## API Reference

### Hooks

#### `useAuth()`
For email/password authentication (works with `AuthProvider`).

```tsx
import { useAuth } from '@micahgoodman/auth';

const { 
  user, 
  session, 
  loading, 
  signIn, 
  signOut, 
  signUp, 
  resetPassword, 
  updateProfile 
} = useAuth();
```

#### `useKeycloakAuth()`
For Keycloak authentication (works with `KeycloakAuthProvider`).

```tsx
import { useKeycloakAuth } from '@micahgoodman/auth';

const { 
  user, 
  session, 
  loading, 
  keycloakAuthenticated,
  keycloak, // Access to Keycloak instance
  signIn,
  signOut,
  error 
} = useKeycloakAuth();
```

#### `useRemoteAuth()`
For embedded SDK mode (works with `RemoteAuthProvider`).

```tsx
import { useRemoteAuth } from '@micahgoodman/auth';

const { 
  user, 
  session, 
  loading, 
  error, 
  signOut 
} = useRemoteAuth();
```

#### `useUnifiedAuth()`
Works with ANY auth provider - writes components once, works everywhere.

```tsx
import { useUnifiedAuth } from '@micahgoodman/auth';

const { user, session, loading, signOut } = useUnifiedAuth();
// Automatically detects and uses the correct provider
```

### Configuration Utilities

#### `getSupabaseKeycloakConfig()`
Generate Supabase Keycloak provider configuration.

```tsx
import { getSupabaseKeycloakConfig } from '@micahgoodman/auth';

const config = getSupabaseKeycloakConfig({
  url: 'https://keycloak.example.com',
  realm: 'my-realm',
  clientId: 'my-app'
});
// Returns: { keycloakUrl, clientId }
```

#### `getKeycloakConfigFromEnv()`
Load Keycloak config from environment variables.

```tsx
import { getKeycloakConfigFromEnv } from '@micahgoodman/auth';

const config = getKeycloakConfigFromEnv();
// Reads VITE_KEYCLOAK_URL, VITE_KEYCLOAK_REALM, VITE_KEYCLOAK_CLIENT_ID
```

#### `KEYCLOAK_INIT_OPTIONS`
Pre-configured Keycloak init options.

```tsx
import { KEYCLOAK_INIT_OPTIONS } from '@micahgoodman/auth';

keycloak.init(KEYCLOAK_INIT_OPTIONS.LOGIN_REQUIRED);
// or
keycloak.init(KEYCLOAK_INIT_OPTIONS.CHECK_SSO);
```

## Environment Setup

### For Email/Password Auth

```env
VITE_SUPABASE_URL=https://your-project.supabase.co
VITE_SUPABASE_ANON_KEY=your-anon-key
```

### For Keycloak Auth

```env
VITE_SUPABASE_URL=https://your-project.supabase.co
VITE_SUPABASE_ANON_KEY=your-anon-key
VITE_KEYCLOAK_URL=https://keycloak.example.com
VITE_KEYCLOAK_REALM=my-realm
VITE_KEYCLOAK_CLIENT_ID=my-app
```

### For Embedded SDK Mode (Parent App)

```env
# Parent app's own Supabase (optional)
VITE_SUPABASE_URL=https://parent-project.supabase.co
VITE_SUPABASE_ANON_KEY=parent-anon-key

# Keycloak (shared across all apps)
VITE_KEYCLOAK_URL=https://keycloak.example.com
VITE_KEYCLOAK_REALM=shared-realm
VITE_KEYCLOAK_CLIENT_ID=parent-app

# Each embedded module's Supabase backend
VITE_NOTES_SUPABASE_URL=https://notes-backend.supabase.co
VITE_NOTES_SUPABASE_ANON_KEY=notes-anon-key
```

### Supabase Dashboard Configuration

#### Email/Password Auth
- Enable email auth in Supabase dashboard
- Configure email templates
- Set up RLS policies for your tables

#### Keycloak Provider

1. Go to **Authentication > Providers > Keycloak**
2. Enable Keycloak provider
3. Configure:
   - **Keycloak URL:** `https://your-keycloak.com/realms/your-realm`
   - **Client ID:** Your app's Keycloak client ID
   - **Client Secret:** From Keycloak client settings
   
4. Set callback URL in Keycloak:
   - **Valid Redirect URIs:** `https://your-project.supabase.co/auth/v1/callback`
   - **Web Origins:** `https://your-app.com`

5. Enable **Direct Access Grants** in Keycloak client settings

### Styling

The package uses minimal inline styles for maximum flexibility:

- Override with CSS classes
- Provide your own modal components  
- Use hooks directly for complete UI control

```tsx
// Custom UI with hooks
import { useUnifiedAuth } from '@micahgoodman/auth';

function CustomAuthUI() {
  const { user, signOut } = useUnifiedAuth();
  return (
    <YourCustomUI user={user} onSignOut={signOut} />
  );
}
```

## TypeScript

```ts
import type {
  SupabaseConfig,
  AuthProviderProps,
} from '@micahgoodman/auth';

import type {
  User,
  Session,
  AuthError,
} from '@supabase/supabase-js';
```

## Advanced Usage

### Multi-Module Architecture

For apps with multiple embedded modules:

```tsx
import Keycloak from 'keycloak-js';
import { RemoteAuthProvider } from '@micahgoodman/auth';

const keycloak = new Keycloak(/* config */);

function App() {
  const [token, setToken] = React.useState<string>();
  
  // ... Keycloak initialization ...
  
  return (
    <div>
      {/* Notes Module */}
      <RemoteAuthProvider 
        supabaseClient={notesSupabase}
        keycloakToken={token}
        onTokenExpired={refreshToken}
      >
        <NotesSection />
      </RemoteAuthProvider>
      
      {/* Tasks Module */}
      <RemoteAuthProvider 
        supabaseClient={tasksSupabase}
        keycloakToken={token}
        onTokenExpired={refreshToken}
      >
        <TasksSection />
      </RemoteAuthProvider>
      
      {/* Each module has its own Supabase backend */}
      {/* All share the same Keycloak realm */}
      {/* User logs in ONCE */}
    </div>
  );
}
```

### Protected Routes

```tsx
import { useUnifiedAuth, isAuthenticated } from '@micahgoodman/auth';

function ProtectedRoute({ children }) {
  const auth = useUnifiedAuth();

  if (auth.loading) return <Loading />;
  if (!isAuthenticated(auth)) return <Navigate to="/login" />;

  return children;
}
```

## Troubleshooting

### "Module not found: keycloak-js"

**Solution:** Keycloak is an optional dependency. Install it if using Keycloak features:

```bash
npm install keycloak-js
```

### "CORS error" in Embedded SDK Mode

**Solution:** Configure CORS in Supabase Edge Functions:

```typescript
const corsHeaders = {
  'Access-Control-Allow-Origin': 'https://parent-app.com',
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
};
```

### "Unauthorized" errors with Keycloak

**Checklist:**
- ✅ Keycloak provider configured in Supabase dashboard
- ✅ Client ID matches in Keycloak and Supabase
- ✅ All apps use the same Keycloak realm
- ✅ Redirect URIs configured in Keycloak
- ✅ Direct Access Grants enabled

### Token not refreshing in Embedded SDK Mode

**Solution:** Implement `onTokenExpired` callback:

```tsx
<RemoteAuthProvider
  keycloakToken={token}
  onTokenExpired={async () => {
    await keycloak.updateToken(30);
    return keycloak.token!;
  }}
>
```

### User sees login twice

**Issue:** You're using the wrong provider for embedded mode.

**Solution:** Use `RemoteAuthProvider` (not `KeycloakAuthProvider`) in embedded components:

```tsx
// ❌ Wrong - shows login UI again
<KeycloakAuthProvider keycloakInstance={keycloak}>

// ✅ Correct - silent token exchange
<RemoteAuthProvider keycloakToken={token}>
```

## Migration from v1.x

### No Breaking Changes!

Existing email/password auth code works unchanged:

```tsx
// v1.x code - still works in v2.x
import { AuthProvider, useAuth } from '@micahgoodman/auth';
```

### New Features Available

- ✅ Keycloak authentication
- ✅ Embedded SDK mode
- ✅ Unified auth hook
- ✅ Configuration utilities

Adopt these features incrementally at your own pace.

## Architecture Decisions

### Why Three Providers?

Each provider serves a specific use case:

1. **AuthProvider** - Traditional apps with email/password auth
2. **KeycloakAuthProvider** - Standalone apps using OAuth/SSO
3. **RemoteAuthProvider** - Embedded components (library mode with remote backend)

### Why Not a Single Provider?

Separate providers:
- ✅ Keep bundle size small (only import what you need)
- ✅ Make code intent clear
- ✅ Allow different initialization patterns
- ✅ Avoid prop confusion

### NPM Package Scope

**This package handles:**
- Token exchange (`signInWithIdToken`)
- Session management
- Token refresh
- Auth state management
- UI components

**Your app handles:**
- Keycloak instance initialization
- Environment configuration
- App-specific routing
- Backend setup

This separation keeps the package focused and reusable.

## Examples

See the `/examples` directory (coming soon) for:
- Email/password app
- Keycloak standalone app
- Parent app with embedded modules
- Multi-module architecture

## Contributing

Contributions welcome! Please open an issue first to discuss changes.

## Support

For issues or questions:
- 📝 [GitHub Issues](https://github.com/micahgoodman/auth/issues)
- 📖 [Supabase Docs](https://supabase.com/docs/guides/auth)
- 🔑 [Keycloak Docs](https://www.keycloak.org/docs/latest/)

## License

MIT