Skip to main content

Next.js Adapter

The @rampart-auth/nextjs adapter provides full-stack authentication for Next.js applications using the App Router. It covers server-side token verification in Server Components, edge middleware for route protection, and a client-side auth context for interactive pages.

Installation

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

Quick Start

1. Configure the Provider

Create a Rampart configuration file:

// lib/rampart.ts
import { createRampartAuth } from "@rampart-auth/nextjs";

export const rampart = createRampartAuth({
issuerUrl: process.env.RAMPART_URL!,
clientId: process.env.RAMPART_CLIENT_ID!,
clientSecret: process.env.RAMPART_CLIENT_SECRET!,
redirectUri: process.env.RAMPART_REDIRECT_URI!,
postLogoutRedirectUri: process.env.NEXT_PUBLIC_APP_URL!,
scopes: ["openid", "profile", "email"],
});

2. Add the Auth API Route

// app/api/auth/[...rampart]/route.ts
import { rampart } from "@/lib/rampart";

export const { GET, POST } = rampart.handlers();

This creates the following routes automatically:

RoutePurpose
/api/auth/loginInitiates the Authorization Code flow
/api/auth/callbackHandles the OAuth redirect
/api/auth/logoutClears the session and redirects to Rampart logout
/api/auth/sessionReturns the current session (for client-side)
/api/auth/refreshRefreshes the access token

3. Add Middleware

// middleware.ts
import { rampart } from "@/lib/rampart";

export const middleware = rampart.middleware({
publicPaths: ["/", "/about", "/api/health"],
});

export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};

4. Wrap the Layout with AuthProvider

// app/layout.tsx
import { RampartProvider } from "@rampart-auth/nextjs/client";

export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<RampartProvider>{children}</RampartProvider>
</body>
</html>
);
}

Environment Variables

Add these to your .env.local:

RAMPART_URL=https://auth.example.com
RAMPART_CLIENT_ID=my-nextjs-app
RAMPART_CLIENT_SECRET=your-client-secret
RAMPART_REDIRECT_URI=http://localhost:3000/api/auth/callback
NEXT_PUBLIC_APP_URL=http://localhost:3000

Server-Side Authentication

Server Components

Use getSession() in Server Components to access the authenticated user:

// app/dashboard/page.tsx
import { rampart } from "@/lib/rampart";

export default async function DashboardPage() {
const session = await rampart.getSession();

if (!session) {
redirect("/api/auth/login");
}

return (
<div>
<h1>Dashboard</h1>
<p>Welcome, {session.user.name}</p>
<p>Email: {session.user.email}</p>
<p>Roles: {session.user.roles.join(", ")}</p>
</div>
);
}

Server Actions

Verify authentication in Server Actions:

// app/actions.ts
"use server";

import { rampart } from "@/lib/rampart";

export async function createTask(formData: FormData) {
const session = await rampart.getSession();
if (!session) {
throw new Error("Unauthorized");
}

const title = formData.get("title") as string;

// Use the access token to call your API
const res = await fetch(`${process.env.API_URL}/tasks`, {
method: "POST",
headers: {
Authorization: `Bearer ${session.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ title, userId: session.user.sub }),
});

if (!res.ok) {
throw new Error("Failed to create task");
}

return res.json();
}

Route Handlers

Protect API routes:

// app/api/tasks/route.ts
import { rampart } from "@/lib/rampart";
import { NextResponse } from "next/server";

export async function GET() {
const session = await rampart.getSession();
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

// Fetch tasks for the authenticated user
return NextResponse.json({
tasks: [
{ id: "1", title: "Review PR", assignee: session.user.sub },
],
});
}

Middleware

The middleware intercepts requests and redirects unauthenticated users to the login page.

// middleware.ts
import { rampart } from "@/lib/rampart";

export const middleware = rampart.middleware({
// Routes that don't require authentication
publicPaths: [
"/",
"/about",
"/pricing",
"/api/health",
"/api/webhooks/(.*)",
],

// Routes that require specific roles
rolePaths: {
"/admin/(.*)": ["admin"],
"/billing/(.*)": ["admin", "billing"],
},

// Where to redirect unauthenticated users (default: /api/auth/login)
loginPath: "/api/auth/login",

// Where to redirect users who lack required roles
forbiddenPath: "/403",
});

export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};

Client-Side Authentication

useAuth() Hook

Access authentication state in Client Components:

"use client";

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

export function UserMenu() {
const { user, isAuthenticated, isLoading } = useAuth();

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

if (!isAuthenticated) {
return <a href="/api/auth/login">Log in</a>;
}

return (
<div>
<span>{user.name}</span>
<a href="/api/auth/logout">Log out</a>
</div>
);
}

useAccessToken() Hook

Get a fresh access token for API calls from the client:

"use client";

import { useAccessToken } from "@rampart-auth/nextjs/client";
import { useState, useEffect } from "react";

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

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

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

Session Object

The session object returned by getSession() and the useAuth() hook:

interface RampartSession {
user: {
sub: string; // User ID
email?: string; // Email address
name?: string; // Display name
roles: string[]; // Assigned roles
orgId?: string; // Organization ID
};
accessToken: string; // Current access token
refreshToken: string; // Refresh token
idToken: string; // ID token
expiresAt: number; // Access token expiration timestamp
}

Full Working Example

// lib/rampart.ts
import { createRampartAuth } from "@rampart-auth/nextjs";

export const rampart = createRampartAuth({
issuerUrl: process.env.RAMPART_URL!,
clientId: process.env.RAMPART_CLIENT_ID!,
clientSecret: process.env.RAMPART_CLIENT_SECRET!,
redirectUri: process.env.RAMPART_REDIRECT_URI!,
postLogoutRedirectUri: process.env.NEXT_PUBLIC_APP_URL!,
scopes: ["openid", "profile", "email"],
});
// app/api/auth/[...rampart]/route.ts
import { rampart } from "@/lib/rampart";
export const { GET, POST } = rampart.handlers();
// middleware.ts
import { rampart } from "@/lib/rampart";

export const middleware = rampart.middleware({
publicPaths: ["/", "/about"],
rolePaths: { "/admin/(.*)": ["admin"] },
});

export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};
// app/layout.tsx
import { RampartProvider } from "@rampart-auth/nextjs/client";

export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<RampartProvider>{children}</RampartProvider>
</body>
</html>
);
}
// app/page.tsx
import { rampart } from "@/lib/rampart";
import { redirect } from "next/navigation";
import { LogoutButton } from "@/components/LogoutButton";

export default async function HomePage() {
const session = await rampart.getSession();

if (!session) {
return (
<div>
<h1>Welcome to My App</h1>
<a href="/api/auth/login">Log in</a>
</div>
);
}

return (
<div>
<h1>Welcome, {session.user.name}</h1>
<p>You are logged in as {session.user.email}</p>
<nav>
<a href="/dashboard">Dashboard</a>
{session.user.roles.includes("admin") && <a href="/admin">Admin</a>}
</nav>
<LogoutButton />
</div>
);
}
// components/LogoutButton.tsx
"use client";

export function LogoutButton() {
return (
<a href="/api/auth/logout">
<button>Log out</button>
</a>
);
}
// app/dashboard/page.tsx
import { rampart } from "@/lib/rampart";
import { redirect } from "next/navigation";

export default async function DashboardPage() {
const session = await rampart.getSession();
if (!session) redirect("/api/auth/login");

const res = await fetch(`${process.env.API_URL}/tasks`, {
headers: { Authorization: `Bearer ${session.accessToken}` },
cache: "no-store",
});

const { tasks } = await res.json();

return (
<div>
<h1>Dashboard</h1>
<h2>Your Tasks</h2>
<ul>
{tasks.map((t: { id: string; title: string }) => (
<li key={t.id}>{t.title}</li>
))}
</ul>
</div>
);
}

Security Considerations

  • Store RAMPART_CLIENT_SECRET only in server-side environment variables. Never prefix it with NEXT_PUBLIC_.
  • Use HTTP-only cookies for session storage (the adapter handles this automatically).
  • Set SameSite=Lax on session cookies to prevent CSRF.
  • Enable HTTPS in production — session cookies require secure transport.
  • The middleware runs at the edge, so route protection happens before your application code executes.