React components for Keycloak authentication with SSR support, inspired by Clerk. Built on Auth.js (NextAuth v5) for secure server-side token management.
Key Feature: Tokens are kept server-side only. The client never has access to access tokens or refresh tokens, making this approach more secure than traditional client-side OAuth.
- SSR-first authentication using Auth.js with Keycloak provider
- Pre-built UI components (user avatar, user menu)
- Conditional rendering components (SignedIn, SignedOut, Protect)
- Server-side session and token management
- Automatic token refresh (server-side)
- Role-based access control
npm install keycloak-react next-auth// auth.ts (at project root)
import { createKeycloakAuth } from "keycloak-react/server";
export const { handlers, auth, signIn, signOut } = createKeycloakAuth({
keycloakUrl: process.env.KEYCLOAK_URL!,
realm: process.env.KEYCLOAK_REALM!,
clientId: process.env.KEYCLOAK_CLIENT_ID!,
clientSecret: process.env.KEYCLOAK_CLIENT_SECRET!,
});// app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth";
export const { GET, POST } = handlers;Copy .env.example to .env.local and fill in your values:
cp .env.example .env.localRequired variables:
# Keycloak Configuration
KEYCLOAK_URL=https://keycloak.example.com
KEYCLOAK_REALM=myrealm
KEYCLOAK_CLIENT_ID=my-app
KEYCLOAK_CLIENT_SECRET=your-client-secret
# Auth.js Configuration
# Generate secret with: openssl rand -base64 32
NEXTAUTH_SECRET=your-random-secret
NEXTAUTH_URL=http://localhost:3000// app/layout.tsx
import { KeycloakAuthProvider } from "keycloak-react/client";
import { auth } from "@/auth";
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const session = await auth();
return (
<html lang="en">
<body>
<KeycloakAuthProvider session={session}>
{children}
</KeycloakAuthProvider>
</body>
</html>
);
}// app/page.tsx
"use client";
import {
SignedIn,
SignedOut,
SignInButton,
SignOutButton,
useAuth,
} from "keycloak-react/client";
export default function Home() {
return (
<div>
<SignedOut>
<h1>Welcome!</h1>
<SignInButton>Sign In with Keycloak</SignInButton>
</SignedOut>
<SignedIn>
<UserGreeting />
<SignOutButton>Sign Out</SignOutButton>
</SignedIn>
</div>
);
}
function UserGreeting() {
const { user } = useAuth();
return <h1>Hello, {user?.name}!</h1>;
}Tokens are only available on the server. Use them for API calls:
// app/api/data/route.ts
import { getServerSession } from "keycloak-react/server";
import { NextRequest } from "next/server";
export async function GET(req: NextRequest) {
const session = await getServerSession(req);
if (!session.isAuthenticated) {
return new Response("Unauthorized", { status: 401 });
}
// Use the access token for backend API calls
const response = await fetch("https://api.example.com/data", {
headers: {
Authorization: `Bearer ${session.accessToken}`,
},
});
return Response.json(await response.json());
}// app/dashboard/page.tsx
import { getServerSession } from "keycloak-react/server";
import { redirect } from "next/navigation";
import { headers, cookies } from "next/headers";
export default async function DashboardPage() {
const session = await getServerSession({ headers: headers(), cookies: cookies() });
if (!session.isAuthenticated) {
redirect("/api/auth/signin");
}
return (
<div>
<h1>Dashboard</h1>
<p>Welcome, {session.user?.name}</p>
</div>
);
}// Server-side role check
import { hasRole, hasAnyRole } from "keycloak-react/server";
export default async function AdminPage() {
const isAdmin = await hasRole(req, "admin");
if (!isAdmin) {
return <AccessDenied />;
}
return <AdminDashboard />;
}// Client-side role check
"use client";
import { useHasRole, Protect } from "keycloak-react/client";
function AdminButton() {
const isAdmin = useHasRole("admin");
if (!isAdmin) return null;
return <button>Admin Panel</button>;
}
// Or use Protect component
function AdminSection() {
return (
<Protect
roles={["admin"]}
unauthorizedFallback={<p>You don't have access to this section.</p>}
>
<AdminDashboard />
</Protect>
);
}Creates Auth.js configuration for Keycloak:
const { handlers, auth, signIn, signOut } = createKeycloakAuth({
keycloakUrl: string; // Keycloak server URL
realm: string; // Realm name
clientId: string; // Client ID
clientSecret: string; // Client secret
basePath?: string; // Auth route base path (default: /api/auth)
options?: NextAuthConfig; // Additional Auth.js options
});Get the full session including access token (server-only):
const session = await getServerSession(req);
// session.user - User info
// session.accessToken - Access token for API calls
// session.isAuthenticated - BooleanGet just the user (without tokens):
const user = await getUser(req);Check if user has a specific role:
const isAdmin = await hasRole(req, "admin");Check if user has any of the specified roles:
const canEdit = await hasAnyRole(req, ["admin", "editor"]);Wraps your app to provide auth context:
<KeycloakAuthProvider
session={session} // Initial session from server
basePath="/api/auth" // Auth route base path
refetchInterval={0} // Session refresh interval (seconds)
refetchOnWindowFocus={true}
>
{children}
</KeycloakAuthProvider>Hook to access auth state:
const {
isLoading, // Loading state
isAuthenticated, // Boolean
user, // User object (no tokens!)
roles, // User's resource roles
realmRoles, // User's realm roles
signIn, // Sign in function
signOut, // Sign out function
error, // Any session error
} = useAuth();Get just the current user:
const user = useUser();Check if user has a role:
const isAdmin = useHasRole("admin");<SignedIn>- Render children only when signed in<SignedOut>- Render children only when signed out<Protect>- Protect content with optional role requirements<RedirectToSignIn>- Redirect to sign in page
<SignInButton>- Triggers sign in<SignOutButton>- Triggers sign out
This library follows a secure-by-default approach:
-
Tokens stay server-side: Access tokens and refresh tokens are stored in HTTP-only cookies and the JWT. They are never exposed to client-side JavaScript.
-
Automatic refresh: Token refresh happens automatically on the server when tokens expire.
-
Keycloak logout: When signing out, the library also revokes the refresh token on Keycloak.
-
Session only on client: The client only receives session information (user data, roles) - never the actual tokens.
Make sure your Keycloak client is configured correctly:
- Access Type:
confidential(to use client secret) - Valid Redirect URIs:
http://localhost:3000/*(your app URL) - Web Origins:
http://localhost:3000 - Client Authentication: Enabled
For local testing, this repository also includes an importable client export at
demo/keycloak-client-demo.json (client ID demo). You can import it from the
Keycloak Admin Console and then set KEYCLOAK_CLIENT_SECRET=demo-secret-change-me
in your local env file.
If migrating from the previous client-side-only version:
-
Replace
KeycloakAuthProviderprops:// Before (CSR) <KeycloakAuthProvider url="..." realm="..." clientId="..."> // After (SSR) <KeycloakAuthProvider session={session}>
-
Remove
keycloak-jsreferences:useKeycloak()is no longer availablegetToken()is no longer available on client- Use server-side
getServerSession()for token access
-
Update imports:
// Before import { ... } from "keycloak-react"; // After - Client components import { ... } from "keycloak-react/client"; // After - Server utilities import { ... } from "keycloak-react/server";
-
Add
"use client"directive to client components.
Apache License 2.0