Vue.js Integration

Vue.js Integration

This guide covers how to integrate Skycloak authentication into Vue.js applications using modern OAuth2/OIDC libraries and Vue best practices.

Prerequisites

  • Vue.js 3+ application (Vue 2 compatible with adjustments)
  • Node.js 14+
  • Skycloak cluster with configured realm and client
  • Basic understanding of Vue.js composition API and reactivity

Quick Start

1. Install Dependencies

npm install @vue-keycloak/keycloak keycloak-js
# or
npm install oidc-client-ts

For Vue 3 with TypeScript:

npm install @vue-keycloak/keycloak keycloak-js @types/keycloak-js

2. Configure Keycloak Plugin

Create src/plugins/keycloak.js:

import Keycloak from 'keycloak-js';

const keycloakConfig = {
  url: 'https://your-cluster-id.app.skycloak.io',
  realm: 'your-realm',
  clientId: 'your-vue-app',
};

const keycloak = new Keycloak(keycloakConfig);

export default keycloak;

3. Setup Vue Plugin

Create src/plugins/vue-keycloak.js:

import { reactive, readonly } from 'vue';
import keycloak from './keycloak';

const state = reactive({
  isAuthenticated: false,
  user: null,
  token: null,
  refreshToken: null,
  idToken: null,
  roles: [],
  loading: true,
  error: null,
});

const keycloakPlugin = {
  install(app, options = {}) {
    const {
      initOptions = {
        onLoad: 'check-sso',
        checkLoginIframe: false,
        silentCheckSsoRedirectUri: window.location.origin + '/silent-check-sso.html',
      },
      onReady = () => {},
      onInitError = () => {},
    } = options;

    // Initialize Keycloak
    keycloak
      .init(initOptions)
      .then((authenticated) => {
        state.isAuthenticated = authenticated;
        state.loading = false;

        if (authenticated) {
          updateState();
          setupTokenRefresh();
        }

        onReady(keycloak);
      })
      .catch((error) => {
        state.error = error;
        state.loading = false;
        onInitError(error);
      });

    // Update state from Keycloak
    const updateState = () => {
      state.token = keycloak.token;
      state.refreshToken = keycloak.refreshToken;
      state.idToken = keycloak.idToken;
      state.user = keycloak.tokenParsed;
      state.roles = extractRoles();
    };

    // Extract roles from token
    const extractRoles = () => {
      const realmRoles = keycloak.tokenParsed?.realm_access?.roles || [];
      const clientRoles =
        keycloak.tokenParsed?.resource_access?.[keycloakConfig.clientId]?.roles || [];
      return [...realmRoles, ...clientRoles];
    };

    // Setup automatic token refresh
    const setupTokenRefresh = () => {
      setInterval(() => {
        keycloak
          .updateToken(30)
          .then((refreshed) => {
            if (refreshed) {
              updateState();
              console.log('Token refreshed');
            }
          })
          .catch(() => {
            console.error('Failed to refresh token');
            state.isAuthenticated = false;
          });
      }, 60000); // Check every minute
    };

    // Keycloak methods
    const login = (options = {}) => {
      return keycloak.login(options);
    };

    const logout = (options = {}) => {
      return keycloak.logout(options);
    };

    const register = (options = {}) => {
      return keycloak.register(options);
    };

    const hasRole = (role) => {
      return state.roles.includes(role);
    };

    const hasAnyRole = (...roles) => {
      return roles.some((role) => hasRole(role));
    };

    const hasAllRoles = (...roles) => {
      return roles.every((role) => hasRole(role));
    };

    const getToken = async () => {
      try {
        await keycloak.updateToken(5);
        return keycloak.token;
      } catch (error) {
        console.error('Failed to get token:', error);
        throw error;
      }
    };

    // Provide globally
    app.config.globalProperties.$keycloak = keycloak;
    app.config.globalProperties.$auth = {
      state: readonly(state),
      login,
      logout,
      register,
      hasRole,
      hasAnyRole,
      hasAllRoles,
      getToken,
    };

    // Provide for composition API
    app.provide('keycloak', keycloak);
    app.provide('auth', {
      state: readonly(state),
      login,
      logout,
      register,
      hasRole,
      hasAnyRole,
      hasAllRoles,
      getToken,
    });
  },
};

export default keycloakPlugin;

4. Configure Main App

// main.js
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import keycloakPlugin from './plugins/vue-keycloak';

const app = createApp(App);

app.use(keycloakPlugin, {
  initOptions: {
    onLoad: 'check-sso',
    silentCheckSsoRedirectUri: window.location.origin + '/silent-check-sso.html',
    checkLoginIframe: false,
  },
  onReady: (keycloak) => {
    console.log('Keycloak ready', keycloak.authenticated);
    app.use(router);
    app.mount('#app');
  },
  onInitError: (error) => {
    console.error('Keycloak init error', error);
  },
});

5. Create Auth Composable

// composables/useAuth.js
import { inject, computed } from 'vue';

export function useAuth() {
  const auth = inject('auth');

  if (!auth) {
    throw new Error('Auth plugin not installed');
  }

  return {
    // State
    isAuthenticated: computed(() => auth.state.isAuthenticated),
    user: computed(() => auth.state.user),
    loading: computed(() => auth.state.loading),
    error: computed(() => auth.state.error),

    // User info
    userId: computed(() => auth.state.user?.sub),
    username: computed(() => auth.state.user?.preferred_username),
    email: computed(() => auth.state.user?.email),
    fullName: computed(() => {
      const user = auth.state.user;
      if (!user) return '';
      return `${user.given_name || ''} ${user.family_name || ''}`.trim();
    }),

    // Methods
    login: auth.login,
    logout: auth.logout,
    register: auth.register,
    hasRole: auth.hasRole,
    hasAnyRole: auth.hasAnyRole,
    hasAllRoles: auth.hasAllRoles,
    getToken: auth.getToken,
  };
}

Component Implementation

Login Component

<!-- components/LoginButton.vue -->
<template>
  <div>
    <button v-if="!isAuthenticated" @click="handleLogin" class="btn-primary">
      Login with Skycloak
    </button>
    <div v-else class="user-menu">
      <span>Welcome, {{ fullName || username }}!</span>
      <button @click="handleLogout" class="btn-secondary">Logout</button>
    </div>
  </div>
</template>

<script setup>
import { useAuth } from '@/composables/useAuth';
import { useRouter } from 'vue-router';

const router = useRouter();
const { isAuthenticated, username, fullName, login, logout } = useAuth();

const handleLogin = () => {
  login({
    redirectUri: window.location.origin + router.currentRoute.value.fullPath,
  });
};

const handleLogout = () => {
  logout({
    redirectUri: window.location.origin,
  });
};
</script>

Protected Route Component

<!-- components/ProtectedRoute.vue -->
<template>
  <div v-if="loading" class="loading">Loading authentication...</div>
  <div v-else-if="!isAuthenticated" class="unauthorized">
    <h2>Authentication Required</h2>
    <p>Please log in to access this page.</p>
    <button @click="login">Login</button>
  </div>
  <slot v-else />
</template>

<script setup>
import { useAuth } from '@/composables/useAuth';
import { onMounted } from 'vue';

const { isAuthenticated, loading, login } = useAuth();

onMounted(() => {
  if (!loading.value && !isAuthenticated.value) {
    // Optionally auto-redirect to login
    // login()
  }
});
</script>

Role-Based Component

<!-- components/RoleGuard.vue -->
<template>
  <div v-if="hasAccess">
    <slot />
  </div>
  <div v-else-if="showFallback" class="access-denied">
    <slot name="fallback">
      <p>You don't have permission to view this content.</p>
    </slot>
  </div>
</template>

<script setup>
import { computed } from 'vue';
import { useAuth } from '@/composables/useAuth';

const props = defineProps({
  roles: {
    type: [String, Array],
    required: true,
  },
  requireAll: {
    type: Boolean,
    default: false,
  },
  showFallback: {
    type: Boolean,
    default: true,
  },
});

const { hasRole, hasAnyRole, hasAllRoles } = useAuth();

const hasAccess = computed(() => {
  const roleArray = Array.isArray(props.roles) ? props.roles : [props.roles];

  if (props.requireAll) {
    return hasAllRoles(...roleArray);
  } else {
    return hasAnyRole(...roleArray);
  }
});
</script>

Router Guards

// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import { useAuth } from '@/composables/useAuth';

const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import('../views/Home.vue'),
  },
  {
    path: '/profile',
    name: 'Profile',
    component: () => import('../views/Profile.vue'),
    meta: { requiresAuth: true },
  },
  {
    path: '/admin',
    name: 'Admin',
    component: () => import('../views/Admin.vue'),
    meta: {
      requiresAuth: true,
      roles: ['admin'],
    },
  },
  {
    path: '/editor',
    name: 'Editor',
    component: () => import('../views/Editor.vue'),
    meta: {
      requiresAuth: true,
      roles: ['editor', 'admin'],
      requireAll: false,
    },
  },
];

const router = createRouter({
  history: createWebHistory(),
  routes,
});

// Navigation guard
router.beforeEach((to, from, next) => {
  const auth = useAuth();

  // Wait for auth to be ready
  if (auth.loading.value) {
    const unwatch = auth.$watch('loading', (loading) => {
      if (!loading) {
        unwatch();
        checkAuth();
      }
    });
    return;
  }

  checkAuth();

  function checkAuth() {
    const requiresAuth = to.matched.some((record) => record.meta.requiresAuth);
    const requiredRoles = to.meta.roles;

    if (requiresAuth && !auth.isAuthenticated.value) {
      // Store intended route
      sessionStorage.setItem('redirectPath', to.fullPath);
      auth.login();
      return;
    }

    if (requiredRoles && requiredRoles.length > 0) {
      const requireAll = to.meta.requireAll || false;
      const hasAccess = requireAll
        ? auth.hasAllRoles(...requiredRoles)
        : auth.hasAnyRole(...requiredRoles);

      if (!hasAccess) {
        next({ name: 'AccessDenied' });
        return;
      }
    }

    next();
  }
});

export default router;

API Integration

Axios Interceptor

// services/api.js
import axios from 'axios';
import { useAuth } from '@/composables/useAuth';

const api = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 10000,
});

// Request interceptor
api.interceptors.request.use(
  async (config) => {
    const auth = useAuth();

    if (auth.isAuthenticated.value) {
      try {
        const token = await auth.getToken();
        config.headers.Authorization = `Bearer ${token}`;
      } catch (error) {
        console.error('Failed to get token:', error);
      }
    }

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

// Response interceptor
api.interceptors.response.use(
  (response) => response,
  async (error) => {
    const auth = useAuth();

    if (error.response?.status === 401) {
      // Token might be expired
      if (auth.isAuthenticated.value) {
        // Try to refresh token
        try {
          await auth.getToken();
          // Retry original request
          return api.request(error.config);
        } catch (refreshError) {
          // Refresh failed, redirect to login
          auth.logout();
        }
      }
    }

    return Promise.reject(error);
  }
);

export default api;

API Service Example

// services/userService.js
import api from './api';

export const userService = {
  async getProfile() {
    const response = await api.get('/users/profile');
    return response.data;
  },

  async updateProfile(data) {
    const response = await api.put('/users/profile', data);
    return response.data;
  },

  async getUsers() {
    const response = await api.get('/users');
    return response.data;
  },

  async getUserById(id) {
    const response = await api.get(`/users/${id}`);
    return response.data;
  },
};

Pinia Store Integration

// stores/auth.js
import { defineStore } from 'pinia';
import { useAuth } from '@/composables/useAuth';

export const useAuthStore = defineStore('auth', {
  state: () => ({
    profile: null,
    preferences: null,
    permissions: [],
  }),

  getters: {
    isProfileComplete: (state) => {
      return state.profile?.firstName && state.profile?.lastName;
    },

    can: (state) => (permission) => {
      return state.permissions.includes(permission);
    },
  },

  actions: {
    async loadProfile() {
      const auth = useAuth();

      if (!auth.isAuthenticated.value) {
        throw new Error('User not authenticated');
      }

      try {
        const response = await api.get('/users/profile');
        this.profile = response.data;
      } catch (error) {
        console.error('Failed to load profile:', error);
        throw error;
      }
    },

    async updateProfile(data) {
      try {
        const response = await api.put('/users/profile', data);
        this.profile = response.data;
        return response.data;
      } catch (error) {
        console.error('Failed to update profile:', error);
        throw error;
      }
    },

    async loadPermissions() {
      try {
        const response = await api.get('/users/permissions');
        this.permissions = response.data;
      } catch (error) {
        console.error('Failed to load permissions:', error);
        throw error;
      }
    },

    reset() {
      this.profile = null;
      this.preferences = null;
      this.permissions = [];
    },
  },
});

Advanced Features

Silent Authentication

Create public/silent-check-sso.html:

<!DOCTYPE html>
<html>
  <head>
    <title>Silent SSO Check</title>
  </head>
  <body>
    <script>
      parent.postMessage(location.href, location.origin);
    </script>
  </body>
</html>

Custom Directives

// directives/auth.js
export const vAuth = {
  mounted(el, binding) {
    const { isAuthenticated } = useAuth();

    const updateVisibility = () => {
      el.style.display = isAuthenticated.value ? '' : 'none';
    };

    updateVisibility();

    // Watch for changes
    el._unwatchAuth = watch(isAuthenticated, updateVisibility);
  },

  unmounted(el) {
    el._unwatchAuth?.();
  },
};

export const vRole = {
  mounted(el, binding) {
    const { hasRole, hasAnyRole } = useAuth();
    const roles = Array.isArray(binding.value) ? binding.value : [binding.value];
    const requireAll = binding.modifiers.all;

    const updateVisibility = () => {
      const hasAccess = requireAll ? roles.every((role) => hasRole(role)) : hasAnyRole(...roles);

      el.style.display = hasAccess ? '' : 'none';
    };

    updateVisibility();

    // Watch for auth state changes
    const { state } = useAuth();
    el._unwatchRoles = watch(() => state.roles, updateVisibility);
  },

  unmounted(el) {
    el._unwatchRoles?.();
  },
};

// Register globally
app.directive('auth', vAuth);
app.directive('role', vRole);

Usage:

<template>
  <div>
    <!-- Only show for authenticated users -->
    <button v-auth @click="doSomething">Authenticated Action</button>

    <!-- Only show for users with admin role -->
    <div v-role="'admin'">Admin Panel</div>

    <!-- Only show for users with either role -->
    <div v-role="['editor', 'moderator']">Content Management</div>

    <!-- Only show for users with all roles -->
    <div v-role.all="['admin', 'super_user']">Super Admin Panel</div>
  </div>
</template>

Token Management Service

// services/tokenService.js
import { ref, computed } from 'vue';
import keycloak from '@/plugins/keycloak';

class TokenService {
  constructor() {
    this.tokenExpiry = ref(null);
    this.refreshInterval = null;
  }

  get isTokenExpired() {
    if (!this.tokenExpiry.value) return true;
    return new Date() >= this.tokenExpiry.value;
  }

  get timeUntilExpiry() {
    if (!this.tokenExpiry.value) return 0;
    return Math.max(0, this.tokenExpiry.value - new Date());
  }

  startTokenRefresh() {
    this.updateTokenExpiry();

    // Clear existing interval
    if (this.refreshInterval) {
      clearInterval(this.refreshInterval);
    }

    // Check token every 30 seconds
    this.refreshInterval = setInterval(() => {
      const timeLeft = this.timeUntilExpiry;

      // Refresh if less than 5 minutes left
      if (timeLeft < 5 * 60 * 1000) {
        this.refreshToken();
      }
    }, 30000);
  }

  async refreshToken() {
    try {
      const refreshed = await keycloak.updateToken(30);
      if (refreshed) {
        this.updateTokenExpiry();
        console.log('Token refreshed successfully');
      }
      return refreshed;
    } catch (error) {
      console.error('Failed to refresh token:', error);
      throw error;
    }
  }

  updateTokenExpiry() {
    if (keycloak.tokenParsed?.exp) {
      this.tokenExpiry.value = new Date(keycloak.tokenParsed.exp * 1000);
    }
  }

  stopTokenRefresh() {
    if (this.refreshInterval) {
      clearInterval(this.refreshInterval);
      this.refreshInterval = null;
    }
  }

  async getValidToken() {
    if (this.isTokenExpired) {
      await this.refreshToken();
    }
    return keycloak.token;
  }
}

export const tokenService = new TokenService();

Testing

Unit Tests

// tests/unit/composables/useAuth.spec.js
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { useAuth } from '@/composables/useAuth';

// Mock the inject function
vi.mock('vue', async () => {
  const actual = await vi.importActual('vue');
  return {
    ...actual,
    inject: vi.fn(),
  };
});

describe('useAuth', () => {
  let mockAuth;

  beforeEach(() => {
    mockAuth = {
      state: {
        isAuthenticated: true,
        user: {
          sub: 'user-123',
          preferred_username: 'testuser',
          email: '[email protected]',
          given_name: 'Test',
          family_name: 'User',
        },
        loading: false,
        error: null,
      },
      login: vi.fn(),
      logout: vi.fn(),
      hasRole: vi.fn(),
      hasAnyRole: vi.fn(),
      hasAllRoles: vi.fn(),
      getToken: vi.fn(),
    };

    vi.mocked(inject).mockReturnValue(mockAuth);
  });

  it('returns computed properties correctly', () => {
    const auth = useAuth();

    expect(auth.isAuthenticated.value).toBe(true);
    expect(auth.username.value).toBe('testuser');
    expect(auth.email.value).toBe('[email protected]');
    expect(auth.fullName.value).toBe('Test User');
  });

  it('calls login method correctly', () => {
    const auth = useAuth();
    const options = { redirectUri: 'http://localhost:3000' };

    auth.login(options);

    expect(mockAuth.login).toHaveBeenCalledWith(options);
  });

  it('checks roles correctly', () => {
    const auth = useAuth();
    mockAuth.hasRole.mockReturnValue(true);

    const result = auth.hasRole('admin');

    expect(mockAuth.hasRole).toHaveBeenCalledWith('admin');
    expect(result).toBe(true);
  });
});

Component Tests

// tests/unit/components/RoleGuard.spec.js
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import RoleGuard from '@/components/RoleGuard.vue';
import { useAuth } from '@/composables/useAuth';

vi.mock('@/composables/useAuth');

describe('RoleGuard', () => {
  it('shows content when user has required role', () => {
    vi.mocked(useAuth).mockReturnValue({
      hasAnyRole: vi.fn().mockReturnValue(true),
      hasAllRoles: vi.fn().mockReturnValue(true),
    });

    const wrapper = mount(RoleGuard, {
      props: {
        roles: 'admin',
      },
      slots: {
        default: '<div>Protected Content</div>',
      },
    });

    expect(wrapper.text()).toContain('Protected Content');
  });

  it('shows fallback when user lacks required role', () => {
    vi.mocked(useAuth).mockReturnValue({
      hasAnyRole: vi.fn().mockReturnValue(false),
      hasAllRoles: vi.fn().mockReturnValue(false),
    });

    const wrapper = mount(RoleGuard, {
      props: {
        roles: 'admin',
        showFallback: true,
      },
      slots: {
        fallback: '<div>Access Denied</div>',
      },
    });

    expect(wrapper.text()).toContain('Access Denied');
  });

  it('checks all roles when requireAll is true', () => {
    const hasAllRoles = vi.fn().mockReturnValue(true);
    vi.mocked(useAuth).mockReturnValue({
      hasAnyRole: vi.fn(),
      hasAllRoles,
    });

    mount(RoleGuard, {
      props: {
        roles: ['admin', 'editor'],
        requireAll: true,
      },
    });

    expect(hasAllRoles).toHaveBeenCalledWith('admin', 'editor');
  });
});

E2E Tests

// tests/e2e/auth.spec.js
import { test, expect } from '@playwright/test';

test.describe('Authentication Flow', () => {
  test('redirects to Keycloak login', async ({ page }) => {
    await page.goto('/profile');

    // Should redirect to Keycloak
    await expect(page).toHaveURL(/.*your-cluster\.skycloak\.com.*/);
    await expect(page).toHaveURL(/.*protocol\/openid-connect\/auth.*/);
  });

  test('shows user info after login', async ({ page }) => {
    // Mock authenticated state
    await page.addInitScript(() => {
      window.localStorage.setItem('kc-token', 'mock-token');
    });

    await page.goto('/');

    // Check for authenticated UI
    await expect(page.locator('text=Welcome')).toBeVisible();
    await expect(page.locator('button:has-text("Logout")')).toBeVisible();
  });

  test('protects admin routes', async ({ page }) => {
    // Mock authenticated state without admin role
    await page.addInitScript(() => {
      window.localStorage.setItem('kc-token', 'mock-token');
      window.mockRoles = ['user'];
    });

    await page.goto('/admin');

    // Should show access denied
    await expect(page.locator('text=Access Denied')).toBeVisible();
  });
});

Production Considerations

Environment Configuration

// .env.production
VITE_KEYCLOAK_URL=https://auth.yourcompany.com
VITE_KEYCLOAK_REALM=production
VITE_KEYCLOAK_CLIENT_ID=vue-app-prod
VITE_API_BASE_URL=https://api.yourcompany.com

Build Configuration

// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [vue()],
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          keycloak: ['keycloak-js'],
          vendor: ['vue', 'vue-router', 'pinia'],
        },
      },
    },
  },
  define: {
    // Feature flags
    __VUE_PROD_DEVTOOLS__: false,
    __ENABLE_KEYCLOAK_DEBUG__: false,
  },
});

Security Headers

# nginx.conf
server {
    # Security headers
    add_header Content-Security-Policy "default-src 'self'; connect-src 'self' https://*.skycloak.io https://api.yourcompany.com; frame-src 'self' https://*.skycloak.io;" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # SPA routing
    location / {
        try_files $uri $uri/ /index.html;
    }
}

Performance Optimization

// Lazy load auth-heavy components
const AdminPanel = () =>
  import(
    /* webpackChunkName: "admin" */
    '@/views/AdminPanel.vue'
  );

// Prefetch auth endpoints
const prefetchAuthEndpoints = () => {
  const link = document.createElement('link');
  link.rel = 'prefetch';
  link.href = `${import.meta.env.VITE_KEYCLOAK_URL}/realms/${
    import.meta.env.VITE_KEYCLOAK_REALM
  }/.well-known/openid-configuration`;
  document.head.appendChild(link);
};

// Call on app initialization
prefetchAuthEndpoints();

Troubleshooting

Common Issues

  1. CORS Errors

    • Add your Vue app URL to Keycloak client’s Web Origins
    • Ensure API allows credentials from your domain
    • Check for proper CORS headers in responses
  2. Silent Check SSO Failing

    • Verify silent-check-sso.html is accessible
    • Check third-party cookie settings in browser
    • Ensure redirect URI is registered in Keycloak
  3. Token Refresh Loop

    • Check token expiration settings in Keycloak
    • Verify clock synchronization between client and server
    • Implement exponential backoff for refresh attempts

Debug Mode

// plugins/keycloak-debug.js
export function enableKeycloakDebug() {
  if (import.meta.env.DEV) {
    window.addEventListener('keycloak-token-expired', () => {
      console.log('Token expired event');
    });

    window.addEventListener('keycloak-auth-success', () => {
      console.log('Auth success event');
    });

    window.addEventListener('keycloak-auth-error', (e) => {
      console.error('Auth error event', e);
    });

    // Log all Keycloak events
    const originalInit = keycloak.init;
    keycloak.init = function (...args) {
      console.log('Keycloak init called with:', args);
      return originalInit.apply(this, args);
    };
  }
}

Performance Monitoring

// plugins/auth-performance.js
export function monitorAuthPerformance() {
  const metrics = {
    initStart: 0,
    initEnd: 0,
    loginStart: 0,
    loginEnd: 0,
    tokenRefreshCount: 0,
  };

  // Monitor init time
  performance.mark('keycloak-init-start');

  keycloak.onReady = () => {
    performance.mark('keycloak-init-end');
    performance.measure('keycloak-init', 'keycloak-init-start', 'keycloak-init-end');

    const measure = performance.getEntriesByName('keycloak-init')[0];
    console.log(`Keycloak initialized in ${measure.duration}ms`);
  };

  // Monitor token refresh
  const originalUpdateToken = keycloak.updateToken;
  keycloak.updateToken = function (...args) {
    metrics.tokenRefreshCount++;
    const start = performance.now();

    return originalUpdateToken.apply(this, args).then((result) => {
      const duration = performance.now() - start;
      console.log(`Token refresh #${metrics.tokenRefreshCount} took ${duration}ms`);
      return result;
    });
  };

  return metrics;
}

Next Steps