Nuxt
This example uses Nuxt 4. There are a couple methods by which you can integrate Keycloak to your Nuxt application. We're going to explore two methods here, one uses keycloak-js and the other leverages oidc-client-ts. The keycloak-js library provides a simple, client-only method, but lacks some of the sophistication provided by the oidc-client-ts library that is heavily supported and more widely used.
View a live deployed version for keycloak-js and oidc-client-ts.
Both examples render in the browser (ssr: false) and log users in with the authorization code flow and PKCE. You need Node.js 24 and pnpm.
Both examples need a public OIDC client, without a client secret: client authentication off, the standard flow enabled, http://localhost:3000/* as valid redirect URI, and + as web origin and as valid post logout redirect URI. Both run on port 3000, so one client works for both.
Nuxt with keycloak-js
-
Clone the Phase Two example repo.
-
Open the Nuxt folder within
/frameworks/nuxtand open thekeycloak-jsfolder within/frameworks/nuxt/keycloak-js. -
Point the app at your Keycloak. The defaults in
nuxt.config.tspoint at the hosted Phase Two demo realm:runtimeConfig: {public: {keycloakUrl: "https://app.phasetwo.io/auth",keycloakRealm: "p2examples",keycloakClientId: "nuxt-example",},},Nuxt overrides them with the matching environment variables:
NUXT_PUBLIC_KEYCLOAK_URL,NUXT_PUBLIC_KEYCLOAK_REALMandNUXT_PUBLIC_KEYCLOAK_CLIENT_ID. To use the local Keycloak from the examples repo, copy the example file:cp .env.example .env.envpoints the app at the local Keycloak and itsnuxt-exampleclient:NUXT_PUBLIC_KEYCLOAK_URL=http://localhost:8080/authNUXT_PUBLIC_KEYCLOAK_REALM=p2examplesNUXT_PUBLIC_KEYCLOAK_CLIENT_ID=nuxt-exampleFor another Keycloak, create
.envthe same way. keycloak-js takes the Keycloak URL and the realm separately, so split your realm's issuer URL,https://<your-keycloak-host>/auth/realms/<your-realm>: setNUXT_PUBLIC_KEYCLOAK_URLtohttps://<your-keycloak-host>/authandNUXT_PUBLIC_KEYCLOAK_REALMto<your-realm>. SetNUXT_PUBLIC_KEYCLOAK_CLIENT_IDto the ID of your client. -
Run
pnpm installand thenpnpm dev, and open localhost:3000.keycloak-jsis a JavaScript library that provides a fast way to secure an application. -
The project makes use of the following Nuxt items, all in the
app/folder: components, composables, layouts, pages and plugins. We'll review the ones that handle the login. -
The main component that shows the user's authenticated state is in
app/components/UserStatus.vue. In this component we call theuseKeycloakcomposable, which returns the login state and theloginandlogoutfunctions that we've wrapped to make easily available.const { state, login, logout } = useKeycloak();Lower in the file the component leverages
v-ifchecks onstate.authenticatedandstate.error. Depending on the state, it shows the user's name and email with a Log out button, an error, or a Log in button. TheTokenPanelscomponent shows the decoded tokens. -
Let's take a look at the setup for the composable next. Our composable is in
app/composables/useKeycloak.ts. A composable is a function defined that can be called anywhere in the Nuxt application. It's a good way to abstract logic to be reused. In our caseuseKeycloakState()holds the authenticated state withuseState, anduseKeycloak()wraps thekeycloak-jsadapter that the plugin provides (more on that in the next step).import type { KeycloakTokenParsed } from "keycloak-js";type KeycloakState = {authenticated: boolean;tokenParsed?: KeycloakTokenParsed;idTokenParsed?: KeycloakTokenParsed;error?: string;};export const useKeycloakState = () =>useState<KeycloakState>("keycloak", () => ({ authenticated: false }));export function useKeycloak() {const { $keycloak } = useNuxtApp();return {state: useKeycloakState(),login: () => $keycloak.login(),logout: () =>$keycloak.logout({ redirectUri: `${window.location.origin}/` }),};}logoutends the Keycloak session and sends the user back to the app. -
In the plugin,
app/plugins/keycloak.client.ts, we instantiate thekeycloak-jsadapter with the values from the runtime config. We then provide that instance to the app as$keycloak.import Keycloak from "keycloak-js";export default defineNuxtPlugin(async () => {const { keycloakUrl, keycloakRealm, keycloakClientId } =useRuntimeConfig().public;const keycloak = new Keycloak({url: keycloakUrl,realm: keycloakRealm,clientId: keycloakClientId,});const state = useKeycloakState();const sync = () => {state.value = {...state.value,authenticated: keycloak.authenticated === true,tokenParsed: keycloak.tokenParsed,idTokenParsed: keycloak.idTokenParsed,};};keycloak.onAuthSuccess = sync;keycloak.onAuthRefreshSuccess = sync;keycloak.onAuthLogout = sync;keycloak.onTokenExpired = () => {keycloak.updateToken(30).catch(() => {keycloak.clearToken();sync();});};try {await keycloak.init({onLoad: "check-sso",silentCheckSsoRedirectUri: `${window.location.origin}/silent-check-sso.html`,pkceMethod: "S256",checkLoginIframe: false,});} catch (error) {state.value = {...state.value,error:error instanceof Error? error.message: "Could not initialize keycloak-js",};}sync();return { provide: { keycloak } };});Nuxt waits for the plugin before it renders the app, so the logged-out UI doesn't flash while keycloak-js starts. With
check-sso, keycloak-js logs the user in only if they already have a Keycloak session. It checks in a hidden iframe that loadspublic/silent-check-sso.html, so the page doesn't reload:<!doctype html><html><body><script>parent.postMessage(location.href, location.origin);</script></body></html>When the access token expires, the plugin refreshes it with
updateToken. The event handlers copy the adapter's state intouseKeycloakState(), so the components update. -
The logic for checking the
authenticatedstate can be used to expand in ways to secure your site in a number of ways.
Nuxt with oidc-client-ts
The oidc-client-ts package is a well-maintained and used library. It provides a lot of utilities for building out a fully production app.
-
Clone the Phase Two example repo.
-
Open the Nuxt folder within
/frameworks/nuxtand open theoidc-client-tsfolder within/frameworks/nuxt/oidc-client-ts. -
Point the app at your Keycloak. The defaults in
nuxt.config.tspoint at the hosted Phase Two demo realm:runtimeConfig: {public: {oidcIssuerUri: "https://app.phasetwo.io/auth/realms/p2examples",oidcClientId: "nuxt-oidc-client-ts-example",},},Nuxt overrides them with the matching environment variables:
NUXT_PUBLIC_OIDC_ISSUER_URIandNUXT_PUBLIC_OIDC_CLIENT_ID. To use the local Keycloak from the examples repo, copy the example file:cp .env.example .env.envpoints the app at the local Keycloak and itsnuxt-oidc-client-ts-exampleclient:NUXT_PUBLIC_OIDC_ISSUER_URI=http://localhost:8080/auth/realms/p2examplesNUXT_PUBLIC_OIDC_CLIENT_ID=nuxt-oidc-client-ts-exampleFor another Keycloak, create
.envthe same way, and setNUXT_PUBLIC_OIDC_ISSUER_URIto your realm's issuer URL andNUXT_PUBLIC_OIDC_CLIENT_IDto the ID of your client. -
Run
pnpm installand thenpnpm dev, and open localhost:3000. -
The structure of the project is similar to the
keycloak-jsversion but with the use of a store and a middleware. -
We'll review where we configure the OIDC client. Open the plugin
app/plugins/oidc.client.ts. It creates a singleUserManagerfrom the runtime config and provides it to the app as$userManager. TheUserManageris theoidc-client-tsobject that logs users in and out.import { UserManager } from "oidc-client-ts";export default defineNuxtPlugin(() => {const { oidcIssuerUri, oidcClientId } = useRuntimeConfig().public;const userManager = new UserManager({authority: oidcIssuerUri,client_id: oidcClientId,redirect_uri: `${window.location.origin}/auth`,silent_redirect_uri: `${window.location.origin}/silent-refresh`,post_logout_redirect_uri: `${window.location.origin}/`,scope: "openid profile email",});return { provide: { userManager } };});oidc-client-tslogs users in with the authorization code flow and PKCE. It keeps the tokens in session storage and renews them before they expire. -
We leverage the
pinialibrary to store the user information and make it easily accessible. Openapp/stores/auth.ts. From within this file, we wrap theUserobject exposed by theoidc-client-tspackage, which theUserManagerevents keep up to date. The store also exposessignIn,signOutand the handlers for the login callbacks.import type { User } from "oidc-client-ts";export const useAuthStore = defineStore("auth", () => {const { $userManager } = useNuxtApp();const user = shallowRef<User | null>(null);const error = shallowRef<string | null>(null);let initialized = false;$userManager.events.addUserLoaded((loadedUser) => {user.value = loadedUser;error.value = null;});$userManager.events.addUserUnloaded(() => {user.value = null;});$userManager.events.addSilentRenewError((renewError) => {error.value = renewError.message;});async function init() {if (initialized) {return;}initialized = true;const storedUser = await $userManager.getUser();user.value = storedUser && !storedUser.expired ? storedUser : null;}function signIn() {return $userManager.signinRedirect({state: { returnTo: window.location.pathname },});}function signOut() {return $userManager.signoutRedirect();}async function handleCallback() {const signedInUser = await $userManager.signinCallback();const state = signedInUser?.state as { returnTo?: string } | undefined;return state?.returnTo ?? "/";}function handleSilentCallback() {return $userManager.signinSilentCallback();}function setError(authError: unknown) {error.value =authError instanceof Error ? authError.message : String(authError);}return {user,error,init,signIn,signOut,handleCallback,handleSilentCallback,setError,};});signInsends the current path along with the login, andhandleCallbackreturns it, so the user comes back to the page they started from.signOutalso ends the Keycloak session. -
There are a couple of pages in play here that the library leverages during the login.
app/pages/auth.vuecompletes the login when Keycloak redirects back to/auth, andapp/pages/silent-refresh.vuehandles silent renewals on/silent-refresh. For instance inapp/pages/auth.vue:<script setup lang="ts">const auth = useAuthStore();const router = useRouter();onMounted(async () => {try {await router.replace(await auth.handleCallback());} catch (callbackError) {auth.setError(callbackError);await router.replace("/");}});</script> -
We have also created a middleware file in
app/middleware/auth.global.ts. Nuxt runs it before every route, and it loads the stored user into the store before the first page renders.export default defineNuxtRouteMiddleware(async () => {await useAuthStore().init();}); -
Now that we have all the things set up, the component
app/components/UserStatus.vuereads the user from the store.storeToRefskeepsuseranderrorreactive.const auth = useAuthStore();const { user, error } = storeToRefs(auth);With this, the user object is now easily available. A simple
v-if="user"allows the app to determine what UI to show, and the buttons callauth.signIn()andauth.signOut(). -
A bit more complicated of a setup, but more elegant in the handling of the logged in flow. The
oidc-client-tslibrary allows for much better fine-tuning of the experience.