Skip to main content

Vue.js

View a live deployed version.

info

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.

  1. Clone the Phase Two example repo.

  2. Open the Vue folder within /frameworks/vue. You need Node.js 24 and pnpm.

  3. Point the app at your Keycloak. The app reads the issuer URL of your realm from VITE_OIDC_ISSUER_URI and the client ID from VITE_OIDC_CLIENT_ID. .env sets 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.local

    Vite loads .env.local over .env, so the app now uses the local Keycloak and its vue-example client:

    VITE_OIDC_ISSUER_URI=http://localhost:8080/auth/realms/p2examples
    VITE_OIDC_CLIENT_ID=vue-example

    For another Keycloak, create .env.local the same way, and set VITE_OIDC_ISSUER_URI to your realm's issuer URL and VITE_OIDC_CLIENT_ID to the ID of your client. The app runs in the browser, so it uses a public client and no client secret. The client needs http://localhost:3000/* as valid redirect URI, and + as web origin and as valid post logout redirect URI.

  4. Run pnpm install and then pnpm dev. The app runs on localhost:3000.

  5. We'll review where we configure the OIDC client. Open src/auth.ts. It creates the oidc-client-ts UserManager from 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.

  6. With the UserManager defined, we expose it to the components through a composable. Switch to src/composables/useAuth.ts. It keeps the signed-in user in reactive state, which the UserManager events update, and useAuth() returns that state with the signIn and signOut actions:

    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 = loadedUser
    error.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(),
    }
    }

    signIn sends the current path along with the login, so the app can bring the user back to it. signOut also ends the Keycloak session.

    src/main.ts loads the stored user with initAuth() 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')
  7. There are a few routes in play here that the library leverages during the login. src/router/index.ts adds /auth and /silent-refresh next 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 router

    Keycloak redirects the user back to /auth after the login. src/views/AuthCallbackView.vue completes 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 } | undefined
    await router.replace(state?.returnTo ?? '/')
    } catch (callbackError) {
    setAuthError(callbackError)
    await router.replace('/')
    }
    })
    </script>

    src/views/SilentRefreshView.vue handles silent renewals on /silent-refresh:

    <script setup lang="ts">
    import { onMounted } from 'vue'
    import { userManager } from '@/auth'

    onMounted(() => userManager.signinSilentCallback())
    </script>
  8. Now that we have all the things set up, the src/components/UserStatus.vue component reads the user's state with useAuth() 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.vue decodes the access and ID tokens with jwt-decode and shows their claims.