Comprehensive Keycloak authentication module for Nuxt 4 with SSR support, auto token refresh, role-based guards, and TypeScript.
- 🔐 Hybrid Authentication - Support for both client-side (SPA) and server-side rendering (SSR) modes
- 🔄 Auto Token Refresh - Automatically refresh access tokens before expiration
- 🛡️ Role-Based Guards - Protect routes with realm and client/resource roles
- 📘 TypeScript Support - Full type safety with comprehensive TypeScript definitions
- 🔍 Silent Check-SSO - Non-intrusive authentication check for optional auth (auto-generated HTML file)
- 🔑 JWT Verification - Secure server-side token validation using JWKS with jose
- ⚙️ Configurable - Every feature can be toggled via configuration
- 🎯 Production Ready - Error handling, logging, and security best practices
- 🚀 Zero Config - Silent Check-SSO HTML automatically generated in public directory
- Install the module:
pnpm add nuxt-keycloak
# or
npm install nuxt-keycloak
# or
yarn add nuxt-keycloak- Add nuxt-keycloakto themodulessection ofnuxt.config.ts:
export default defineNuxtConfig({
  modules: ['nuxt-keycloak'],
  keycloak: {
    url: 'http://localhost:8080',
    realm: 'your-realm',
    clientId: 'your-client-id',
  }
})- Configure environment variables (optional):
NUXT_PUBLIC_KEYCLOAK_URL=http://localhost:8080
NUXT_PUBLIC_KEYCLOAK_REALM=your-realm
NUXT_PUBLIC_KEYCLOAK_CLIENT_ID=your-client-idThat's it! You can now use Keycloak authentication in your Nuxt app ✨
Use the useKeycloak() composable in your components:
<script setup>
const { isAuthenticated, user, login, logout } = useKeycloak()
</script>
<template>
  <div>
    <div v-if="!isAuthenticated">
      <button @click="login()">Login</button>
    </div>
    <div v-else>
      <p>Welcome, {{ user?.username }}!</p>
      <button @click="logout()">Logout</button>
    </div>
  </div>
</template>Protect entire pages by adding middleware:
<!-- pages/dashboard.vue -->
<script setup>
definePageMeta({
  middleware: 'keycloak-auth'
})
</script>Protect pages based on Keycloak roles:
<!-- pages/admin.vue -->
<script setup>
definePageMeta({
  middleware: ['keycloak-auth', 'keycloak-role'],
  keycloakRoles: {
    realm: ['admin'], // Requires 'admin' realm role
    resource: {  // OR requires client role
      clientId: 'my-app',
      roles: ['manager']
    }
  }
})
</script>Server utilities are automatically available in Nuxt server routes (no import needed):
// server/api/protected.ts
export default defineEventHandler(async (event) => {
  // Require authentication - auto-imported
  const user = requireKeycloakAuth(event)
  return {
    message: 'Protected data',
    user
  }
})// server/api/admin.ts
export default defineEventHandler(async (event) => {
  // Require specific role - auto-imported
  const user = requireRealmRole(event, 'admin')
  return {
    message: 'Admin data',
    user
  }
})You can also explicitly import from nuxt-keycloak/server:
// server/api/custom.ts
import { extractToken, verifyKeycloakToken, requireRealmRole } from 'nuxt-keycloak/server'
export default defineEventHandler(async (event) => {
  const token = extractToken(event)
  const user = await verifyKeycloakToken(token)
  // Use imported functions
  requireRealmRole(event, 'admin')
  return { user }
})When deploying behind Traefik or another reverse proxy that already verifies JWT tokens, you can configure the module to skip JWKS verification and simply decode tokens:
export default defineNuxtConfig({
  keycloak: {
    url: process.env.NUXT_PUBLIC_KEYCLOAK_URL,
    realm: process.env.NUXT_PUBLIC_KEYCLOAK_REALM,
    clientId: process.env.NUXT_PUBLIC_KEYCLOAK_CLIENT_ID,
    server: {
      verifyToken: 'decode', // Decode only, trust proxy verification
      middleware: true,
    },
  }
})Or use environment variable:
NUXT_KEYCLOAK_SERVER_VERIFY_TOKEN=decodeImportant: Only use verifyToken: 'decode' when tokens are pre-verified by a trusted reverse proxy (Traefik ForwardAuth, etc.). This mode skips all signature and expiration validation.
For more control, manually verify tokens:
// server/api/custom.ts
export default defineEventHandler(async (event) => {
  const token = extractToken(event)
  if (!token) {
    throw createError({
      statusCode: 401,
      message: 'No token provided'
    })
  }
  const user = await verifyKeycloakToken(token)
  if (!user) {
    throw createError({
      statusCode: 401,
      message: 'Invalid token'
    })
  }
  // Check custom logic
  if (!hasRealmRole(event, 'special-access')) {
    throw createError({
      statusCode: 403,
      message: 'Forbidden'
    })
  }
  return { user }
})All configuration options:
export default defineNuxtConfig({
  keycloak: {
    // Keycloak server URL
    url: 'http://localhost:8080',
    // Realm name
    realm: 'my-realm',
    // Client ID (must be a public client)
    clientId: 'my-client',
    // Keycloak initialization options
    initOptions: {
      onLoad: 'check-sso', // or 'login-required'
      pkceMethod: 'S256',
      flow: 'standard',
      checkLoginIframe: false,
      silentCheckSsoRedirectUri: 'http://localhost:3000/silent-check-sso.html'
    },
    // Server-side configuration
    server: {
      verifyToken: true, // true = full JWKS verification, false = disabled, 'decode' = decode only (for reverse proxy scenarios)
      middleware: false, // Enable global middleware
      jwksCacheDuration: 600000, // JWKS cache duration (10 minutes)
      rejectUnauthorized: true // Verify SSL certificates for JWKS endpoint
    },
    // Client-side configuration
    client: {
      autoRefreshToken: true, // Auto-refresh tokens
      minTokenValidity: 30, // Refresh when < 30s validity
      persistRefreshToken: true // Store refresh token in localStorage
    }
  }
})Use environment variables for different environments:
# Public (exposed to client)
NUXT_PUBLIC_KEYCLOAK_URL=https://keycloak.example.com
NUXT_PUBLIC_KEYCLOAK_REALM=production
NUXT_PUBLIC_KEYCLOAK_CLIENT_ID=nuxt-app
NUXT_PUBLIC_APP_URL=https://app.example.com
# Private (server-only)
NUXT_KEYCLOAK_SERVER_VERIFY_TOKEN=true  # true, false, or 'decode'
NUXT_KEYCLOAK_SERVER_MIDDLEWARE=falseReturns:
- keycloak- Keycloak instance (client-side only)
- isAuthenticated- Authentication status
- isInitialized- Initialization status
- user- User profile data
- token- Access token
- tokenParsed- Parsed token claims
- login(redirectUri?)- Redirect to login
- logout(redirectUri?)- Logout user
- register(redirectUri?)- Redirect to registration
- updateToken(minValidity?)- Manually refresh token
- loadUserProfile()- Load user profile
- hasRealmRole(role)- Check realm role
- hasResourceRole(role, resource?)- Check client role
- isTokenExpired(minValidity?)- Check token expiration
All utilities are auto-imported in Nuxt server routes. You can also explicitly import them from nuxt-keycloak/server:
import {
  verifyKeycloakToken,
  extractToken,
  requireKeycloakAuth
} from 'nuxt-keycloak/server'Available Functions:
- verifyKeycloakToken(token)- Verify JWT token using JWKS
- extractToken(event)- Extract Bearer token from Authorization header
- getKeycloakUser(event)- Get authenticated user from context
- getKeycloakToken(event)- Get access token from context
- isAuthenticated(event)- Check if user is authenticated
- hasRealmRole(event, role)- Check if user has realm role
- hasResourceRole(event, role, resource?)- Check if user has client role
- requireKeycloakAuth(event)- Require authentication (throws 401 if not authenticated)
- requireRealmRole(event, role)- Require realm role (throws 403 if missing)
- requireResourceRole(event, role, resource?)- Require client role (throws 403 if missing)
- keycloak-auth- Protect routes (requires authentication)
- keycloak-role- Enforce role-based access (use with route meta)
- Login to Keycloak Admin Console
- Create a new realm or use existing
- Note the realm name
- Go to Clients → Create
- Client ID: Your app name (e.g., nuxt-app)
- Client Protocol: openid-connect
- Access Type: public(for SPA/Nuxt)
- Valid Redirect URIs: http://localhost:3000/*(or your app URL)
- Web Origins: http://localhost:3000(or your app URL)
- Save
- Standard Flow: Enabled
- Direct Access Grants: Enabled (for password grant)
- Implicit Flow: Disabled (use PKCE instead)
- Go to Roles → Add Role
- Create roles like admin,user,manager
- Assign roles to users
- Go to Users → Add User
- Set username, email, etc.
- Go to Credentials tab → Set password
- Go to Role Mappings → Assign roles
Check out the playground directory for comprehensive examples:
- Public Page - /- No authentication required
- Dashboard - /dashboard- Requires authentication
- Admin Panel - /admin- Requires 'admin' realm role
- Profile - /profile- Shows user info and token details
Make sure your Keycloak client has the correct Web Origins configured. Add your app's origin (e.g., http://localhost:3000).
Check that:
- client.autoRefreshTokenis- truein config
- Refresh token is valid and not expired
- Client has Standard Flow enabled in Keycloak
The module automatically creates /public/silent-check-sso.html for you. If you need to customize it:
- The file is automatically generated in your public/directory
- Configure initOptions.silentCheckSsoRedirectUriin config (e.g.,http://localhost:3000/silent-check-sso.html)
- Add the URL to Valid Redirect URIs in Keycloak client
Note: The file is auto-generated by the module on nuxt prepare/nuxt dev, so you don't need to manually create it.
Ensure:
- User has the required role assigned in Keycloak
- Route meta includes keycloakRolesconfiguration
- Both keycloak-authandkeycloak-rolemiddlewares are applied
Local development
# Install dependencies
pnpm install
# Generate type stubs
pnpm run dev:prepare
# Develop with the playground
pnpm run dev
# Build the playground
pnpm run dev:build
# Run ESLint
pnpm run lint
# Run Vitest
pnpm run test
pnpm run test:watch
# Release new version
pnpm run releaseThis project was inspired by and builds upon ideas from:
- vue-keycloak by José Miguel Gonçalves and Gery Hirschfeld (Apache 2.0)
Special thanks to the open-source community for their contributions to Keycloak integration solutions.
Copyright (c) 2025