Nuxt.js Integration

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

  1. In Skycloak, navigate to your cluster → ApplicationsCreate Application
  2. Set Application Type to Confidential (server-side session, so the client secret stays on the Nuxt server)
  3. Add Redirect URIs: http://localhost:3000/auth/oidc/callback for local dev, plus your production URL
  4. 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-auth

3. 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-session
⚠️
NUXT_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_SECRET and both Skycloak env vars via your hosting platform’s secret manager, never .env in production.
  • Ensure redirectUri matches exactly (protocol, host, path, trailing slash) what’s registered in the Skycloak Application’s Redirect URIs — a mismatch is the most common cause of invalid_redirect_uri here.
  • Serve over HTTPS in production; nuxt-oidc-auth marks session cookies Secure automatically when NODE_ENV=production.

Troubleshooting

  1. invalid_redirect_uri from Skycloak — the registered Redirect URI must match NUXT_OIDC_SKYCLOAK_REDIRECT_URI exactly, including scheme and port.
  2. Session not persisting across requests — confirm NUXT_OIDC_SESSION_SECRET is set and identical across all server instances (a per-instance random secret breaks sessions behind a load balancer).
  3. loggedIn stays false after callback — check the browser console for third-party cookie blocking if the app and Skycloak cluster are on different top-level domains.

Next Steps

Last updated on