@micahgoodman/auth
v2.0.0
Published
Complete authentication package for React with Supabase and Keycloak support. Includes email/password auth and embedded SDK mode for federated identity.
Maintainers
Readme
@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)
npm install @micahgoodman/auth @supabase/supabase-jsWith Keycloak Support
npm install @micahgoodman/auth @supabase/supabase-js keycloak-jsThree Authentication Modes
This package supports three authentication modes:
- Email/Password Auth (default) - Traditional auth with Supabase
- Keycloak Auth - For standalone apps using Keycloak OAuth
- Embedded SDK Mode - For components embedded in parent apps with SSO
Mode 1: Email/Password Auth (Default)
Use this for: Traditional email/password authentication
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
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:
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.clientIdMode 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
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:
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
- User logs into parent app with Keycloak (sees login UI ONCE)
- Parent app gets Keycloak token
- Parent passes token to
RemoteAuthProvider - Provider silently exchanges token for Supabase session (backend-to-backend)
- 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:
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.
<AuthButton /><AuthModal />
Sign in/sign up modal.
const [showAuth, setShowAuth] = useState(false);
<AuthModal onClose={() => setShowAuth(false)} /><ProfileModal />
User profile management modal.
const [showProfile, setShowProfile] = useState(false);
<ProfileModal onClose={() => setShowProfile(false)} />API Reference
Hooks
useAuth()
For email/password authentication (works with AuthProvider).
import { useAuth } from '@micahgoodman/auth';
const {
user,
session,
loading,
signIn,
signOut,
signUp,
resetPassword,
updateProfile
} = useAuth();useKeycloakAuth()
For Keycloak authentication (works with KeycloakAuthProvider).
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).
import { useRemoteAuth } from '@micahgoodman/auth';
const {
user,
session,
loading,
error,
signOut
} = useRemoteAuth();useUnifiedAuth()
Works with ANY auth provider - writes components once, works everywhere.
import { useUnifiedAuth } from '@micahgoodman/auth';
const { user, session, loading, signOut } = useUnifiedAuth();
// Automatically detects and uses the correct providerConfiguration Utilities
getSupabaseKeycloakConfig()
Generate Supabase Keycloak provider configuration.
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.
import { getKeycloakConfigFromEnv } from '@micahgoodman/auth';
const config = getKeycloakConfigFromEnv();
// Reads VITE_KEYCLOAK_URL, VITE_KEYCLOAK_REALM, VITE_KEYCLOAK_CLIENT_IDKEYCLOAK_INIT_OPTIONS
Pre-configured Keycloak init options.
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
VITE_SUPABASE_URL=https://your-project.supabase.co
VITE_SUPABASE_ANON_KEY=your-anon-keyFor Keycloak Auth
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-appFor Embedded SDK Mode (Parent App)
# 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-keySupabase Dashboard Configuration
Email/Password Auth
- Enable email auth in Supabase dashboard
- Configure email templates
- Set up RLS policies for your tables
Keycloak Provider
Go to Authentication > Providers > Keycloak
Enable Keycloak provider
Configure:
- Keycloak URL:
https://your-keycloak.com/realms/your-realm - Client ID: Your app's Keycloak client ID
- Client Secret: From Keycloak client settings
- Keycloak URL:
Set callback URL in Keycloak:
- Valid Redirect URIs:
https://your-project.supabase.co/auth/v1/callback - Web Origins:
https://your-app.com
- Valid Redirect URIs:
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
// Custom UI with hooks
import { useUnifiedAuth } from '@micahgoodman/auth';
function CustomAuthUI() {
const { user, signOut } = useUnifiedAuth();
return (
<YourCustomUI user={user} onSignOut={signOut} />
);
}TypeScript
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:
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
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:
npm install keycloak-js"CORS error" in Embedded SDK Mode
Solution: Configure CORS in Supabase Edge Functions:
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:
<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:
// ❌ 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:
// 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:
- AuthProvider - Traditional apps with email/password auth
- KeycloakAuthProvider - Standalone apps using OAuth/SSO
- 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:
License
MIT
