Keycloak Authentication with Vue3 + Pinia

Erin Lim
7 min readMay 10, 2023

--

Greetings comrades!

Chances are you’re here because you were trying integrate Keycloak authentication into your Vue 3 application with Pinia but has been cracking your head because of the lack of examples & documentation?

No? Really?

Well, here’s a guide on how you can do that anyways. 😜

But before that…

The purpose of this tutorial is to show you how to integrate Keycloak into your Vue 3 application with Pinia. If you’re here hoping to learn how to setup Keycloak then I’m sure you can find references to do just that somewhere else.

Now that that’s out of the way, let’s begin!

Things you’ll need

The Realm name, Client ID & URL to your Keycloak authentication, which you can find somewhere in your Keycloak admin dashboard settings.

Create a new Vue 3 project

For starters, we need a new Vue 3 project, so let’s create a new one with Vite!

Please refer to Vite’s official guide on how to do that.

Install npm packages

We’ll need these npm packages installed into our application for it to work.

npm i axios cors keycloak-js pinia pinia-plugin-persistedstate vue-router

You can read up more about each of these packages through their own official documentation.

However, the important thing to note is that we’re going to use pinia-plugin-persistedstate with Pinia because without it, our store states would be lost every time we refresh our application.

Create a simple Pinia store

Let’s create a new file under src > stores called authStore.js.

// file: src/stores/authStore.js

import { defineStore } from "pinia";

export const useAuthStore = defineStore({
id: "storeAuth",
state: () => {
return {
authenticated: false,
user: {},
test: false
}
},
persist: true,
getters: {},
actions: {
testAction() {
this.test = !this.test;
}
}
});

To be able to call our store globally within our application, we would also need to create a plugin for it under the src > plugins directory.

// file: src/plugins/authStore.js

import { useAuthStore } from "@/stores/authStore.js";

// Setup auth store as a plugin so it can be accessed globally in our FE
const authStorePlugin = {
install(app, option) {
app.config.globalProperties.$store = useAuthStore(option.pinia);
}
}

export default authStorePlugin;

Now let’s head over to the main.js file & add these in.

// file: src/main.js

import { createApp } from 'vue';
import { createPinia } from 'pinia';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
import App from './App.vue';
import router from './router';
import AuthStorePlugin from './plugins/authStore';

// Styles
import './style.css';

// Create Pinia instance
const pinia = createPinia();

// Use persisted state with Pinia so our store data will persist even after page refresh
pinia.use(piniaPluginPersistedstate);

const renderApp = () => {
const app = createApp(App);
app.use(AuthStorePlugin, { pinia });
app.use(pinia);
app.use(router);
app.mount('#app');
}

renderApp();

Let’s create a router file under the router directory, where we’ll declare all the routes to our application.

// file: src/router/index.js

import { createWebHistory, createRouter } from "vue-router";

// COMPONENTS
import Home from '@components/Home.vue';

const routes = [
{
path: "/",
name: "Home",
component: Home
}
];

const router = createRouter({
history: createWebHistory(),
routes, // short for `routes: routes`
});

export default router

Before we forget, let’s update our App.vue file.

<template>
<router-view />
</template>

Home component is actually the same component that was originally generated by Vite, renamed & with a few extra buttons.

Run this in the FE directory :

npm run dev

You should see your Vue 3 app running on localhost with persisted state working.

To test the persisted state, click on the “Test” button & you should see the value toggling from false to true.

Refresh the page & the value should still be true. This means our persisted state is working as expected.

Create a service for Keycloak

We’re going to create a service for Keycloak under src > services > keycloak.js so that our store can use it.

(Please define all the env variables in your .env file)

// file: src/services/keycloak.js

import Keycloak from 'keycloak-js';

const options = {
url: import.meta.env.VITE_KEYCLOAK_URL,
clientId: import.meta.env.VITE_KEYCLOAK_CLIENT_ID,
realm: import.meta.env.VITE_KEYCLOAK_REALM
}

const keycloak = new Keycloak(options);
let authenticated;
let store = null;

/**
* Initializes Keycloak, then run callback. This will prompt you to login.
*
* @param onAuthenticatedCallback
*/
async function init(onInitCallback) {
try {
authenticated = await keycloak.init({ onLoad: "login-required" })
onInitCallback()
} catch (error) {
console.error("Keycloak init failed")
console.error(error)
}
};

/**
* Initializes store with Keycloak user data
*
*/
async function initStore(storeInstance) {
try {
store = storeInstance
store.initOauth(keycloak)

// Show alert if user is not authenticated
if (!authenticated) { alert("not authenticated") }
} catch (error) {
console.error("Keycloak init failed")
console.error(error)
}
};

/**
* Logout user
*/
function logout(url) {
keycloak.logout({ redirectUri: url });
}

/**
* Refreshes token
*/
async function refreshToken() {
try {
await keycloak.updateToken(480);
return keycloak;
} catch (error) {
console.error('Failed to refresh token');
console.error(error);
}
}

const KeycloakService = {
CallInit: init,
CallInitStore: initStore,
CallLogout: logout,
CallTokenRefresh: refreshToken
};

export default KeycloakService;

We’re keeping the service simple with only 4methods:

  • init() will initialize Keycloak & asks user to login.
  • initStore() will store Keycloak user data to the Pinia store.
  • logout() will… well… logout the user.
  • refreshToken() will refresh user’s access token.

Pretty straightforward.

Import Keycloak service into main.js

Add these 2 lines into the file for Keycloak to tell user to login on render.

// file: src/main.js

import keycloakService from '@services/keycloak';

...

// renderApp(); // comment out the previous render app call

keycloakService.CallInit(renderApp);
// END OF FILE

Then head over to your authStore plugin & update the file with a few more lines…

// file: src/plugins/authStore.js

import { useAuthStore } from "@/stores/authStore.js";
import keycloakService from '@services/keycloak';

// Setup auth store as a plugin so it can be accessed globally in our FE
const authStorePlugin = {
install(app, option) {
const store = useAuthStore(option.pinia);

// Global store
app.config.globalProperties.$store = store;

// Store keycloak user data into store
keycloakService.CallInitStore(store);
}
}

export default authStorePlugin;

You should be automatically redirected to the Keycloak login page that looks something like this.

This means our Keycloak has been successfully initialized when our Vue app renders

Import Keycloak service into Pinia store

Let’s update our authStore.js & add a few more actions.

// file: src/stores/authStore.js

import { defineStore } from "pinia";
import keycloakService from '@services/keycloak';

...

actions: {
testAction() {
this.test = !this.test;
},
// Initialize Keycloak OAuth
async initOauth(keycloak, clearData = true) {
if(clearData) { await this.clearUserData(); }

this.authenticated = keycloak.authenticated;
this.user.username = keycloak.idTokenParsed.preferred_username;
this.user.token = keycloak.token;
this.user.refToken = keycloak.refreshToken;
},
// Logout user
async logout() {
try {
await keycloakService.CallLogout(import.meta.env.VITE_APP_URL);
await this.clearUserData();
} catch (error) {
console.error(error);
}
},
// Refresh user's token
async refreshUserToken() {
try {
const keycloak = await keycloakService.CallTokenRefresh();
this.initOauth(keycloak, false);
} catch (error) {
console.error(error);
}
},
// Clear user's store data
clearUserData() {
this.authenticated = false;
this.user = {};
}
}

...

You should now be able to logout from Keycloak when you press the “Logout” button.

Create a Token Interceptor for API calls

Let’s imagine that we also have a Node + Express backend that will be verifying our Keycloak user’s access token, to do that we would need a token interceptor for all axios requests being sent from our Vue app.

Let’s create an API service for axios.

// file: src/services/api.js

import axios from "axios";

// Creating an instance for axios to be used by the token interceptor service
const instance = axios.create({
baseURL: `${import.meta.env.VITE_BE_API_URL}/api`,
headers: {
"Content-Type": "application/json",
},
});

export default instance;

VITE_BE_API_URL should be the URL to you Node + Express backend

Then create a Token Interceptor service.

// file: src/services/tokenInterceptors.js

import axiosInstance from "@services/api";

const setup = (store) => {
axiosInstance.interceptors.request.use(
(config) => {
// If user is authenticated, place access token in request header.
if (store.authenticated) {
config.headers["x-access-token"] = store.user.token;
}

return config;
},
(error) => {
return Promise.reject(error);
}
);

axiosInstance.interceptors.response.use(
(res) => {
return res;
},
async (error) => {
const oriConfig = error.config;

if (error.response?.status === 401 && !oriConfig._retry) {
oriConfig._retry = true;

try {
// Refresh token then retry once
await store.refreshUserToken();

// Place refreshed access token in the request header
oriConfig.headers.headers["x-access-token"] = store.user.token;

return axiosInstance(oriConfig);
} catch (_error) {
console.error("Refresh token failed");
console.error(_error);

return Promise.reject(_error);
}
}

return Promise.reject(error);
}
);
};

export default setup;

The token interceptor will place the user’s access token in the request header, if authenticated. It will also refresh the token & retry with the new token if failed the first time.

Now import the token interceptor into our authStore plugin.

// file: src/plugins/authStore.js

import setupInterceptors from '@services/tokenInterceptors';

...

// Store keycloak user data into store
keycloakService.CallInitStore(store);

// Setup token interceptor so every FE API calls will have the access token for BE to verify
setupInterceptors(store);

...

Time to put everything together

If you have everything setup properly, you should be prompted to login then see your username & tokens displayed on the Home page after each successful login.

Username, Token & Refresh Token redacted for security purposes.
  • Click on the “Test” button to test persisted state.
  • Click on the “Refresh Token” button & you should see your tokens being updated.
  • Click on the “Backend Validation” button to see how you can send access token through API calls.
  • Click on the “Logout” button to logout user.

Final Thoughts

And that is how you setup Vue 3 with Pinia, persisted state & Keycloak.

Thank you for reading this guide just as much as I enjoyed writing it.
I hope you were able to learn something from it & apply this to your applications. Feel free to comment, discuss or clap!

And if you need the source code for reference, you can find them here.

Till next time! 👋

--

--

Erin Lim

Photographer turned Full Stack Developer. Tech nerd, loves music, gaming & memes..