React Quickstart

Add "Sign in with UniAuth" to your React application in minutes. This guide walks you through installing the React SDK, setting up the provider, using authentication hooks, and protecting routes.

Prerequisites

OAuth App Setup: In the Developer Console, set the Redirect URI to http://localhost:3000/callback for local development. You can add additional redirect URIs for staging and production later.

Installation

Install the UniAuth React SDK and its peer dependency:

npm install @uniauth/react @uniauth/js

Or with yarn:

yarn add @uniauth/react @uniauth/js

Provider Setup

Wrap your application with the UniAuthProvider at the root level. This initializes the SDK, manages authentication state, and provides context to all child components.

// src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { UniAuthProvider } from '@uniauth/react';
import App from './App';

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <UniAuthProvider
      domain="https://uniauth.id"
      clientId="your-client-id"
      redirectUri="http://localhost:3000/callback"
      scope="openid profile email"
    >
      <BrowserRouter>
        <App />
      </BrowserRouter>
    </UniAuthProvider>
  </React.StrictMode>
);

Provider Props

PropTypeRequiredDescription
domainstringYesYour UniAuth domain (e.g., https://uniauth.id)
clientIdstringYesYour OAuth application's client ID from the Developer Console
redirectUristringYesThe URL where UniAuth redirects after authentication. Must match a registered redirect URI.
scopestringNoSpace-separated OAuth scopes. Defaults to openid profile email

Using the Hook

The useUniAuth() hook is your primary interface for interacting with authentication state and actions. It must be called within a component that is a descendant of UniAuthProvider.

import { useUniAuth } from '@uniauth/react';

function Header() {
  const {
    user,              // UserInfo | null — the authenticated user's profile
    isAuthenticated,   // boolean — true when user is signed in
    isLoading,         // boolean — true during initial auth check
    error,             // Error | null — the last authentication error
    login,             // (options?) => Promise<void> — redirect to UniAuth login
    logout,            // () => Promise<void> — sign out and redirect to end-session
    getAccessToken,    // () => Promise<string | null> — get a valid access token
    refreshToken,      // () => Promise<void> — manually refresh the token
  } = useUniAuth();

  if (isLoading) return <div>Loading...</div>;

  return (
    <header>
      {isAuthenticated ? (
        <div>
          <span>Welcome, {user?.name}</span>
          <button onClick={() => logout()}>Sign out</button>
        </div>
      ) : (
        <button onClick={() => login()}>Sign in with UniAuth</button>
      )}
    </header>
  );
}

Hook Return Values

PropertyTypeDescription
userUserInfo | nullThe authenticated user's profile (sub, name, email, picture, etc.)
isAuthenticatedbooleanTrue when the user is signed in and tokens are valid
isLoadingbooleanTrue during initialization (checking stored tokens or handling callback)
errorError | nullSet if the last authentication operation failed
login(options?)functionRedirects the user to UniAuth for sign-in. Accepts optional { scope }
logout()functionClears local session and redirects to UniAuth end-session endpoint
getAccessToken()functionReturns a valid access token, automatically refreshing if expired
refreshToken()functionManually triggers a token refresh

There is also a convenience hook useUser() that returns just user, isLoading, and isAuthenticated:

import { useUser } from '@uniauth/react';

function ProfileCard() {
  const { user, isLoading, isAuthenticated } = useUser();

  if (isLoading) return <div>Loading profile...</div>;
  if (!isAuthenticated) return null;

  return (
    <div>
      {user?.picture && <img src={user.picture} alt={user.name || ''} />}
      <h2>{user?.name}</h2>
      <p>{user?.email}</p>
    </div>
  );
}

Pre-Built Components

The React SDK includes ready-to-use components for common authentication UI patterns. All components accept className and style props for custom styling.

LoginButton

Renders a button that triggers the UniAuth login flow when clicked:

import { LoginButton } from '@uniauth/react';

// Default label: "Sign in with UniAuth"
<LoginButton />

// Custom label and styling
<LoginButton className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
  Log in
</LoginButton>

LogoutButton

Renders a button that signs the user out and redirects to UniAuth's end-session endpoint:

import { LogoutButton } from '@uniauth/react';

// Default label: "Sign out"
<LogoutButton />

// Custom label
<LogoutButton className="text-red-600 hover:text-red-700">
  Sign out of my account
</LogoutButton>

ProtectedRoute

Wraps content that requires authentication. If the user is not signed in, they are automatically redirected to the UniAuth login page:

import { ProtectedRoute } from '@uniauth/react';

// Basic usage — redirects to login if not authenticated
<ProtectedRoute>
  <Dashboard />
</ProtectedRoute>

// With a custom loading fallback
<ProtectedRoute fallback={<div className="animate-pulse">Checking authentication...</div>}>
  <Dashboard />
</ProtectedRoute>

// With custom scopes for the login redirect
<ProtectedRoute loginOptions={{ scope: 'openid profile email phone' }}>
  <AdminPanel />
</ProtectedRoute>

UserProfile

Displays the authenticated user's avatar, name, and email. Returns null when not authenticated:

import { UserProfile } from '@uniauth/react';

// Renders avatar + name + email
<UserProfile className="flex items-center gap-3" />

Callback Page

After the user signs in at UniAuth, they are redirected back to your redirectUri with an authorization code. You need a callback page that exchanges this code for tokens. The SDK handles PKCE verification and token exchange automatically:

// src/pages/Callback.tsx
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useUniAuth } from '@uniauth/react';

export default function CallbackPage() {
  const { isAuthenticated, isLoading, error } = useUniAuth();
  const navigate = useNavigate();

  useEffect(() => {
    // The provider automatically handles the callback on mount
    // when it detects ?code= and ?state= in the URL
    if (isAuthenticated) {
      navigate('/dashboard', { replace: true });
    }
  }, [isAuthenticated, navigate]);

  if (error) {
    return (
      <div className="text-center py-12">
        <h2 className="text-xl font-semibold text-red-600">Authentication Failed</h2>
        <p className="mt-2 text-gray-600">{error.message}</p>
        <a href="/" className="mt-4 inline-block text-blue-600 underline">Return home</a>
      </div>
    );
  }

  return (
    <div className="text-center py-12">
      <p className="text-gray-600">Signing you in...</p>
    </div>
  );
}
How it works: When UniAuthProvider mounts and detects code and state query parameters in the URL, it automatically initiates the token exchange. It verifies the CSRF state parameter, sends the authorization code along with the PKCE code verifier to UniAuth's token endpoint, fetches the user profile from the UserInfo endpoint, and updates the authentication state. The URL is cleaned up after the exchange completes.

Making Authenticated API Calls

Use getAccessToken() to retrieve a valid access token for API requests. The SDK automatically refreshes the token if it has expired:

import { useUniAuth } from '@uniauth/react';

function DataFetcher() {
  const { getAccessToken } = useUniAuth();
  const [data, setData] = useState(null);

  const fetchProtectedData = async () => {
    const token = await getAccessToken();
    if (!token) {
      console.error('Not authenticated');
      return;
    }

    const response = await fetch('https://api.yourapp.com/data', {
      headers: {
        Authorization: `Bearer ${token}`,
        'Content-Type': 'application/json',
      },
    });

    if (response.ok) {
      setData(await response.json());
    }
  };

  return (
    <div>
      <button onClick={fetchProtectedData}>Load Data</button>
      {data && <pre>{JSON.stringify(data, null, 2)}</pre>}
    </div>
  );
}

On your backend, validate the access token by calling UniAuth's UserInfo endpoint or by verifying the JWT signature using the keys from the JWKS endpoint:

// Backend validation (Node.js / Express example)
app.get('/api/data', async (req, res) => {
  const token = req.headers.authorization?.replace('Bearer ', '');
  if (!token) return res.status(401).json({ error: 'Missing token' });

  // Option 1: Validate via UserInfo endpoint
  const userRes = await fetch('https://uniauth.id/api/oauth/userinfo', {
    headers: { Authorization: `Bearer ${token}` },
  });

  if (!userRes.ok) return res.status(401).json({ error: 'Invalid token' });

  const user = await userRes.json();
  res.json({ message: `Hello, ${user.name}`, data: { /* ... */ } });
});

Protected Routes

Use the ProtectedRoute component with React Router to protect entire routes:

import { Routes, Route } from 'react-router-dom';
import { ProtectedRoute } from '@uniauth/react';

function App() {
  return (
    <Routes>
      {/* Public routes */}
      <Route path="/" element={<HomePage />} />
      <Route path="/callback" element={<CallbackPage />} />
      <Route path="/about" element={<AboutPage />} />

      {/* Protected routes — redirect to login if not authenticated */}
      <Route path="/dashboard" element={
        <ProtectedRoute fallback={<LoadingSpinner />}>
          <DashboardPage />
        </ProtectedRoute>
      } />
      <Route path="/settings" element={
        <ProtectedRoute>
          <SettingsPage />
        </ProtectedRoute>
      } />
      <Route path="/admin/*" element={
        <ProtectedRoute loginOptions={{ scope: 'openid profile email' }}>
          <AdminLayout />
        </ProtectedRoute>
      } />
    </Routes>
  );
}

SSR Considerations

The React SDK stores tokens in sessionStorage, which is not available during server-side rendering. The UniAuthProvider handles this gracefully:

  • During SSR, isLoading is true and user is null
  • After hydration in the browser, the SDK checks for stored tokens and updates state
  • Components using isLoading will render their loading state during SSR

For server-rendered applications (like Next.js) where you need server-side access to the user's identity, consider the server-side token exchange pattern described in the Next.js Quickstart.

User Profile Fields

The user object returned by the hook follows the OpenID Connect standard claims. The fields available depend on the scopes you requested:

FieldScopeDescription
subopenidUnique, app-specific identifier for the user
nameprofileFull display name
given_nameprofileFirst name
family_nameprofileLast name
pictureprofileAvatar URL
emailemailEmail address
email_verifiedemailWhether the email has been verified
phone_numberphonePhone number
addressaddressStructured address (street, city, region, postal code, country)

Complete Example

Here is a complete Vite + React Router application with login, callback, a protected dashboard, and logout:

// src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { UniAuthProvider } from '@uniauth/react';
import { HomePage } from './pages/Home';
import { CallbackPage } from './pages/Callback';
import { DashboardPage } from './pages/Dashboard';

const UNIAUTH_DOMAIN = 'https://uniauth.id';
const CLIENT_ID = 'your-client-id';
const REDIRECT_URI = window.location.origin + '/callback';

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <UniAuthProvider
      domain={UNIAUTH_DOMAIN}
      clientId={CLIENT_ID}
      redirectUri={REDIRECT_URI}
    >
      <BrowserRouter>
        <Routes>
          <Route path="/" element={<HomePage />} />
          <Route path="/callback" element={<CallbackPage />} />
          <Route path="/dashboard" element={<DashboardPage />} />
        </Routes>
      </BrowserRouter>
    </UniAuthProvider>
  </React.StrictMode>
);
// src/pages/Home.tsx
import { useUniAuth, LoginButton, UserProfile } from '@uniauth/react';
import { Link } from 'react-router-dom';

export function HomePage() {
  const { isAuthenticated, isLoading } = useUniAuth();

  if (isLoading) return <div>Loading...</div>;

  return (
    <div style={{ maxWidth: 600, margin: '0 auto', padding: '2rem' }}>
      <h1>My Application</h1>
      {isAuthenticated ? (
        <div>
          <UserProfile className="flex items-center gap-3 mb-4" />
          <Link to="/dashboard">Go to Dashboard</Link>
        </div>
      ) : (
        <div>
          <p>Sign in to access your dashboard.</p>
          <LoginButton className="px-4 py-2 bg-blue-600 text-white rounded-lg">
            Sign in with UniAuth
          </LoginButton>
        </div>
      )}
    </div>
  );
}
// src/pages/Callback.tsx
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useUniAuth } from '@uniauth/react';

export function CallbackPage() {
  const { isAuthenticated, error } = useUniAuth();
  const navigate = useNavigate();

  useEffect(() => {
    if (isAuthenticated) navigate('/dashboard', { replace: true });
  }, [isAuthenticated, navigate]);

  if (error) return <div>Login failed: {error.message}</div>;
  return <div>Signing you in...</div>;
}
// src/pages/Dashboard.tsx
import { useUniAuth, useUser, ProtectedRoute, LogoutButton } from '@uniauth/react';

function DashboardContent() {
  const { user } = useUser();
  const { getAccessToken } = useUniAuth();
  const [apiData, setApiData] = useState<string | null>(null);

  const callApi = async () => {
    const token = await getAccessToken();
    const res = await fetch('/api/protected', {
      headers: { Authorization: `Bearer ${token}` },
    });
    const data = await res.json();
    setApiData(JSON.stringify(data, null, 2));
  };

  return (
    <div style={{ maxWidth: 600, margin: '0 auto', padding: '2rem' }}>
      <h1>Dashboard</h1>
      <p>Welcome, {user?.name || user?.email}!</p>
      <p>Your UniAuth subject ID: {user?.sub}</p>

      <button onClick={callApi} className="px-4 py-2 bg-gray-200 rounded mt-4">
        Call Protected API
      </button>
      {apiData && <pre className="mt-4 p-4 bg-gray-100 rounded">{apiData}</pre>}

      <div className="mt-6">
        <LogoutButton className="px-4 py-2 bg-red-600 text-white rounded-lg">
          Sign out
        </LogoutButton>
      </div>
    </div>
  );
}

export function DashboardPage() {
  return (
    <ProtectedRoute fallback={<div>Loading dashboard...</div>}>
      <DashboardContent />
    </ProtectedRoute>
  );
}

Next Steps