@nightmar3/uauth-react
v1.0.6
Published
React hooks and components for Universal Auth SDK
Maintainers
Readme
@nightmar3/uauth-react
React hooks and components for Universal Auth SDK. Works with any backend implementing the Universal Auth contract.
[!IMPORTANT] Backend Required: This SDK requires a backend API. See Backend Requirements for the complete API contract, or use our Backend Implementation Guide for step-by-step instructions.
Setup Guide
Step 1: Install Packages
npm install @nightmar3/uauth-core @nightmar3/uauth-reactStep 2: Create Auth Instance
// lib/auth.ts
import { createAuth } from '@nightmar3/uauth-core'
export const auth = createAuth({
baseURL: 'https://api.yourapp.com/auth',
storage: localStorage,
})Step 3: Wrap Your App with AuthProvider
// App.tsx
import { AuthProvider } from '@nightmar3/uauth-react'
import { auth } from './lib/auth'
function App() {
return (
<AuthProvider auth={auth}>
<YourApp />
</AuthProvider>
)
}Step 4: Create Login Form
import { useState } from 'react'
import { useAuth } from '@nightmar3/uauth-react'
function LoginForm() {
const { signIn, isLoading, error } = useAuth()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
const result = await signIn('password', { email, password })
if (result.ok) {
// Redirect or show success
console.log('Logged in:', result.data?.user)
}
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Signing in...' : 'Sign In'}
</button>
{error && <div className="error">{error.message}</div>}
</form>
)
}Step 5: Create Protected Content
import { RequireAuth, useAuth } from '@nightmar3/uauth-react'
function Dashboard() {
const { user, signOut } = useAuth()
return (
<div>
<h1>Welcome {user?.name}</h1>
<button onClick={signOut}>Sign Out</button>
</div>
)
}
function App() {
return (
<RequireAuth fallback={<LoginForm />}>
<Dashboard />
</RequireAuth>
)
}That's it! Your React app now has full authentication support.
Use Cases
Email/Password Authentication
const { signIn, signUp, isLoading, error } = useAuth()
// Sign in
const result = await signIn('password', { email, password })
// Sign up
const result = await signUp({ email, password, name })OAuth Authentication (Optional)
OAuth is completely optional. If you don't need OAuth, skip this section - no extra network requests will be made.
To add OAuth support:
1. Add the OAuth2 plugin to AuthProvider:
import { createAuth, createOAuth2Plugin } from '@nightmar3/uauth-core'
import { AuthProvider } from '@nightmar3/uauth-react'
const auth = createAuth({
baseURL: 'https://api.yourapp.com/auth',
storage: localStorage,
})
// Only include if you want OAuth
const plugins = [createOAuth2Plugin()]
function App() {
return (
<AuthProvider auth={auth} plugins={plugins}>
<YourApp />
</AuthProvider>
)
}2. Use the useOAuth hook:
import { useOAuth } from '@nightmar3/uauth-react'
function OAuthButtons() {
const { providers, isLoading, signInWithOAuth } = useOAuth()
// No OAuth configured - render nothing
if (providers.length === 0) return null
if (isLoading) return <div>Loading...</div>
return (
<div>
{providers.map((provider) => (
<button
key={provider.name}
onClick={() => signInWithOAuth(provider.name)}
>
Continue with {provider.displayName || provider.name}
</button>
))}
</div>
)
}3. Complete login page with OAuth:
import { useState } from 'react'
import { useAuth, useOAuth } from '@nightmar3/uauth-react'
function LoginPage() {
const { signIn, isLoading, error } = useAuth()
const { providers, signInWithOAuth, isLoading: oauthLoading } = useOAuth()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
await signIn('password', { email, password })
}
return (
<div>
{/* OAuth Buttons (only shows if providers available) */}
{providers.length > 0 && (
<>
<div className="oauth-buttons">
{providers.map((p) => (
<button
key={p.name}
onClick={() => signInWithOAuth(p.name)}
disabled={oauthLoading}
>
Continue with {p.displayName}
</button>
))}
</div>
<div className="divider">or</div>
</>
)}
{/* Email/Password Form */}
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Signing in...' : 'Sign In'}
</button>
</form>
{error && <div className="error">{error.message}</div>}
</div>
)
}Protected Routes (React Router)
import { Navigate } from 'react-router-dom'
import { RequireAuth } from '@nightmar3/uauth-react'
function ProtectedRoute({ children }) {
return (
<RequireAuth fallback={<Navigate to="/login" />}>
{children}
</RequireAuth>
)
}
// Usage in routes
<Route path="/dashboard" element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
} />Role-Based Access Control
import { AuthGuard } from '@nightmar3/uauth-react'
function AdminRoute({ children }) {
return (
<AuthGuard
check={(user) => user.role === 'admin'}
fallback={<Navigate to="/" />}
>
{children}
</AuthGuard>
)
}Custom User Type
interface MyUser {
id: string
email: string
name: string
role: 'admin' | 'user'
}
// Type the hook
const { user } = useAuth<MyUser>()
// user is typed as MyUser | null
if (user) {
console.log(user.role) // 'admin' | 'user'
}
// Type the AuthGuard
<AuthGuard<MyUser> check={(user) => user.role === 'admin'}>
<AdminPanel />
</AuthGuard>Backend Requirements
The SDK expects a backend API that implements the following endpoints. You can use our FastAPI reference implementation or build your own.
Endpoints
| Method | Path | Description | Request Body | Response Data |
|--------|------|-------------|--------------|---------------|
| POST | /sign-in/password | Sign in with email/password | { email, password } | { user, tokens } |
| POST | /sign-in/oauth2 | Exchange OAuth code for tokens | { provider, code, redirect_uri } | { user, tokens } |
| POST | /sign-up | Create new account | { email, password, name? } | { user, tokens } |
| DELETE| /session | Sign out (revoke tokens) | - | { ok: boolean } |
| GET | /session | Get current user session | - | { user } |
| POST | /token/refresh | Refresh access token | { refresh_token } | { tokens } |
| GET | /providers | List available OAuth providers | - | { providers } |
Response Format
All API responses must follow this envelope structure:
interface ApiResponse<T> {
ok: boolean
data: T | null
error: {
code: string
message: string
details?: any
} | null
}Token Structure
The backend must return tokens in this format:
interface AuthTokens {
access_token: string
refresh_token: string
expires_in: number // seconds
}OAuth Provider Response (Optional)
If implementing OAuth support, the /providers endpoint must return:
interface OAuth2Provider {
name: string
displayName: string
clientId: string
authorizationUrl: string
scope?: string
}Example:
{
"ok": true,
"data": {
"providers": [
{
"name": "google",
"displayName": "Google",
"clientId": "your-google-client-id",
"authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth",
"scope": "openid email profile"
}
]
},
"error": null
}API Reference
AuthProvider
Provides authentication context to your app.
<AuthProvider
auth={authInstance}
plugins={[]} // Optional: plugins like OAuth2
loadOnMount={true} // Optional: load session on mount (default: true)
autoRefresh={true} // Optional: auto-refresh tokens before expiry (default: true)
refreshBeforeExpiry={60} // Optional: seconds before expiry to refresh (default: 60)
>
{children}
</AuthProvider>| Prop | Type | Default | Description |
|------|------|---------|-------------|
| auth | UniversalAuth | Required | Auth instance from createAuth() |
| plugins | Plugin[] | [] | Plugins to install (e.g., OAuth2) |
| loadOnMount | boolean | true | Load session on mount |
| autoRefresh | boolean | true | Auto-refresh tokens before expiry |
| refreshBeforeExpiry | number | 60 | Seconds before expiry to refresh |
useAuth()
Main hook for accessing auth state and methods.
const {
user, // Current user or null
isLoading, // Loading state
isAuthenticated, // True if user is logged in
error, // Last error or null
signIn, // Sign in function
signUp, // Sign up function
signOut, // Sign out function
refresh, // Refresh tokens
refetch, // Reload session
setUser, // Manually set user
getPlugin, // Get installed plugin by name
pluginsReady, // Whether plugins are installed
auth, // The auth instance
} = useAuth()useOAuth() (requires OAuth2 plugin)
Hook for OAuth authentication. Only works when createOAuth2Plugin() is passed to AuthProvider.
const {
providers, // Available OAuth providers
isLoading, // Loading state
signInWithOAuth, // Sign in with OAuth provider
} = useOAuth()| Property | Type | Description |
|----------|------|-------------|
| providers | OAuth2Provider[] | Available providers from backend |
| isLoading | boolean | Whether providers are loading |
| signInWithOAuth | (provider: string) => Promise<void> | Trigger OAuth flow |
RequireAuth
Only renders children if user is authenticated.
<RequireAuth
fallback={<LoginPage />}
loadingFallback={<Spinner />}
>
<ProtectedContent />
</RequireAuth>| Prop | Type | Default | Description |
|------|------|---------|-------------|
| children | ReactNode | Required | Content to show when authenticated |
| fallback | ReactNode | null | Content to show when not authenticated |
| loadingFallback | ReactNode | null | Content to show while loading |
GuestOnly
Only renders children if user is NOT authenticated.
<GuestOnly fallback={<Navigate to="/dashboard" />}>
<LoginPage />
</GuestOnly>| Prop | Type | Default | Description |
|------|------|---------|-------------|
| children | ReactNode | Required | Content to show when not authenticated |
| fallback | ReactNode | null | Content to show when authenticated |
| loadingFallback | ReactNode | null | Content to show while loading |
AuthGuard
Advanced guard with custom check function.
<AuthGuard
check={(user) => user.role === 'admin'}
fallback={<AccessDenied />}
>
<AdminPanel />
</AuthGuard>| Prop | Type | Default | Description |
|------|------|---------|-------------|
| children | ReactNode \| (user) => ReactNode | Required | Content or render function |
| check | (user) => boolean | - | Custom validation function |
| fallback | ReactNode | null | Content when check fails |
| loadingFallback | ReactNode | null | Content while loading |
Patterns
Auto Token Refresh
By default, tokens are automatically refreshed before they expire. This happens silently in the background - no user interaction required.
// Default: auto-refresh enabled, refreshes 60 seconds before expiry
<AuthProvider auth={auth}>
<App />
</AuthProvider>
// Customize when to refresh (e.g., 5 minutes before expiry)
<AuthProvider auth={auth} refreshBeforeExpiry={300}>
<App />
</AuthProvider>
// Disable auto-refresh (not recommended)
<AuthProvider auth={auth} autoRefresh={false}>
<App />
</AuthProvider>How it works:
- When user signs in or session loads, a timer is set based on token expiry
- Token is refreshed automatically before it expires
- On sign out, the timer is cleared
- If refresh fails, the next API request will trigger a 401 → automatic refresh retry
Loading States
function App() {
const { isLoading } = useAuth()
if (isLoading) {
return <FullPageSpinner />
}
return <YourApp />
}Error Handling
function LoginForm() {
const { signIn, error } = useAuth()
const [localError, setLocalError] = useState(null)
const handleSubmit = async (data) => {
setLocalError(null)
const result = await signIn('password', data)
if (!result.ok) {
setLocalError(result.error?.message || 'Login failed')
}
}
return (
<>
{localError && <Alert>{localError}</Alert>}
{error && <Alert>{error.message}</Alert>}
<form onSubmit={handleSubmit}>...</form>
</>
)
}Manual Session Refresh
function RefreshButton() {
const { refresh, refetch } = useAuth()
const handleRefresh = async () => {
await refresh() // Refresh tokens
await refetch() // Reload user data
}
return <button onClick={handleRefresh}>Refresh Session</button>
}Conditionally Enable OAuth
// Only enable OAuth in certain environments
const plugins = process.env.REACT_APP_ENABLE_OAUTH === 'true'
? [createOAuth2Plugin()]
: []
function App() {
return (
<AuthProvider auth={auth} plugins={plugins}>
<YourApp />
</AuthProvider>
)
}Legacy OAuth2 API
The previous OAuth2Provider and useOAuth2 APIs are still available for backwards compatibility, but we recommend using the new plugin-based approach with useOAuth().
// Legacy (still works)
import { OAuth2Provider, useOAuth2 } from '@nightmar3/uauth-react'
// New approach (recommended)
import { AuthProvider, useOAuth } from '@nightmar3/uauth-react'
import { createOAuth2Plugin } from '@nightmar3/uauth-core'License
MIT
