Nuxt.js Integration
This guide covers how to integrate Skycloak authentication into Nuxt.js applications using nuxt-oidc-auth, a module built specifically for OIDC authorization-code + PKCE flows in Nuxt 3.
Which auth module should I use?
nuxt-oidc-auth speaks OIDC directly and works with any standards-compliant provider (including Skycloak) with no adapter code. @sidebase/nuxt-auth (built on Auth.js) is a good choice if you’re already using Auth.js providers elsewhere, but requires a generic OAuth provider config for Keycloak. This guide uses nuxt-oidc-auth since it maps most directly onto Skycloak’s OIDC endpoints.
Prerequisites
- Nuxt 3.9+ application
- Node.js 18+
- Skycloak cluster with configured realm and client
- Basic understanding of Nuxt server routes and middleware
Quick Start
1. Create an Application in Skycloak
- In Skycloak, navigate to your cluster → Applications → Create Application
- Set Application Type to
Confidential(server-side session, so the client secret stays on the Nuxt server) - Add Redirect URIs:
http://localhost:3000/auth/oidc/callbackfor local dev, plus your production URL - After creation, copy the Client ID and Client Secret from the credentials tab — you’ll need both below
2. Install Dependencies
npx nuxi module add nuxt-oidc-auth3. Configure the Module
Add to nuxt.config.ts:
export default defineNuxtConfig({
modules: ['nuxt-oidc-auth'],
oidc: {
defaultProvider: 'skycloak',
providers: {
skycloak: {
clientId: process.env.NUXT_OIDC_SKYCLOAK_CLIENT_ID,
clientSecret: process.env.NUXT_OIDC_SKYCLOAK_CLIENT_SECRET,
baseUrl: 'https://your-cluster-id.app.skycloak.io/realms/your-realm',
redirectUri: process.env.NUXT_OIDC_SKYCLOAK_REDIRECT_URI,
scope: ['openid', 'profile', 'email'],
pkce: true,
state: true,
},
},
session: {
automaticRefresh: true,
expirationCheck: true,
},
},
})4. Set Environment Variables
Create .env:
NUXT_OIDC_SKYCLOAK_CLIENT_ID=your-nuxt-app
NUXT_OIDC_SKYCLOAK_CLIENT_SECRET=your-client-secret
NUXT_OIDC_SKYCLOAK_REDIRECT_URI=http://localhost:3000/auth/oidc/callback
NUXT_OIDC_SESSION_SECRET=a-random-48-char-string-used-to-encrypt-the-sessionNUXT_OIDC_SESSION_SECRET must be at least 48 characters. Generate one with openssl rand -base64 48 and never commit it — load it from your deployment platform’s secret store.5. Add Login/Logout Controls
<!-- components/AuthButton.vue -->
<script setup lang="ts">
const { loggedIn, user, clear } = useOidcAuth()
</script>
<template>
<div v-if="loggedIn" class="flex items-center gap-4">
<span>Welcome, {{ user?.name }}</span>
<button @click="clear()">Sign Out</button>
</div>
<a v-else href="/auth/skycloak/login">Sign In with Skycloak</a>
</template>The module auto-registers /auth/<provider>/login, /auth/<provider>/callback, and /auth/<provider>/logout server routes — no manual route handlers needed for the basic flow.
Protecting Pages and Routes
Client-Side Route Middleware
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to) => {
const { loggedIn } = useOidcAuth()
if (!loggedIn.value && to.path.startsWith('/dashboard')) {
return navigateTo('/auth/skycloak/login', { external: true })
}
})<!-- pages/dashboard/index.vue -->
<script setup lang="ts">
definePageMeta({ middleware: 'auth' })
const { user } = useOidcAuth()
</script>Server API Route Protection
// server/api/profile.get.ts
export default defineEventHandler(async (event) => {
const session = await getUserSession(event)
if (!session?.user) {
throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
}
return {
id: session.user.sub,
email: session.user.email,
roles: session.user.realm_access?.roles ?? [],
}
})Role-Based Access
// server/utils/requireRealmRole.ts
import type { H3Event } from 'h3'
export async function requireRealmRole(event: H3Event, ...roles: string[]) {
const session = await getUserSession(event)
const userRoles: string[] = session?.user?.realm_access?.roles ?? []
if (!roles.some((role) => userRoles.includes(role))) {
throw createError({ statusCode: 403, statusMessage: 'Forbidden' })
}
return session
}// server/api/admin/users.get.ts
export default defineEventHandler(async (event) => {
await requireRealmRole(event, 'admin')
// ...admin-only logic
})Calling Downstream APIs with the Access Token
// server/api/orders.get.ts
export default defineEventHandler(async (event) => {
const { token } = await getUserSession(event)
return $fetch('https://api.example.com/orders', {
headers: { Authorization: `Bearer ${token?.accessToken}` },
})
})automaticRefresh: true (set in Step 3) handles silent token refresh server-side, so token.accessToken is always current when read inside a handler.
Production Considerations
- Set
NUXT_OIDC_SESSION_SECRETand both Skycloak env vars via your hosting platform’s secret manager, never.envin production. - Ensure
redirectUrimatches exactly (protocol, host, path, trailing slash) what’s registered in the Skycloak Application’s Redirect URIs — a mismatch is the most common cause ofinvalid_redirect_urihere. - Serve over HTTPS in production;
nuxt-oidc-authmarks session cookiesSecureautomatically whenNODE_ENV=production.
Troubleshooting
-
invalid_redirect_urifrom Skycloak — the registered Redirect URI must matchNUXT_OIDC_SKYCLOAK_REDIRECT_URIexactly, including scheme and port. -
Session not persisting across requests — confirm
NUXT_OIDC_SESSION_SECRETis set and identical across all server instances (a per-instance random secret breaks sessions behind a load balancer). -
loggedInstaysfalseafter callback — check the browser console for third-party cookie blocking if the app and Skycloak cluster are on different top-level domains.