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, or localStorage depending 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 organization parameter in config and login options.
  • ID token claims access — New getIdTokenClaims() method and useIdTokenClaims() hook to read verified claims (ACR, AMR, groups, auth_time, session ID).
  • Scopes as array — JS SDK accepts scopes as 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 optionslogin() accepts maxAge, acrValues, organization, and per-call scope overrides.
  • New error typeTimeoutError for request timeout failures, separate from NetworkError.
  • Lifecycle managementdestroy() method to clean up event listeners and BroadcastChannel when unmounting.
  • New React hooksuseAccessToken(), useOrganization(), and useIdTokenClaims().

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

Setup

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 claims

Logout

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-configuration document with a 1-hour TTL, reducing network requests.
  • Pluggable token storage (v2) — Choose memory (most secure, lost on refresh), sessionStorage (default, per-tab), or localStorage (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/js

UniAuthProvider

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:

PropTypeDefaultDescription
issuerstringUniAuth issuer URL (required)
clientIdstringOAuth client ID (required)
redirectUristringCallback URL (required)
scopestring"openid profile email"Space-delimited scopes
storagestring"sessionStorage"Token storage backend
verifyIdTokenbooleantrueVerify ID token RS256 signature
crossTabSyncbooleantrueSync auth events across tabs
organizationstringOrganization ID for multi-tenant
postLogoutRedirectUristringRedirect after logout
onError(error: Error) => voidGlobal 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 page

Setup

<?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>
@endsection

SDK Comparison

FeatureJS SDKReact SDKPHP SDK
PKCE (S256)YesYesYes
CSRF protectionYesYesYes
ID token verificationYes (RS256 via JWKS)Yes (RS256 via JWKS)No (parse only)
Token refreshAutomaticAutomaticManual
Token storageMemory / sessionStorage / localStorageMemory / sessionStorage / localStoragePHP Session
Cross-tab syncYes (BroadcastChannel)Yes (BroadcastChannel)N/A
Pluggable storageYesYes (via prop)N/A
OrganizationsYesYesNo
SSR supportYesYesN/A (server-only)
DependenciesNone@uniauth/js, React 18+None (PHP 8+, curl)
UI componentsNoYesNo

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 scope config option (space-delimited string) is now scopes (string array). Change scope: "openid profile email" to scopes: ["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, set verifyIdToken: false.
  • New error types — Import TimeoutError if 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(), and useIdTokenClaims() are new additions. Existing hooks (useUniAuth, useUser) are unchanged.
  • LoginButton — Now accepts an optional loginOptions prop. 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.