Auth for React

OpenID Connect authentication SDK for React single-page applications.

Installation

npm install @usetransactional/auth-react
pnpm add @usetransactional/auth-react
yarn add @usetransactional/auth-react

Quick Start

Wrap your application in the TransactionalAuthProvider and use the useAuth hook to access authentication state.

import { TransactionalAuthProvider, useAuth } from "@usetransactional/auth-react";
 
function App() {
  return (
    <TransactionalAuthProvider
      domain="auth.yourdomain.com"
      clientId="your-client-id"
      redirectUri={window.location.origin}
    >
      <LoginButton />
    </TransactionalAuthProvider>
  );
}
 
function LoginButton() {
  const { isAuthenticated, isLoading, loginWithRedirect, logout, user } = useAuth();
 
  if (isLoading) return <div>Loading...</div>;
 
  if (isAuthenticated) {
    return (
      <div>
        <p>Welcome, {user?.name}</p>
        <button onClick={() => logout()}>Log out</button>
      </div>
    );
  }
 
  return <button onClick={() => loginWithRedirect()}>Log in</button>;
}

Provider

The TransactionalAuthProvider wraps your application and manages all authentication state, token storage, and silent renewal.

import { TransactionalAuthProvider } from "@usetransactional/auth-react";
 
function App() {
  return (
    <TransactionalAuthProvider
      domain="auth.yourdomain.com"
      clientId="your-client-id"
      redirectUri={window.location.origin}
      scope="openid profile email"
      audience="https://api.yourdomain.com"
      cacheLocation="localstorage"
      useRefreshTokens={true}
      onRedirectCallback={(appState) => {
        window.history.replaceState(
          {},
          document.title,
          appState?.returnTo || window.location.pathname
        );
      }}
    >
      {children}
    </TransactionalAuthProvider>
  );
}

Props

PropTypeDefaultDescription
domainstringrequiredYour Transactional Auth domain
clientIdstringrequiredYour application's client ID
redirectUristringwindow.location.originURL to redirect to after login
scopestring"openid profile email"OAuth scopes to request
audiencestringAPI audience identifier
cacheLocation"memory" | "localstorage""localstorage"Where to store tokens
useRefreshTokensbooleantrueUse refresh tokens for silent renewal
onRedirectCallback(appState?) => voidCalled after the redirect login completes

Hooks

useAuth

The primary hook for accessing all authentication state and methods.

import { useAuth } from "@usetransactional/auth-react";
 
function Dashboard() {
  const {
    isAuthenticated,
    isLoading,
    user,
    error,
    loginWithRedirect,
    loginWithPopup,
    logout,
    getAccessToken,
    getIdTokenClaims,
    hasPermission,
    hasRole,
  } = useAuth();
 
  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
 
  return (
    <div>
      <p>Authenticated: {isAuthenticated ? "Yes" : "No"}</p>
      {user && <p>Email: {user.email}</p>}
      {hasPermission("emails:send") && <p>You can send emails</p>}
      {hasRole("admin") && <p>You are an admin</p>}
    </div>
  );
}

Returns:

PropertyTypeDescription
isAuthenticatedbooleanWhether the user is logged in
isLoadingbooleanWhether the auth state is still loading
userUser | undefinedThe authenticated user's profile
errorError | undefinedAny authentication error
loginWithRedirect(options?) => Promise<void>Redirect to the login page
loginWithPopup(options?) => Promise<void>Open login in a popup window
logout(options?) => voidLog the user out
getAccessToken() => Promise<string>Get the current access token (refreshes if expired)
getIdTokenClaims() => Promise<IdTokenClaims>Get the decoded ID token claims
hasPermission(permission: string) => booleanCheck if the user has a specific permission
hasRole(role: string) => booleanCheck if the user has a specific role

useUser

A convenience hook that returns just the user object and loading state.

import { useUser } from "@usetransactional/auth-react";
 
function UserProfile() {
  const { user, isLoading } = 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

Returns the current access token or null if the user is not authenticated. The token is kept fresh automatically.

import { useAccessToken } from "@usetransactional/auth-react";
 
function ApiStatus() {
  const token = useAccessToken();
 
  return (
    <div>
      <p>Token available: {token ? "Yes" : "No"}</p>
    </div>
  );
}

Components

AuthGuard

Protects its children from being rendered unless the user is authenticated. Displays a fallback while loading and optionally redirects unauthenticated users to the login page.

import { AuthGuard } from "@usetransactional/auth-react";
 
function ProtectedPage() {
  return (
    <AuthGuard
      fallback={<div>Loading...</div>}
      onUnauthenticated={() => console.log("User not authenticated")}
      autoRedirect={true}
    >
      <h1>Protected Content</h1>
      <p>Only visible to authenticated users.</p>
    </AuthGuard>
  );
}

Props:

PropTypeDefaultDescription
fallbackReactNodeShown while auth state is loading
onUnauthenticated() => voidCalled when the user is not authenticated
autoRedirectbooleantrueAutomatically redirect to login if not authenticated

withAuthenticationRequired

A higher-order component (HOC) that wraps a component and ensures the user is authenticated before rendering it.

import { withAuthenticationRequired } from "@usetransactional/auth-react";
 
function SettingsPage() {
  return <h1>Account Settings</h1>;
}
 
export default withAuthenticationRequired(SettingsPage, {
  fallback: <div>Redirecting to login...</div>,
  onUnauthenticated: () => console.log("Redirecting..."),
  returnTo: "/settings",
});

Options:

OptionTypeDefaultDescription
fallbackReactNodeShown while auth state is loading
onUnauthenticated() => voidCalled when redirect happens
returnTostringURL to return to after login

Login and Logout Options

loginWithRedirect

Redirects the user to the Transactional Auth login page.

const { loginWithRedirect } = useAuth();
 
// Basic redirect
await loginWithRedirect();
 
// With options
await loginWithRedirect({
  returnTo: "/dashboard",
  appState: { targetUrl: "/onboarding" },
  connection: "google-oauth2", // skip to a specific identity provider
  loginHint: "user@example.com", // pre-fill the email field
});

Options:

OptionTypeDescription
returnTostringURL to navigate to after login
appStateobjectCustom state passed through the redirect
connectionstringIdentity provider connection name
loginHintstringPre-fill the email/username field

loginWithPopup

Opens the login page in a popup window instead of redirecting.

const { loginWithPopup } = useAuth();
 
// Basic popup
await loginWithPopup();
 
// With options
await loginWithPopup({
  returnTo: "/dashboard",
  appState: { targetUrl: "/onboarding" },
  connection: "google-oauth2",
  loginHint: "user@example.com",
});

Takes the same options as loginWithRedirect.

logout

Logs the user out and optionally redirects.

const { logout } = useAuth();
 
// Basic logout (redirects to current origin)
logout();
 
// With options
logout({
  returnTo: "https://yourdomain.com",
  federated: true, // also log out of the identity provider
});

Options:

OptionTypeDescription
returnTostringURL to redirect to after logout
federatedbooleanAlso log out from the upstream identity provider

Making API Calls

Use getAccessToken() to retrieve a valid access token and attach it to your API requests as a bearer token.

import { useAuth } from "@usetransactional/auth-react";
import { useState, useEffect } from "react";
 
function EmailList() {
  const { getAccessToken, isAuthenticated } = useAuth();
  const [emails, setEmails] = useState([]);
 
  useEffect(() => {
    if (!isAuthenticated) return;
 
    async function fetchEmails() {
      const token = await getAccessToken();
 
      const response = await fetch("https://api.yourdomain.com/emails", {
        headers: {
          Authorization: `Bearer ${token}`,
          "Content-Type": "application/json",
        },
      });
 
      if (response.ok) {
        const data = await response.json();
        setEmails(data.emails);
      }
    }
 
    fetchEmails();
  }, [isAuthenticated, getAccessToken]);
 
  return (
    <ul>
      {emails.map((email) => (
        <li key={email.id}>{email.subject}</li>
      ))}
    </ul>
  );
}

Creating a Reusable API Client

import { useAuth } from "@usetransactional/auth-react";
import { useCallback } from "react";
 
function useApiClient() {
  const { getAccessToken } = useAuth();
 
  const apiFetch = useCallback(
    async (path: string, options: RequestInit = {}) => {
      const token = await getAccessToken();
 
      const response = await fetch(`https://api.yourdomain.com${path}`, {
        ...options,
        headers: {
          Authorization: `Bearer ${token}`,
          "Content-Type": "application/json",
          ...options.headers,
        },
      });
 
      if (!response.ok) {
        throw new Error(`API error: ${response.status}`);
      }
 
      return response.json();
    },
    [getAccessToken]
  );
 
  return { apiFetch };
}
 
// Usage
function Dashboard() {
  const { apiFetch } = useApiClient();
 
  async function sendEmail() {
    await apiFetch("/emails", {
      method: "POST",
      body: JSON.stringify({
        to: "user@example.com",
        subject: "Hello",
        htmlBody: "<p>World</p>",
      }),
    });
  }
 
  return <button onClick={sendEmail}>Send Email</button>;
}

Next.js App Router

When using the SDK with Next.js App Router, the provider must be rendered in a client component since it relies on browser APIs and React context.

Create a client wrapper component:

// app/providers.tsx
"use client";
 
import { TransactionalAuthProvider } from "@usetransactional/auth-react";
 
export function AuthProvider({ children }: { children: React.ReactNode }) {
  return (
    <TransactionalAuthProvider
      domain="auth.yourdomain.com"
      clientId="your-client-id"
      redirectUri={typeof window !== "undefined" ? window.location.origin : ""}
      audience="https://api.yourdomain.com"
    >
      {children}
    </TransactionalAuthProvider>
  );
}

Use it in your root layout:

// app/layout.tsx
import { AuthProvider } from "./providers";
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <AuthProvider>{children}</AuthProvider>
      </body>
    </html>
  );
}

Use hooks in client components:

// app/dashboard/page.tsx
"use client";
 
import { useAuth, AuthGuard } from "@usetransactional/auth-react";
 
export default function DashboardPage() {
  return (
    <AuthGuard fallback={<div>Loading...</div>}>
      <DashboardContent />
    </AuthGuard>
  );
}
 
function DashboardContent() {
  const { user } = useAuth();
  return <h1>Welcome, {user?.name}</h1>;
}

Full SPA Example

import {
  TransactionalAuthProvider,
  useAuth,
  AuthGuard,
} from "@usetransactional/auth-react";
import { BrowserRouter, Routes, Route, Link, Navigate } from "react-router-dom";
 
// --- App Entry ---
 
function App() {
  return (
    <TransactionalAuthProvider
      domain="auth.yourdomain.com"
      clientId="your-client-id"
      redirectUri={window.location.origin}
      audience="https://api.yourdomain.com"
      scope="openid profile email"
      cacheLocation="localstorage"
      useRefreshTokens={true}
      onRedirectCallback={(appState) => {
        window.history.replaceState(
          {},
          document.title,
          appState?.returnTo || "/"
        );
      }}
    >
      <BrowserRouter>
        <Navigation />
        <Routes>
          <Route path="/" element={<HomePage />} />
          <Route
            path="/dashboard"
            element={
              <AuthGuard fallback={<div>Loading...</div>}>
                <DashboardPage />
              </AuthGuard>
            }
          />
          <Route
            path="/settings"
            element={
              <AuthGuard fallback={<div>Loading...</div>}>
                <SettingsPage />
              </AuthGuard>
            }
          />
        </Routes>
      </BrowserRouter>
    </TransactionalAuthProvider>
  );
}
 
// --- Navigation ---
 
function Navigation() {
  const { isAuthenticated, isLoading, loginWithRedirect, logout, user } = useAuth();
 
  return (
    <nav>
      <Link to="/">Home</Link>
      {isAuthenticated && <Link to="/dashboard">Dashboard</Link>}
      {isAuthenticated && <Link to="/settings">Settings</Link>}
 
      {isLoading ? (
        <span>Loading...</span>
      ) : isAuthenticated ? (
        <div>
          <span>{user?.name}</span>
          <button onClick={() => logout({ returnTo: window.location.origin })}>
            Log out
          </button>
        </div>
      ) : (
        <button onClick={() => loginWithRedirect()}>Log in</button>
      )}
    </nav>
  );
}
 
// --- Pages ---
 
function HomePage() {
  const { isAuthenticated } = useAuth();
 
  return (
    <div>
      <h1>Welcome</h1>
      {isAuthenticated ? (
        <p>You are logged in. Visit your <Link to="/dashboard">dashboard</Link>.</p>
      ) : (
        <p>Please log in to continue.</p>
      )}
    </div>
  );
}
 
function DashboardPage() {
  const { user, getAccessToken } = useAuth();
  const [data, setData] = useState(null);
 
  useEffect(() => {
    async function loadData() {
      const token = await getAccessToken();
      const res = await fetch("https://api.yourdomain.com/dashboard", {
        headers: { Authorization: `Bearer ${token}` },
      });
      setData(await res.json());
    }
    loadData();
  }, [getAccessToken]);
 
  return (
    <div>
      <h1>Dashboard</h1>
      <p>Welcome back, {user?.name}</p>
      {data && <pre>{JSON.stringify(data, null, 2)}</pre>}
    </div>
  );
}
 
function SettingsPage() {
  const { user, hasPermission } = useAuth();
 
  return (
    <div>
      <h1>Settings</h1>
      <p>Email: {user?.email}</p>
      {hasPermission("settings:write") ? (
        <button>Save Changes</button>
      ) : (
        <p>You do not have permission to modify settings.</p>
      )}
    </div>
  );
}