Vue.js
View a live deployed version.
We will use the Phase Two Vue example code here, but the logic could easily be applied to any existing application.
This example uses Vue.js 3 with Vite and Vue Router. We're going to leverage oidc-client-ts to integrate OIDC authentication with the Vue app. 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 Vue folder within
/frameworks/vue. You need Node.js 24 and pnpm. -
Point the app at your Keycloak. The app reads the issuer URL of your realm from
VITE_OIDC_ISSUER_URIand the client ID fromVITE_OIDC_CLIENT_ID..envsets them to the hosted Phase Two demo realm. To use the local Keycloak from the examples repo, copy the sample file:cp .env.local.sample .env.localVite loads
.env.localover.env, so the app now uses the local Keycloak and itsvue-exampleclient:VITE_OIDC_ISSUER_URI=http://localhost:8080/auth/realms/p2examplesVITE_OIDC_CLIENT_ID=vue-exampleFor another Keycloak, create
.env.localthe same way, and setVITE_OIDC_ISSUER_URIto your realm's issuer URL andVITE_OIDC_CLIENT_IDto the ID of your client. The app runs in the browser, so it uses a public client and no client secret. The client needshttp://localhost:3000/*as valid redirect URI, and+as web origin and as valid post logout redirect URI. -
Run
pnpm installand thenpnpm dev. The app runs on localhost:3000. -
We'll review where we configure the OIDC client. Open
src/auth.ts. It creates the oidc-client-tsUserManagerfrom the environment variables:import { UserManager } from 'oidc-client-ts'export const userManager = new UserManager({authority: import.meta.env.VITE_OIDC_ISSUER_URI,client_id: import.meta.env.VITE_OIDC_CLIENT_ID,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',})oidc-client-ts logs users in with the authorization code flow and PKCE. It keeps the tokens in session storage and renews them before they expire.
-
With the
UserManagerdefined, we expose it to the components through a composable. Switch tosrc/composables/useAuth.ts. It keeps the signed-in user in reactive state, which theUserManagerevents update, anduseAuth()returns that state with thesignInandsignOutactions:import type { User } from 'oidc-client-ts'import { shallowRef } from 'vue'import { userManager } from '@/auth'const user = shallowRef<User | null>(null)const error = shallowRef<Error | null>(null)userManager.events.addUserLoaded((loadedUser) => {user.value = loadedUsererror.value = null})userManager.events.addUserUnloaded(() => {user.value = null})userManager.events.addSilentRenewError((renewError) => {error.value = renewError})export async function initAuth() {const storedUser = await userManager.getUser()user.value = storedUser && !storedUser.expired ? storedUser : null}export function setAuthError(authError: unknown) {error.value = authError instanceof Error ? authError : new Error(String(authError))}export function useAuth() {return {user,error,signIn: () => userManager.signinRedirect({ state: { returnTo: window.location.pathname } }),signOut: () => userManager.signoutRedirect(),}}signInsends the current path along with the login, so the app can bring the user back to it.signOutalso ends the Keycloak session.src/main.tsloads the stored user withinitAuth()before it mounts the app:import './assets/main.css'import { createApp } from 'vue'import App from './App.vue'import { initAuth } from './composables/useAuth'import router from './router'await initAuth()createApp(App).use(router).mount('#app') -
There are a few routes in play here that the library leverages during the login.
src/router/index.tsadds/authand/silent-refreshnext to the home page:import { createRouter, createWebHistory } from 'vue-router'import AuthCallbackView from '@/views/AuthCallbackView.vue'import HomeView from '@/views/HomeView.vue'import SilentRefreshView from '@/views/SilentRefreshView.vue'const router = createRouter({history: createWebHistory(import.meta.env.BASE_URL),routes: [{ path: '/', name: 'home', component: HomeView },{ path: '/auth', name: 'auth-callback', component: AuthCallbackView },{ path: '/silent-refresh', name: 'silent-refresh', component: SilentRefreshView },],})export default routerKeycloak redirects the user back to
/authafter the login.src/views/AuthCallbackView.vuecompletes the login and returns to the page it started from:<script setup lang="ts">import { onMounted } from 'vue'import { useRouter } from 'vue-router'import { userManager } from '@/auth'import { setAuthError } from '@/composables/useAuth'const router = useRouter()onMounted(async () => {try {const user = await userManager.signinCallback()const state = user?.state as { returnTo?: string } | undefinedawait router.replace(state?.returnTo ?? '/')} catch (callbackError) {setAuthError(callbackError)await router.replace('/')}})</script>src/views/SilentRefreshView.vuehandles silent renewals on/silent-refresh:<script setup lang="ts">import { onMounted } from 'vue'import { userManager } from '@/auth'onMounted(() => userManager.signinSilentCallback())</script> -
Now that we have all the things set up, the
src/components/UserStatus.vuecomponent reads the user's state withuseAuth()and displays the appropriate UI:<script setup lang="ts">import TokenPanels from '@/components/TokenPanels.vue'import { useAuth } from '@/composables/useAuth'const { user, error, signIn, signOut } = useAuth()const buttonClasses ='cursor-pointer rounded-md bg-indigo-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600'</script><template><div><div class="pb-8 text-xl italic">Your current status is:</div><template v-if="user"><div class="mb-2 text-2xl text-p2blue-700">Authenticated</div><div class="mb-6 text-p2blue-700"><div>{{ user.profile.name }}</div><div>{{ user.profile.email }}</div></div><button type="button" :class="buttonClasses" @click="signOut()">Log out</button><TokenPanels :user="user" /></template><template v-else-if="error"><div class="mb-2 text-2xl text-p2blue-700">Authentication error.</div><div class="mb-6">{{ error.message }}</div><button type="button" :class="buttonClasses" @click="signIn()">Log in</button></template><template v-else><div class="mb-6 text-2xl text-p2blue-700">Not authenticated.</div><button type="button" :class="buttonClasses" @click="signIn()">Log in</button></template></div></template>With this, the user object is now easily available. A simple
v-if="user"allows the app to determine what UI to show.src/components/TokenPanels.vuedecodes the access and ID tokens withjwt-decodeand shows their claims.