Skip to main content

React SPA Adapter

The @rampart-auth/react adapter provides React components and hooks for integrating Rampart authentication into single-page applications. It implements the Authorization Code flow with PKCE — the recommended approach for public clients that cannot securely store a client secret.

Installation​

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

Quick Start​

Wrap your application with RampartProvider and use the useAuth hook to access authentication state.

import React from "react";
import ReactDOM from "react-dom/client";
import { RampartProvider } from "@rampart-auth/react";
import App from "./App";

ReactDOM.createRoot(document.getElementById("root")!).render(
<RampartProvider
issuerUrl="https://auth.example.com"
clientId="my-spa"
redirectUri="http://localhost:5173/callback"
scopes={["openid", "profile", "email"]}
>
<App />
</RampartProvider>
);
// App.tsx
import { useAuth } from "@rampart-auth/react";

function App() {
const { user, isAuthenticated, isLoading, login, logout } = useAuth();

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

if (!isAuthenticated) {
return <button onClick={login}>Log in</button>;
}

return (
<div>
<p>Welcome, {user.name}!</p>
<button onClick={logout}>Log out</button>
</div>
);
}

export default App;

Configuration​

<RampartProvider
// Required
issuerUrl="https://auth.example.com"
clientId="my-spa"
redirectUri="http://localhost:5173/callback"

// Optional
realm="default" // Organization/realm
scopes={["openid", "profile", "email"]} // Requested scopes
postLogoutRedirectUri="http://localhost:5173" // Where to go after logout
silentRefresh={true} // Auto-refresh tokens (default: true)
silentRefreshInterval={60} // Refresh check interval in seconds
storage="sessionStorage" // "localStorage" | "sessionStorage" (default)
onError={(error) => console.error(error)} // Global error handler
>
<App />
</RampartProvider>

Hooks​

useAuth()​

The primary hook for accessing authentication state and actions.

import { useAuth } from "@rampart-auth/react";

function MyComponent() {
const {
// State
isAuthenticated, // boolean — is the user logged in?
isLoading, // boolean — is auth state being determined?
user, // User object or null
accessToken, // Current access token string or null
error, // Error object or null

// Actions
login, // () => void — redirect to Rampart login
logout, // () => void — clear session and redirect to logout
getAccessToken, // () => Promise<string> — get a fresh access token
} = useAuth();

return <div>{isAuthenticated ? user.name : "Not logged in"}</div>;
}

useAccessToken()​

Returns a fresh access token, automatically refreshing if needed. Useful for making authenticated API calls.

import { useAccessToken } from "@rampart-auth/react";

function TaskList() {
const getToken = useAccessToken();
const [tasks, setTasks] = React.useState([]);

React.useEffect(() => {
async function fetchTasks() {
const token = await getToken();
const res = await fetch("/api/tasks", {
headers: { Authorization: `Bearer ${token}` },
});
setTasks(await res.json());
}
fetchTasks();
}, [getToken]);

return (
<ul>
{tasks.map((task) => (
<li key={task.id}>{task.title}</li>
))}
</ul>
);
}

useRoles()​

Check if the current user has specific roles.

import { useRoles } from "@rampart-auth/react";

function AdminPanel() {
const { hasRole, hasAnyRole, roles } = useRoles();

if (!hasRole("admin")) {
return <p>Access denied. You need the admin role.</p>;
}

return (
<div>
<h2>Admin Panel</h2>
<p>Your roles: {roles.join(", ")}</p>
{hasAnyRole("super-admin", "owner") && (
<button>Dangerous Action</button>
)}
</div>
);
}

Components​

ProtectedRoute​

Wraps a route so that only authenticated users can access it. Unauthenticated users are redirected to the Rampart login page.

import { ProtectedRoute } from "@rampart-auth/react";
import { BrowserRouter, Routes, Route } from "react-router-dom";

function App() {
return (
<BrowserRouter>
<Routes>
{/* Public routes */}
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />

{/* Protected routes */}
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
<Route
path="/settings"
element={
<ProtectedRoute>
<Settings />
</ProtectedRoute>
}
/>

{/* Role-protected route */}
<Route
path="/admin"
element={
<ProtectedRoute roles={["admin"]} fallback={<AccessDenied />}>
<AdminPanel />
</ProtectedRoute>
}
/>

{/* Callback route — handles the OAuth redirect */}
<Route path="/callback" element={<AuthCallback />} />
</Routes>
</BrowserRouter>
);
}

AuthCallback​

Handles the OAuth 2.0 redirect callback. Place this at your redirectUri route.

import { AuthCallback } from "@rampart-auth/react";

// In your router:
<Route
path="/callback"
element={
<AuthCallback
onSuccess={() => navigate("/dashboard")}
onError={(err) => navigate(`/error?message=${err.message}`)}
>
<p>Completing login...</p>
</AuthCallback>
}
/>

PKCE Flow Details​

The adapter implements the full Authorization Code flow with PKCE (RFC 7636):

  1. Login initiated — generates a cryptographic code_verifier and derives a code_challenge using SHA-256
  2. Redirect to Rampart — sends code_challenge and code_challenge_method=S256 in the authorization request
  3. User authenticates — at the Rampart login page
  4. Callback received — Rampart redirects back with an authorization code
  5. Token exchange — the adapter sends the code and code_verifier to the token endpoint
  6. Tokens stored — access token, refresh token, and ID token are stored in the configured storage
  7. Silent refresh — before the access token expires, the adapter uses the refresh token to obtain new tokens

No client secret is ever used or stored in the browser.

Token Refresh​

By default, the adapter automatically refreshes tokens before they expire. You can control this behavior:

<RampartProvider
silentRefresh={true} // Enable auto-refresh (default: true)
silentRefreshInterval={60} // Check every 60 seconds
onTokenRefreshError={(err) => {
// Token refresh failed — user needs to log in again
console.error("Refresh failed:", err);
}}
>

To manually trigger a refresh:

const { getAccessToken } = useAuth();

// This returns a fresh token, refreshing if needed
const token = await getAccessToken();

Full Working Example​

// main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import { RampartProvider } from "@rampart-auth/react";
import App from "./App";

const RAMPART_URL = import.meta.env.VITE_RAMPART_URL || "https://auth.example.com";
const CLIENT_ID = import.meta.env.VITE_RAMPART_CLIENT_ID || "task-app";
const REDIRECT_URI = import.meta.env.VITE_REDIRECT_URI || "http://localhost:5173/callback";

ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<BrowserRouter>
<RampartProvider
issuerUrl={RAMPART_URL}
clientId={CLIENT_ID}
redirectUri={REDIRECT_URI}
scopes={["openid", "profile", "email", "tasks:read", "tasks:write"]}
postLogoutRedirectUri="http://localhost:5173"
>
<App />
</RampartProvider>
</BrowserRouter>
</React.StrictMode>
);
// App.tsx
import { Routes, Route, Navigate } from "react-router-dom";
import { useAuth, ProtectedRoute, AuthCallback } from "@rampart-auth/react";

function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/callback" element={<AuthCallback onSuccess={() => {}} />} />
<Route
path="/tasks"
element={
<ProtectedRoute>
<TaskList />
</ProtectedRoute>
}
/>
</Routes>
);
}

function Home() {
const { isAuthenticated, login, user } = useAuth();

return (
<div>
<h1>Task Manager</h1>
{isAuthenticated ? (
<div>
<p>Welcome back, {user.name}!</p>
<a href="/tasks">Go to tasks</a>
</div>
) : (
<button onClick={login}>Log in to get started</button>
)}
</div>
);
}

function TaskList() {
const { user, logout, getAccessToken } = useAuth();
const [tasks, setTasks] = React.useState<{ id: string; title: string }[]>([]);
const [newTask, setNewTask] = React.useState("");

React.useEffect(() => {
async function load() {
const token = await getAccessToken();
const res = await fetch("/api/tasks", {
headers: { Authorization: `Bearer ${token}` },
});
if (res.ok) {
const data = await res.json();
setTasks(data.tasks);
}
}
load();
}, [getAccessToken]);

async function addTask(e: React.FormEvent) {
e.preventDefault();
const token = await getAccessToken();
const res = await fetch("/api/tasks", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ title: newTask }),
});
if (res.ok) {
const task = await res.json();
setTasks((prev) => [...prev, task]);
setNewTask("");
}
}

return (
<div>
<header>
<span>Logged in as {user.email}</span>
<button onClick={logout}>Log out</button>
</header>

<h2>My Tasks</h2>
<ul>
{tasks.map((t) => (
<li key={t.id}>{t.title}</li>
))}
</ul>

<form onSubmit={addTask}>
<input
value={newTask}
onChange={(e) => setNewTask(e.target.value)}
placeholder="New task..."
/>
<button type="submit">Add</button>
</form>
</div>
);
}

export default App;

Environment Variables (Vite)​

Create a .env file in your project root:

VITE_RAMPART_URL=https://auth.example.com
VITE_RAMPART_CLIENT_ID=task-app
VITE_REDIRECT_URI=http://localhost:5173/callback

Security Considerations​

  • Never store client secrets in frontend code. The React adapter uses PKCE, which does not require a client secret.
  • Use sessionStorage (default) over localStorage unless you need tokens to persist across tabs. sessionStorage is cleared when the tab is closed.
  • Set short access token lifetimes (5-15 minutes) and rely on silent refresh for seamless UX.
  • Always use HTTPS in production. Token transmission over HTTP is insecure.
  • Configure CORS on your API to only accept requests from your SPA's origin.