Nuxt 3 Authentication with Keycloak: Complete Guide
Last updated: March 2026
Nuxt 3 combines Vue’s reactivity with server-side rendering, file-based routing, and a powerful module ecosystem. When your Nuxt application needs authentication, Keycloak provides single sign-on, multi-factor authentication, and identity federation without locking you into a proprietary provider.
This guide walks through integrating Keycloak with Nuxt 3 using nuxt-auth-utils for server-side OIDC authentication. You will set up secure session handling, route middleware for page protection, role-based access control, and proper SSR token management.
Prerequisites
- Node.js 18+ and npm 10+
- Nuxt 3.10+
- A running Keycloak instance (version 22+). Spin one up with our Docker Compose Generator or use managed Keycloak hosting.
Step 1: Set Up a Keycloak Client
In the Keycloak Admin Console:
- Go to Clients > Create client
- Set Client ID to
nuxt-app - Set Client type to
OpenID Connect - Enable Client authentication (confidential client)
- Under Valid redirect URIs, add
http://localhost:3000/auth/keycloak/callback - Under Valid post logout redirect URIs, add
http://localhost:3000 - Under Web origins, add
http://localhost:3000
After saving, go to the Credentials tab and copy the Client secret.
Create Roles
- Go to the client’s Roles tab
- Create roles like
adminanduser - Assign them to test users under Users > Role mappings > Client roles
For detailed role configuration, see our RBAC feature overview or the guide on fine-grained authorization in Keycloak.
Step 2: Create the Nuxt 3 Project
npx nuxi@latest init keycloak-nuxt-app
cd keycloak-nuxt-app
npm install
Install the auth module:
npm install nuxt-auth-utils
nuxt-auth-utils is a lightweight module built specifically for Nuxt 3 that handles OAuth/OIDC flows server-side using encrypted cookies. It supports Keycloak as a built-in provider.
Step 3: Configure Nuxt
Update your nuxt.config.ts:
// nuxt.config.ts
export default defineNuxtConfig({
compatibilityDate: "2025-01-01",
devtools: { enabled: true },
modules: ["nuxt-auth-utils"],
runtimeConfig: {
oauth: {
keycloak: {
clientId: "",
clientSecret: "",
serverUrl: "",
realm: "",
},
},
session: {
maxAge: 60 * 60 * 24, // 24 hours
password: "", // auto-generated in dev, set in production
},
},
});
Create your .env file:
# .env
NUXT_OAUTH_KEYCLOAK_CLIENT_ID=nuxt-app
NUXT_OAUTH_KEYCLOAK_CLIENT_SECRET=your-client-secret
NUXT_OAUTH_KEYCLOAK_SERVER_URL=http://localhost:8080
NUXT_OAUTH_KEYCLOAK_REALM=myrealm
NUXT_SESSION_PASSWORD=a-random-32-character-or-longer-string
Nuxt’s runtime config automatically maps environment variables with the NUXT_ prefix to the corresponding config keys.
Step 4: Authentication API Routes
Create the server-side routes that handle the OAuth flow.
Login Route
// server/routes/auth/keycloak.get.ts
export default defineOAuthKeycloakEventHandler({
config: {
scope: ["openid", "profile", "email"],
},
async onSuccess(event, { user, tokens }) {
// Store user info and tokens in the session
await setUserSession(event, {
user: {
id: user.sub,
name: user.name || user.preferred_username,
email: user.email,
emailVerified: user.email_verified,
username: user.preferred_username,
roles: user.realm_access?.roles || [],
clientRoles:
user.resource_access?.["nuxt-app"]?.roles || [],
},
tokens: {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
idToken: tokens.id_token,
expiresAt:
Math.floor(Date.now() / 1000) +
(tokens.expires_in || 300),
},
});
return sendRedirect(event, "/");
},
onError(event, error) {
console.error("Keycloak OAuth error:", error);
return sendRedirect(event, "/?error=auth_failed");
},
});
Logout Route
// server/routes/auth/logout.post.ts
export default defineEventHandler(async (event) => {
const session = await getUserSession(event);
const config = useRuntimeConfig();
// Clear the local session
await clearUserSession(event);
// Build the Keycloak logout URL
const keycloakLogoutUrl = new URL(
`${config.oauth.keycloak.serverUrl}/realms/${config.oauth.keycloak.realm}/protocol/openid-connect/logout`
);
if (session?.tokens?.idToken) {
keycloakLogoutUrl.searchParams.set(
"id_token_hint",
session.tokens.idToken
);
}
keycloakLogoutUrl.searchParams.set(
"post_logout_redirect_uri",
getRequestURL(event).origin
);
return { logoutUrl: keycloakLogoutUrl.toString() };
});
Step 5: Token Refresh
Access tokens expire. You need server-side middleware that refreshes them before they expire:
// server/middleware/auth-refresh.ts
export default defineEventHandler(async (event) => {
const session = await getUserSession(event);
if (!session?.tokens?.refreshToken) return;
const now = Math.floor(Date.now() / 1000);
const expiresAt = session.tokens.expiresAt || 0;
// Refresh if token expires within 60 seconds
if (now < expiresAt - 60) return;
const config = useRuntimeConfig();
try {
const tokenUrl = `${config.oauth.keycloak.serverUrl}/realms/${config.oauth.keycloak.realm}/protocol/openid-connect/token`;
const response = await $fetch<{
access_token: string;
refresh_token?: string;
id_token?: string;
expires_in: number;
}>(tokenUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
grant_type: "refresh_token",
client_id: config.oauth.keycloak.clientId,
client_secret: config.oauth.keycloak.clientSecret,
refresh_token: session.tokens.refreshToken,
}).toString(),
});
// Update session with new tokens
await setUserSession(event, {
...session,
tokens: {
accessToken: response.access_token,
refreshToken:
response.refresh_token ||
session.tokens.refreshToken,
idToken:
response.id_token || session.tokens.idToken,
expiresAt:
Math.floor(Date.now() / 1000) +
response.expires_in,
},
});
} catch (error) {
console.error("Token refresh failed:", error);
// Clear session on refresh failure --- user needs to re-login
await clearUserSession(event);
}
});
For more on token lifecycle strategies, see JWT token lifecycle management.
Step 6: Route Middleware for Page Protection
Create Nuxt route middleware to protect pages that require authentication:
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to) => {
const { loggedIn } = useUserSession();
if (!loggedIn.value) {
return navigateTo("/login");
}
});
For role-based protection:
// middleware/admin.ts
export default defineNuxtRouteMiddleware((to) => {
const { user } = useUserSession();
if (!user.value) {
return navigateTo("/login");
}
const roles = user.value.clientRoles || [];
if (!roles.includes("admin")) {
return navigateTo("/unauthorized");
}
});
Step 7: Pages and Components
Login Page
<!-- pages/login.vue -->
<script setup lang="ts">
definePageMeta({ layout: "auth" });
const { loggedIn } = useUserSession();
// Redirect if already logged in
if (loggedIn.value) {
navigateTo("/");
}
</script>
<template>
<div class="login-container">
<div class="login-card">
<h1>Welcome</h1>
<p>Sign in to access your account</p>
<a href="/auth/keycloak" class="login-button">
Sign in with Keycloak
</a>
<p class="login-note">
You will be redirected to the Keycloak login page
</p>
</div>
</div>
</template>
<style scoped>
.login-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #f8fafc;
}
.login-card {
background: white;
padding: 3rem;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
text-align: center;
max-width: 400px;
width: 100%;
}
.login-button {
display: inline-block;
background: #3b82f6;
color: white;
padding: 12px 32px;
border-radius: 8px;
text-decoration: none;
font-weight: 600;
margin-top: 1.5rem;
transition: background 0.2s;
}
.login-button:hover {
background: #2563eb;
}
.login-note {
margin-top: 1rem;
color: #64748b;
font-size: 0.875rem;
}
</style>
Dashboard (Protected Page)
<!-- pages/dashboard.vue -->
<script setup lang="ts">
definePageMeta({
middleware: "auth",
});
const { user, clear: clearSession } = useUserSession();
async function logout() {
const { logoutUrl } = await $fetch("/auth/logout", {
method: "POST",
});
await clearSession();
// Redirect to Keycloak logout
window.location.href = logoutUrl;
}
</script>
<template>
<div class="dashboard">
<header class="dashboard-header">
<h1>Dashboard</h1>
<button class="logout-button" @click="logout">
Sign out
</button>
</header>
<div class="user-card" v-if="user">
<h2>Welcome, {{ user.name || user.username }}</h2>
<div class="user-details">
<div class="detail">
<span class="label">Email</span>
<span class="value">{{ user.email }}</span>
</div>
<div class="detail">
<span class="label">User ID</span>
<span class="value">{{ user.id }}</span>
</div>
<div class="detail">
<span class="label">Roles</span>
<span class="value">
{{ user.roles?.join(", ") || "None" }}
</span>
</div>
</div>
</div>
</div>
</template>
Admin Page (Role-Protected)
<!-- pages/admin.vue -->
<script setup lang="ts">
definePageMeta({
middleware: "admin",
});
const { user } = useUserSession();
</script>
<template>
<div class="admin-panel">
<h1>Admin Panel</h1>
<p>This page is only accessible to users with the admin role.</p>
<pre>{{ JSON.stringify(user, null, 2) }}</pre>
</div>
</template>
Step 8: Server-Side API Calls with Token Forwarding
When your Nuxt server needs to call other APIs on behalf of the authenticated user, forward the access token:
// server/api/backend-data.get.ts
export default defineEventHandler(async (event) => {
const session = await getUserSession(event);
if (!session?.tokens?.accessToken) {
throw createError({
statusCode: 401,
message: "Not authenticated",
});
}
// Forward the access token to your backend API
const data = await $fetch(
"https://api.example.com/data",
{
headers: {
Authorization: `Bearer ${session.tokens.accessToken}`,
},
}
);
return data;
});
This is the server-to-server pattern. The Nuxt server acts as a confidential client, forwarding the user’s access token to downstream services. For a deeper look at this architecture, see the Backend for Frontend (BFF) pattern with Keycloak.
Step 9: TypeScript Type Safety
Define types for your session data to get autocompletion and type checking:
// auth.d.ts
declare module "#auth-utils" {
interface User {
id: string;
name: string;
email: string;
emailVerified: boolean;
username: string;
roles: string[];
clientRoles: string[];
}
interface UserSession {
tokens: {
accessToken: string;
refreshToken: string;
idToken: string;
expiresAt: number;
};
}
}
export {};
Session Security
The nuxt-auth-utils module stores session data in encrypted, HTTP-only cookies. This means:
- No client-side token exposure — access tokens never appear in JavaScript
- CSRF protection — cookies are same-site by default
- Automatic encryption — session data is encrypted with the
NUXT_SESSION_PASSWORD
In production, ensure your session password is at least 32 characters and stored securely. For session management best practices, see Keycloak’s session management capabilities.
CORS Configuration
If your Nuxt app makes direct requests to the Keycloak API (e.g., for user profile updates), you need CORS configured. See configuring CORS with your Keycloak OIDC client for the full walkthrough.
Testing
During development, inspect your authentication tokens with our JWT Token Analyzer. You can access the raw token from the server API or browser dev tools:
// server/api/debug-token.get.ts (development only)
export default defineEventHandler(async (event) => {
if (process.env.NODE_ENV !== "development") {
throw createError({ statusCode: 404 });
}
const session = await getUserSession(event);
return {
accessToken: session?.tokens?.accessToken,
expiresAt: session?.tokens?.expiresAt,
};
});
If you are testing SAML-based identity providers with Keycloak brokering, use our SAML Decoder to inspect SAML assertions.
Further Reading
- Keycloak Securing Applications Guide
- nuxt-auth-utils Documentation
- Vue.js authentication with Keycloak — client-side Vue integration
- Next.js authentication with Keycloak — the React equivalent
- SvelteKit authentication with Keycloak — another SSR framework integration
Wrapping Up
Nuxt 3 with nuxt-auth-utils provides a clean, server-side authentication flow that keeps tokens out of the browser. Combined with Keycloak, you get enterprise-grade SSO, MFA, and user management without writing custom auth infrastructure.
If you want to skip operating Keycloak yourself, Skycloak provides managed Keycloak with automated upgrades, security monitoring, and guaranteed uptime. Visit our pricing page to compare plans, or explore our documentation for integration guides.
Ready to simplify your authentication?
Deploy production-ready Keycloak in minutes. Unlimited users, flat pricing, no SSO tax.