SDKs
UniAuth provides official SDKs for JavaScript, React, and PHP to simplify integration. Each SDK handles PKCE generation, state management, token exchange, ID token verification, and session handling so you can add "Sign in with UniAuth" with minimal code.
SDK v1.0 Deprecated
SDK v1.0 has been deprecated and archived. All new integrations must use v2.0. v1.0 lacked ID token signature verification and is no longer maintained.
What's New in v2.0
- ID token signature verification — RS256 ID tokens are now verified against the JWKS endpoint by default. v1.0 only checked the nonce, which is insufficient for production use.
- Pluggable token storage — Choose between
memory,sessionStorage, orlocalStoragedepending on your security requirements. v1.0 was hardcoded to sessionStorage. - Cross-tab synchronization — Login and logout events are broadcast across tabs via BroadcastChannel, keeping all tabs in sync.
- Organization support — First-class multi-tenant support with the
organizationparameter in config and login options. - ID token claims access — New
getIdTokenClaims()method anduseIdTokenClaims()hook to read verified claims (ACR, AMR, groups, auth_time, session ID). - Scopes as array — JS SDK accepts
scopesas a string array instead of a space-delimited string, avoiding delimiter mistakes. - Retry support — Optional automatic retry with configurable max retries for network failures.
- Custom authorization parameters — Pass arbitrary query parameters to the authorize endpoint via
customParams. - Login options —
login()acceptsmaxAge,acrValues,organization, and per-call scope overrides. - New error type —
TimeoutErrorfor request timeout failures, separate fromNetworkError. - Lifecycle management —
destroy()method to clean up event listeners and BroadcastChannel when unmounting. - New React hooks —
useAccessToken(),useOrganization(), anduseIdTokenClaims().
JavaScript SDK (@uniauth/js)
A lightweight, framework-agnostic JavaScript SDK that works in any browser or Node.js environment. It handles the full OAuth2/OIDC flow including PKCE, ID token signature verification, pluggable token storage, cross-tab sync, and automatic token refresh.
Installation
npm install @uniauth/jsSetup
import { UniAuth } from '@uniauth/js';
const auth = new UniAuth({
issuer: 'https://uniauth.id',
clientId: 'your-client-id',
redirectUri: 'https://yourapp.com/callback',
scopes: ['openid', 'profile', 'email'], // v2: string array (default: openid profile email)
storage: 'sessionStorage', // v2: "memory" | "sessionStorage" | "localStorage"
verifyIdToken: true, // v2: RS256 signature verification (default: true)
crossTabSync: true, // v2: sync login/logout across tabs (default: true)
postLogoutRedirectUri: 'https://yourapp.com',
// Optional v2 fields:
// organization: 'org_abc123', // Multi-tenant org context
// retry: true, // Auto-retry on network failure (default: 3 retries)
// retry: { maxRetries: 5 }, // Custom retry count
// customParams: { audience: 'https://api.example.com' },
});Login
Redirect the user to the UniAuth authorization page. The SDK generates the PKCE challenge and stores the verifier automatically.
// Redirect to UniAuth login
await auth.login();
// With v2 login options
await auth.login({
prompt: 'consent', // Force consent screen
loginHint: '[email protected]', // Pre-fill email
maxAge: 300, // Require re-auth if last login > 5 min ago
acrValues: 'urn:uniauth:acr:mfa', // Request MFA-level authentication
organization: 'org_abc123', // Override org from config
scope: 'openid profile email groups', // Override scopes for this login
customParams: { ui_locales: 'en' }, // Additional authorize params
});Handle Callback
On your callback page, call handleCallback() to exchange the authorization code for tokens. The SDK verifies the CSRF state, validates the PKCE verifier, and verifies the ID token signature against the JWKS endpoint. It returns a TokenSet on success and throws typed errors on failure.
import { UniAuth, AuthorizationError, TokenError } from '@uniauth/js';
// On your callback page (e.g., /callback)
try {
const tokens = await auth.handleCallback();
// tokens.access_token = 'eyJ...'
// tokens.id_token = 'eyJ...'
// tokens.refresh_token = '...'
// tokens.expires_at = 1717027200000 (ms timestamp)
console.log('Logged in! Access token expires at', tokens.expires_at);
} catch (err) {
if (err instanceof AuthorizationError) {
// CSRF state mismatch, nonce mismatch, or user denied access
console.error('Authorization failed:', err.message);
} else if (err instanceof TokenError) {
// Invalid grant, expired code, or ID token signature verification failed
console.error('Token exchange failed:', err.message);
}
}Get Current User
// Fetches user profile from the userinfo endpoint
const user = await auth.getUser();
if (user) {
console.log(user.email, user.name, user.groups);
} else {
console.log('Not logged in');
}ID Token Claims (v2)
After handleCallback(), the verified ID token claims are available without an additional network request:
// Get verified ID token claims (no network request)
const claims = auth.getIdTokenClaims();
if (claims) {
console.log('Subject:', claims.sub);
console.log('ACR level:', claims.acr); // e.g., "urn:uniauth:acr:mfa"
console.log('Auth methods:', claims.amr); // e.g., ["pwd", "otp"]
console.log('Session ID:', claims.sid);
console.log('Groups:', claims.groups); // e.g., ["admins", "developers"]
console.log('Auth time:', claims.auth_time); // Unix timestamp of authentication
}
// Get the current organization context
const org = auth.getOrganization();
// Returns config.organization, or org_id from ID token claimsLogout
Calling logout() revokes the refresh token server-side, clears token storage, broadcasts a logout event to other tabs, and redirects to the UniAuth end-session endpoint.
// Revokes tokens server-side, clears storage, syncs across tabs, and redirects
// to postLogoutRedirectUri configured in the constructor
await auth.logout();Token Management
// Get the current access token (auto-refreshes if expired)
const token = await auth.getAccessToken();
// Make authenticated API calls
const response = await fetch('https://api.yourapp.com/data', {
headers: { Authorization: `Bearer ${token}` }
});
// Check if the user is authenticated (non-expired tokens exist)
const isLoggedIn = auth.isAuthenticated();
// Check if the access token is expired (with optional buffer in seconds)
const expired = auth.isTokenExpired(60); // true if expires within 60s
// Get the full token set (access_token, id_token, refresh_token, etc.)
const tokenSet = auth.getTokens();Cross-Tab Sync (v2)
When crossTabSync is enabled (default), login and logout events are broadcast to all tabs via BroadcastChannel. You can listen for these events with a custom handler:
// Listen for cross-tab auth events
auth.onSync((event) => {
// event is "login" | "logout" | "token_refresh"
if (event === 'logout') {
window.location.href = '/';
} else if (event === 'login') {
window.location.reload();
}
});
// Clean up when your app unmounts (removes BroadcastChannel listener)
auth.destroy();Error Handling
The SDK provides a typed error hierarchy. All errors extend the base UniAuthError class, so you can catch specific error types for fine-grained handling:
import {
UniAuth, UniAuthError,
AuthorizationError, TokenError, NetworkError, TimeoutError
} from "@uniauth/js";
try {
const tokens = await auth.handleCallback();
} catch (err) {
if (err instanceof AuthorizationError) {
// CSRF state mismatch, nonce mismatch, access denied by user
console.error('Authorization error:', err.code, err.message);
} else if (err instanceof TokenError) {
// Invalid grant, expired code, invalid client, ID token verification failed
console.error('Token error:', err.code, err.message);
} else if (err instanceof TimeoutError) {
// Request timed out (v2 — separate from NetworkError)
console.error('Timeout:', err.message);
} else if (err instanceof NetworkError) {
// Fetch failure, DNS resolution error, offline
console.error('Network error:', err.message);
}
}
// Error codes you may encounter:
// AuthorizationError: "state_mismatch", "nonce_mismatch", "access_denied",
// "invalid_callback", "missing_verifier"
// TokenError: "invalid_grant", "invalid_client", "expired_code",
// "token_error", "id_token_verification_failed"
// TimeoutError: "timeout"
// NetworkError: "fetch_failed"Security Features
The JavaScript SDK implements multiple layers of security by default:
- PKCE (S256) — Proof Key for Code Exchange prevents authorization code interception attacks. A cryptographic code verifier/challenge pair is generated for every login flow.
- CSRF state parameter — A random state value is stored and validated on callback to prevent cross-site request forgery.
- ID token signature verification (v2) — RS256 ID tokens are verified against the JWKS endpoint, validating issuer, audience, expiry, and nonce. Enabled by default.
- Nonce validation — A unique nonce is included in the authorization request and validated against the ID token to prevent replay attacks.
- Server-side token revocation — On logout, the SDK calls the revocation endpoint to invalidate the refresh token server-side before clearing local state.
- Automatic token refresh — Access tokens are refreshed automatically with a 30-second buffer before expiry, so API calls never fail due to token expiration.
- OIDC Discovery caching — The SDK fetches and caches the
/.well-known/openid-configurationdocument with a 1-hour TTL, reducing network requests. - Pluggable token storage (v2) — Choose
memory(most secure, lost on refresh),sessionStorage(default, per-tab), orlocalStorage(persists across tabs/sessions) depending on your threat model. - Cross-tab sync (v2) — Login/logout events are broadcast to all tabs via BroadcastChannel so sessions stay consistent.
Full Example
// app.js
import { UniAuth, AuthorizationError, TokenError, TimeoutError } from '@uniauth/js';
const auth = new UniAuth({
issuer: 'https://uniauth.id',
clientId: 'your-client-id',
redirectUri: window.location.origin + '/callback',
postLogoutRedirectUri: window.location.origin,
scopes: ['openid', 'profile', 'email'],
storage: 'sessionStorage',
verifyIdToken: true,
crossTabSync: true,
});
// Login button
document.getElementById('login-btn').addEventListener('click', () => {
auth.login();
});
// Logout button — revokes tokens server-side and redirects
document.getElementById('logout-btn').addEventListener('click', () => {
auth.logout();
});
// Listen for cross-tab auth events
auth.onSync((event) => {
if (event === 'logout') {
window.location.href = '/';
}
});
// Check authentication on page load
async function init() {
// Handle callback if we're on the callback page
if (window.location.pathname === '/callback') {
try {
const tokens = await auth.handleCallback();
// Access verified ID token claims
const claims = auth.getIdTokenClaims();
console.log('Authenticated with ACR:', claims?.acr);
window.location.href = '/dashboard';
} catch (err) {
if (err instanceof AuthorizationError) {
document.body.textContent = 'Authorization failed: ' + err.message;
} else if (err instanceof TokenError) {
document.body.textContent = 'Token exchange failed: ' + err.message;
} else if (err instanceof TimeoutError) {
document.body.textContent = 'Request timed out. Please try again.';
}
}
return;
}
// Check if user is logged in
if (auth.isAuthenticated()) {
const user = await auth.getUser();
document.getElementById('user-name').textContent = user.name;
document.getElementById('login-btn').style.display = 'none';
document.getElementById('logout-btn').style.display = 'block';
}
}
init();TypeScript Interfaces
The JavaScript SDK ships with full TypeScript definitions. Here are the key interfaces:
interface UniAuthConfig {
issuer: string; // Your UniAuth instance URL (e.g., "https://uniauth.id")
clientId: string; // Your application's client ID
redirectUri: string; // OAuth callback URL
scopes?: string[]; // Scope array (default: ["openid", "profile", "email"])
postLogoutRedirectUri?: string; // Where to redirect after logout()
storage?: "memory" | "sessionStorage" | "localStorage"; // Token storage backend (default: "sessionStorage")
verifyIdToken?: boolean; // Verify ID token RS256 signature via JWKS (default: true)
crossTabSync?: boolean; // Sync login/logout across tabs (default: true)
organization?: string; // Organization ID for multi-tenant flows
retry?: boolean | { maxRetries?: number }; // Auto-retry on network failure
customParams?: Record<string, string>; // Extra params sent to authorize endpoint
}
interface LoginOptions {
prompt?: string; // "none" | "login" | "consent" | "select_account"
loginHint?: string; // Pre-fill the email/username field
maxAge?: number; // Max seconds since last authentication
acrValues?: string | string[]; // Requested authentication context class
organization?: string; // Override organization for this login
scope?: string; // Override scopes for this login (space-delimited)
customParams?: Record<string, string>; // Additional authorize parameters
}
interface TokenSet {
access_token: string; // JWT access token (Bearer)
id_token?: string; // RS256-signed ID token with user claims
refresh_token?: string; // Opaque refresh token
token_type: string; // Always "Bearer"
expires_in: number; // Seconds until access_token expires (default: 3600)
expires_at: number; // Millisecond timestamp when access_token expires
scope?: string; // Space-delimited granted scopes
}
interface IdTokenClaims {
sub: string; // Pairwise subject identifier (unique per app)
iss: string; // Issuer URL
aud: string | string[]; // Audience (your client_id)
exp: number; // Expiry (Unix timestamp)
iat: number; // Issued-at (Unix timestamp)
nonce?: string; // Nonce from the authorization request
auth_time?: number; // Time of original authentication
acr?: string; // Authentication context class reference
amr?: string[]; // Authentication methods (e.g., ["pwd", "otp"])
sid?: string; // Session ID
groups?: string[]; // User's group memberships (requires "groups" scope)
at_hash?: string; // Access token hash
[key: string]: unknown; // Custom claims
}
interface UserInfo {
sub: string; // Pairwise subject identifier (unique per app)
name?: string; // Full display name
given_name?: string; // First name
family_name?: string; // Last name
preferred_username?: string; // Username
nickname?: string; // Nickname
picture?: string; // Avatar URL
profile?: string; // Profile URL
website?: string; // Website URL
gender?: string; // Gender
birthdate?: string; // Date of birth (YYYY-MM-DD)
locale?: string; // Locale (e.g., "en-US")
zoneinfo?: string; // Timezone (e.g., "America/New_York")
updated_at?: number; // Last profile update timestamp
email?: string; // Email address (requires "email" scope)
email_verified?: boolean; // Whether the email has been verified
phone_number?: string; // Phone number (requires "phone" scope)
phone_number_verified?: boolean;
groups?: string[]; // Group memberships (requires "groups" scope)
address?: { // Structured address (requires "address" scope)
street_address?: string;
locality?: string;
region?: string;
postal_code?: string;
country?: string;
};
}React SDK (@uniauth/react)
The React SDK wraps the JavaScript SDK with React-specific hooks and components for a seamless developer experience. It provides context-based state management, automatic token refresh, ID token claims access, organization support, and pre-built UI components.
Installation
npm install @uniauth/react @uniauth/jsUniAuthProvider
Wrap your application with the UniAuthProvider at the root level. This initializes the SDK and provides authentication context to all child components.
// App.tsx or main.tsx
import { UniAuthProvider } from '@uniauth/react';
function App() {
return (
<UniAuthProvider
issuer="https://uniauth.id"
clientId="your-client-id"
redirectUri={window.location.origin + '/callback'}
scope="openid profile email"
storage="sessionStorage" // v2: "memory" | "sessionStorage" | "localStorage"
verifyIdToken={true} // v2: RS256 ID token verification (default: true)
crossTabSync={true} // v2: sync login/logout across tabs (default: true)
postLogoutRedirectUri={window.location.origin}
// organization="org_abc123" // v2: multi-tenant org context
// onError={(err) => console.error(err)} // v2: global error handler
>
<Router>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/callback" element={<CallbackPage />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Router>
</UniAuthProvider>
);
}The provider accepts the following props:
| Prop | Type | Default | Description |
|---|---|---|---|
| issuer | string | — | UniAuth issuer URL (required) |
| clientId | string | — | OAuth client ID (required) |
| redirectUri | string | — | Callback URL (required) |
| scope | string | "openid profile email" | Space-delimited scopes |
| storage | string | "sessionStorage" | Token storage backend |
| verifyIdToken | boolean | true | Verify ID token RS256 signature |
| crossTabSync | boolean | true | Sync auth events across tabs |
| organization | string | — | Organization ID for multi-tenant |
| postLogoutRedirectUri | string | — | Redirect after logout |
| onError | (error: Error) => void | — | Global error callback |
useUniAuth Hook
The primary hook for accessing authentication state and methods:
import { useUniAuth } from '@uniauth/react';
function Header() {
const {
isAuthenticated, // boolean
isLoading, // boolean — true while checking auth status
user, // UserInfo | null
tokens, // TokenSet | null
idTokenClaims, // IdTokenClaims | null (v2 — verified claims)
organization, // string | null (v2 — current org context)
login, // (options?: LoginOptions) => Promise<void>
logout, // () => Promise<void> — revokes + redirects
getAccessToken, // () => Promise<string | null>
refreshToken, // () => Promise<void>
error, // Error | null
} = useUniAuth();
if (isLoading) return <div>Loading...</div>;
return (
<header>
{isAuthenticated ? (
<div>
<span>Welcome, {user?.name}</span>
<button onClick={() => logout()}>Log out</button>
</div>
) : (
<button onClick={() => login()}>Log in with UniAuth</button>
)}
</header>
);
}useUser Hook
A convenience hook that returns just the user profile:
import { useUser } from '@uniauth/react';
function Profile() {
const { user, isLoading, isAuthenticated } = useUser();
if (isLoading) return <div>Loading profile...</div>;
if (!user) return <div>Not logged in</div>;
return (
<div>
<img src={user.picture} alt={user.name} />
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}useAccessToken Hook (v2)
A convenience hook for making authenticated API calls:
import { useAccessToken } from '@uniauth/react';
function ApiCaller() {
const { getAccessToken, accessToken, isAuthenticated } = useAccessToken();
const fetchData = async () => {
const token = await getAccessToken(); // auto-refreshes if expired
const res = await fetch('/api/data', {
headers: { Authorization: `Bearer ${token}` }
});
return res.json();
};
return (
<button onClick={fetchData} disabled={!isAuthenticated}>
Fetch Data
</button>
);
}useOrganization Hook (v2)
Access the current organization context for multi-tenant applications:
import { useOrganization } from '@uniauth/react';
function OrgInfo() {
const { organization, orgFromToken } = useOrganization();
// organization: from provider props or ID token org_id
// orgFromToken: the raw org_id claim from the ID token
return <div>Current org: {organization || 'None'}</div>;
}useIdTokenClaims Hook (v2)
Access the verified ID token claims directly, useful for reading ACR, AMR, groups, and session info:
import { useIdTokenClaims } from '@uniauth/react';
function SecurityInfo() {
const claims = useIdTokenClaims();
if (!claims) return null;
return (
<div>
<p>Auth level: {claims.acr}</p>
<p>Methods: {claims.amr?.join(', ')}</p>
<p>Groups: {claims.groups?.join(', ')}</p>
<p>Session: {claims.sid}</p>
</div>
);
}Pre-Built Components
LoginButton
import { LoginButton } from '@uniauth/react';
// Renders a button that triggers the login flow
<LoginButton>Sign in with UniAuth</LoginButton>
// With custom styling
<LoginButton className="btn btn-primary">
Sign in
</LoginButton>
// With v2 login options
<LoginButton loginOptions={{ prompt: 'consent', acrValues: 'urn:uniauth:acr:mfa' }}>
Sign in (MFA required)
</LoginButton>LogoutButton
import { LogoutButton } from '@uniauth/react';
<LogoutButton>Sign out</LogoutButton>
// With custom styling
<LogoutButton className="btn btn-secondary">
Log out
</LogoutButton>ProtectedRoute
import { ProtectedRoute } from '@uniauth/react';
// Redirects to login if not authenticated
<Route
path="/dashboard"
element={
<ProtectedRoute fallback={<div>Loading...</div>}>
<Dashboard />
</ProtectedRoute>
}
/>
// With v2 login options for the redirect
<ProtectedRoute
loginOptions={{ acrValues: 'urn:uniauth:acr:mfa' }}
fallback={<div>Verifying identity...</div>}
>
<AdminPanel />
</ProtectedRoute>UserProfile
import { UserProfile } from '@uniauth/react';
// Renders avatar, name, and email when authenticated
<UserProfile className="flex items-center gap-3" />Callback Page
The UniAuthProvider automatically detects when the user lands on the callback URL (by matching the redirectUri) and exchanges the authorization code for tokens. You do not need to call handleCallback manually. Simply render a loading state and redirect once authentication completes:
import { useUniAuth } from '@uniauth/react';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
function CallbackPage() {
const { isAuthenticated, isLoading, error } = useUniAuth();
const navigate = useNavigate();
useEffect(() => {
if (isAuthenticated) {
navigate('/dashboard');
}
}, [isAuthenticated]);
if (error) return <div>Error: {error.message}</div>;
if (isLoading) return <div>Completing login...</div>;
return null;
}Full React Example
// main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import {
UniAuthProvider, ProtectedRoute,
useUniAuth, useUser, useIdTokenClaims
} from '@uniauth/react';
function Home() {
const { isAuthenticated, login } = useUniAuth();
return (
<div>
<h1>My App</h1>
{!isAuthenticated && (
<button onClick={() => login()}>Sign in with UniAuth</button>
)}
</div>
);
}
function Dashboard() {
const { user } = useUser();
const { logout, getAccessToken } = useUniAuth();
const claims = useIdTokenClaims();
const fetchData = async () => {
const token = await getAccessToken();
const res = await fetch('/api/data', {
headers: { Authorization: `Bearer ${token}` }
});
return res.json();
};
return (
<div>
<h1>Dashboard</h1>
<p>Welcome, {user?.name} ({user?.email})</p>
{claims?.acr && <p>Auth level: {claims.acr}</p>}
{claims?.groups && <p>Groups: {claims.groups.join(', ')}</p>}
<button onClick={() => logout()}>Sign out</button>
</div>
);
}
function Callback() {
const { isAuthenticated, isLoading } = useUniAuth();
React.useEffect(() => {
if (isAuthenticated) window.location.href = '/dashboard';
}, [isAuthenticated]);
if (isLoading) return <p>Logging in...</p>;
return null;
}
ReactDOM.createRoot(document.getElementById('root')!).render(
<UniAuthProvider
issuer="https://uniauth.id"
clientId="your-client-id"
redirectUri={window.location.origin + '/callback'}
scope="openid profile email groups"
verifyIdToken={true}
crossTabSync={true}
postLogoutRedirectUri={window.location.origin}
>
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/callback" element={<Callback />} />
<Route path="/dashboard" element={
<ProtectedRoute><Dashboard /></ProtectedRoute>
} />
</Routes>
</BrowserRouter>
</UniAuthProvider>
);Error Handling Patterns
Handle authentication errors gracefully in your callback page. The provider automatically processes the callback and exposes any errors through the error property on the useUniAuth hook. You can also use the onError prop on the provider for centralized error handling:
import { useUniAuth } from '@uniauth/react';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
function CallbackPage() {
const { isAuthenticated, isLoading, error } = useUniAuth();
const navigate = useNavigate();
useEffect(() => {
if (isAuthenticated) {
navigate('/dashboard');
}
}, [isAuthenticated]);
if (error) {
return (
<div>
<h2>Authentication Failed</h2>
<p>{error.message}</p>
<button onClick={() => navigate('/login')}>
Try again
</button>
</div>
);
}
if (isLoading) return <div>Completing login...</div>;
return null;
}SSR / Hydration Notes
The UniAuth React SDK uses the configured storage backend internally to persist PKCE verifiers and state across redirects. In server-side rendering environments (such as Next.js), the provider handles hydration gracefully — it returns isLoading: true during server rendering and resolves authentication state after hydration on the client. When using memory storage, tokens are not persisted across page refreshes.
For server-side token exchange in Next.js API routes or server components, use the JavaScript SDK directly rather than the React hooks:
// app/api/auth/callback/route.ts (Next.js App Router)
import { UniAuth } from '@uniauth/js';
export async function GET(request: Request) {
const url = new URL(request.url);
const code = url.searchParams.get('code');
const codeVerifier = cookies().get('pkce_verifier')?.value;
const auth = new UniAuth({
issuer: 'https://uniauth.id',
clientId: process.env.UNIAUTH_CLIENT_ID!,
redirectUri: process.env.UNIAUTH_REDIRECT_URI!,
storage: 'memory', // Server-side: use memory storage
verifyIdToken: true, // Verify ID token on the server too
});
const tokens = await auth.exchangeCode(code, codeVerifier);
// Store tokens in httpOnly cookies and redirect to dashboard
}Error Boundary
Wrap your application with an error boundary to catch unexpected authentication errors from the UniAuth provider:
import React from 'react';
class AuthErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// Log to your error tracking service
console.error('Auth error boundary caught:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div style={{ padding: '2rem', textAlign: 'center' }}>
<h2>Authentication Error</h2>
<p>Something went wrong with authentication.</p>
<button onClick={() => {
this.setState({ hasError: false });
window.location.href = '/';
}}>
Return to Home
</button>
</div>
);
}
return this.props.children;
}
}
// Usage: Wrap around UniAuthProvider
function App() {
return (
<AuthErrorBoundary>
<UniAuthProvider
issuer="https://uniauth.id"
clientId="your-client-id"
redirectUri={window.location.origin + '/callback'}
verifyIdToken={true}
crossTabSync={true}
onError={(err) => console.error('Auth error:', err)}
>
<YourApp />
</UniAuthProvider>
</AuthErrorBoundary>
);
}PHP SDK (UniAuth.php)
A single-file PHP SDK with zero dependencies (requires PHP 8.0+ with the curl extension). It handles PKCE, CSRF protection, and token exchange in a simple procedural API.
Installation
Download the SDK file and include it in your project:
# Download from UniAuth
curl -o UniAuth.php https://uniauth.id/sdk/UniAuth.php
# Or download from the UniAuth docs pageSetup
<?php
require_once 'UniAuth.php';
$ua = new UniAuth(
clientId: 'your_client_id',
clientSecret: 'your_client_secret',
redirectUri: 'https://yoursite.com/callback.php',
// Optional: custom UniAuth server URL (defaults to https://uniauth.id)
// baseUrl: 'https://your-uniauth-instance.com'
);Login Page
<?php
// login.php
require_once 'UniAuth.php';
$ua = new UniAuth(
clientId: 'your_client_id',
clientSecret: 'your_client_secret',
redirectUri: 'https://yoursite.com/callback.php'
);
// This generates a PKCE challenge, stores the verifier in the session,
// generates a CSRF state token, and returns the full authorization URL.
$loginUrl = $ua->getLoginUrl();
// Redirect the user
header('Location: ' . $loginUrl);
exit;Callback Page
<?php
// callback.php
require_once 'UniAuth.php';
$ua = new UniAuth(
clientId: 'your_client_id',
clientSecret: 'your_client_secret',
redirectUri: 'https://yoursite.com/callback.php'
);
try {
// Exchange authorization code for user info (one line!)
// Internally: verifies CSRF state, sends code + code_verifier to token endpoint,
// fetches user profile from userinfo endpoint
$user = $ua->handleCallback($_GET['code']);
// $user contains OIDC claims:
// [
// 'sub' => 'pairwise-subject-id',
// 'email' => '[email protected]',
// 'email_verified' => true,
// 'name' => 'John Doe',
// 'given_name' => 'John',
// 'family_name' => 'Doe',
// 'picture' => 'https://uniauth.id/api/avatar/...',
// '_tokens' => [
// 'access_token' => 'eyJ...',
// 'refresh_token' => '...',
// 'id_token' => 'eyJ...',
// 'expires_in' => 3600,
// ]
// ]
// Create your local session
session_start();
$_SESSION['user'] = $user;
// Redirect to your app
header('Location: /dashboard');
exit;
} catch (Exception $e) {
// Handle error (invalid code, CSRF mismatch, etc.)
echo 'Login failed: ' . $e->getMessage();
}Logout
<?php
// logout.php
session_start();
// Get the ID token before destroying the session
$idToken = $_SESSION['user']['_tokens']['id_token'] ?? null;
// Destroy local session
session_destroy();
// Redirect to UniAuth end-session endpoint (optional)
if ($idToken) {
$logoutUrl = 'https://uniauth.id/api/oauth/end-session?'
. http_build_query([
'id_token_hint' => $idToken,
'post_logout_redirect_uri' => 'https://yoursite.com',
]);
header('Location: ' . $logoutUrl);
exit;
}
header('Location: /');
exit;PHP v2.0 Features
PHP SDK v2.0 adds typed exceptions, a full logout() method that handles server-side revocation, token expiry checks, ID token parsing, and max_age / nonce options on getLoginUrl().
Typed Exceptions
All errors thrown by the SDK are instances of UniAuthException with specific subclasses for each error category:
<?php
use UniAuthException;
use UniAuthStateException; // CSRF state / nonce mismatch
use UniAuthTokenException; // Token exchange or refresh failure
use UniAuthApiException; // UserInfo or revocation API errors
try {
$user = $ua->handleCallback($_GET['code']);
} catch (UniAuthStateException $e) {
// CSRF state parameter mismatch — possible MITM attack
error_log('State mismatch: ' . $e->getMessage());
header('Location: /login?error=state_mismatch');
exit;
} catch (UniAuthTokenException $e) {
// Token endpoint returned an error (invalid_grant, expired code, etc.)
error_log('Token error: ' . $e->getMessage());
header('Location: /login?error=token_failed');
exit;
} catch (UniAuthApiException $e) {
// UserInfo endpoint error
error_log('API error: ' . $e->getMessage());
header('Location: /login?error=api_failed');
exit;
}Full Logout (Revoke + Destroy + Redirect)
The logout() method revokes the refresh token server-side, destroys the PHP session, and returns the end-session URL for redirect:
<?php
session_start();
$tokens = $_SESSION['user']['_tokens'];
// Revokes refresh token, destroys session, returns logout URL
$logoutUrl = $ua->logout($tokens, 'https://yourapp.com');
header('Location: ' . $logoutUrl);
exit;Token Expiry and Refresh
<?php
$tokens = $_SESSION['user']['_tokens'];
// Check if the access token has expired
if ($ua->isTokenExpired($tokens)) {
// Refresh the access token — note: refresh tokens rotate on each use,
// so you must store the new tokens returned by refreshAccessToken()
$newTokens = $ua->refreshAccessToken($tokens['refresh_token']);
// IMPORTANT: The old refresh token is now invalid.
// Always persist the new token set.
$_SESSION['user']['_tokens'] = $newTokens;
}
// Parse ID token claims without verification (for display purposes)
$claims = $ua->parseIdToken($tokens['id_token']);
echo 'Hello, ' . $claims['name'];
// Revoke a specific token (e.g., on account deletion)
$ua->revokeToken($tokens['refresh_token']);Login URL Options (max_age, nonce)
<?php
// Force re-authentication if the user authenticated more than 5 minutes ago
$loginUrl = $ua->getLoginUrl([
'max_age' => 300, // Seconds since last authentication
'nonce' => bin2hex(random_bytes(16)), // Custom nonce for ID token replay protection
'prompt' => 'consent', // Force consent screen
]);
header('Location: ' . $loginUrl);
exit;Account Linking
To link UniAuth users with your existing user database, use the pairwise sub claim as a stable identifier:
<?php
// After successful callback
$user = $ua->handleCallback($_GET['code']);
$uniAuthSub = $user['sub']; // Pairwise subject identifier
// Check if user exists in your database
$stmt = $pdo->prepare('SELECT * FROM users WHERE uniauth_id = ?');
$stmt->execute([$uniAuthSub]);
$localUser = $stmt->fetch();
if ($localUser) {
// Existing user — log them in
$_SESSION['user_id'] = $localUser['id'];
} else {
// New user — create account
$stmt = $pdo->prepare(
'INSERT INTO users (email, name, uniauth_id) VALUES (?, ?, ?)'
);
$stmt->execute([
$user['email'],
$user['name'],
$uniAuthSub
]);
$_SESSION['user_id'] = $pdo->lastInsertId();
}Laravel Integration
Integrate the PHP SDK with Laravel using middleware for route protection:
<?php
// app/Http/Middleware/UniAuthMiddleware.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class UniAuthMiddleware
{
public function handle(Request $request, Closure $next)
{
if (!session()->has('uniauth_user')) {
// Store the intended URL for post-login redirect
session()->put('url.intended', $request->url());
return redirect('/auth/login');
}
// Make the UniAuth user available to all views and controllers
view()->share('authUser', session('uniauth_user'));
return $next($request);
}
}<?php
// routes/web.php
use Illuminate\Support\Facades\Route;
Route::get('/auth/login', function () {
$ua = new \UniAuth(
clientId: config('services.uniauth.client_id'),
clientSecret: config('services.uniauth.client_secret'),
redirectUri: config('services.uniauth.redirect_uri'),
);
return redirect($ua->getLoginUrl());
});
Route::get('/auth/callback', function () {
$ua = new \UniAuth(
clientId: config('services.uniauth.client_id'),
clientSecret: config('services.uniauth.client_secret'),
redirectUri: config('services.uniauth.redirect_uri'),
);
try {
$user = $ua->handleCallback(request('code'));
session(['uniauth_user' => $user]);
return redirect()->intended('/dashboard');
} catch (\Exception $e) {
return redirect('/login')->with('error', 'Authentication failed.');
}
});
Route::middleware('uniauth')->group(function () {
Route::get('/dashboard', function () {
return view('dashboard');
});
});{{-- resources/views/dashboard.blade.php --}}
@extends('layouts.app')
@section('content')
<h1>Welcome, {{ $authUser['name'] }}</h1>
<p>Email: {{ $authUser['email'] }}</p>
<p>UniAuth ID: {{ $authUser['sub'] }}</p>
<a href="/auth/logout">Sign out</a>
@endsectionSDK Comparison
| Feature | JS SDK | React SDK | PHP SDK |
|---|---|---|---|
| PKCE (S256) | Yes | Yes | Yes |
| CSRF protection | Yes | Yes | Yes |
| ID token verification | Yes (RS256 via JWKS) | Yes (RS256 via JWKS) | No (parse only) |
| Token refresh | Automatic | Automatic | Manual |
| Token storage | Memory / sessionStorage / localStorage | Memory / sessionStorage / localStorage | PHP Session |
| Cross-tab sync | Yes (BroadcastChannel) | Yes (BroadcastChannel) | N/A |
| Pluggable storage | Yes | Yes (via prop) | N/A |
| Organizations | Yes | Yes | No |
| SSR support | Yes | Yes | N/A (server-only) |
| Dependencies | None | @uniauth/js, React 18+ | None (PHP 8+, curl) |
| UI components | No | Yes | No |
Migrating from v1.0 to v2.0
SDK v1.0 is deprecated. Here are the breaking changes when upgrading to v2.0:
JS SDK
- scope → scopes — The
scopeconfig option (space-delimited string) is nowscopes(string array). Changescope: "openid profile email"toscopes: ["openid", "profile", "email"]. - ID token verification on by default —
handleCallback()now verifies the ID token signature against the JWKS endpoint. If your server does not expose/.well-known/jwks.json, setverifyIdToken: false. - New error types — Import
TimeoutErrorif you use the retry feature.
React SDK
- Provider props — The provider now accepts
issuer(same name as JS SDK). All other existing props remain unchanged. - New hooks —
useAccessToken(),useOrganization(), anduseIdTokenClaims()are new additions. Existing hooks (useUniAuth,useUser) are unchanged. - LoginButton — Now accepts an optional
loginOptionsprop. Existing usage is unaffected.
Minimal upgrade
// v1.0
const auth = new UniAuth({
issuer: 'https://uniauth.id',
clientId: 'my-app',
redirectUri: '/callback',
scope: 'openid profile email',
});
// v2.0 — just rename scope → scopes (array)
const auth = new UniAuth({
issuer: 'https://uniauth.id',
clientId: 'my-app',
redirectUri: '/callback',
scopes: ['openid', 'profile', 'email'],
});v1.0 source code is archived on the v1-archived branch of the SDK repository. It will not receive updates or security patches.