Vue Router Example
This example demonstrates a complete Vue application with Keycloak authentication and Vue Router integration.
Project Structure
src/
├── main.ts # App initialization with IIFE
├── App.vue # Root component
├── router/
│ ├── index.ts # Router configuration
│ ├── routes.ts # Route definitions
│ └── guards.ts # Authentication guards
├── views/
│ ├── Home.vue # Public home page
│ ├── Dashboard.vue # Protected dashboard
│ ├── Profile.vue # User profile
│ └── Admin.vue # Admin-only page
├── components/
│ ├── AuthNav.vue # Navigation with auth state
│ └── LoadingScreen.vue # Loading component
└── layouts/
├── DefaultLayout.vue # Default layout
└── AdminLayout.vue # Admin layoutImplementation
1. Main Application Entry (IIFE Pattern)
typescript
// src/main.ts
import { createApp } from "vue";
import { createKeycloakPlugin } from "keycloak-vue";
import App from "./App.vue";
import { createAppRouter } from "./router";
import LoadingScreen from "./components/LoadingScreen.vue";
// IIFE for older browser compatibility
(async () => {
try {
// Show loading screen immediately
const loadingApp = createApp(LoadingScreen);
loadingApp.mount("#app");
const app = createApp(App);
// Install Keycloak plugin
app.use(
createKeycloakPlugin({
config: {
url: import.meta.env.VITE_KEYCLOAK_URL || "http://localhost:8080",
realm: import.meta.env.VITE_KEYCLOAK_REALM || "demo",
clientId: import.meta.env.VITE_KEYCLOAK_CLIENT_ID || "vue-app",
},
initOptions: {
onLoad: "check-sso",
checkLoginIframe: false,
pkceMethod: "S256",
},
callbacks: {
onReady: (authenticated) => {
console.log("✅ Keycloak ready, authenticated:", authenticated);
// Create router after Keycloak is ready
const router = createAppRouter();
app.use(router);
// Unmount loading screen and mount main app
loadingApp.unmount();
app.mount("#app");
},
onAuthError: (error) => {
console.error("❌ Keycloak auth error:", error);
loadingApp.unmount();
// Show error state
const errorDiv = document.createElement("div");
errorDiv.innerHTML = `
<div style="text-align: center; padding: 2rem;">
<h1>Authentication Error</h1>
<p>Failed to initialize authentication. Please try again.</p>
<button onclick="location.reload()">Retry</button>
</div>
`;
document.body.appendChild(errorDiv);
},
},
})
);
} catch (error) {
console.error("💥 Failed to initialize app:", error);
document.body.innerHTML = `
<div style="text-align: center; padding: 2rem;">
<h1>App Failed to Load</h1>
<p>Please refresh the page to try again.</p>
<button onclick="location.reload()">Refresh</button>
</div>
`;
}
})();2. Router Configuration
typescript
// src/router/index.ts
import { createRouter, createWebHistory } from "vue-router";
import { routes } from "./routes";
import { setupAuthGuards } from "./guards";
export function createAppRouter() {
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
});
// Setup authentication guards
setupAuthGuards(router);
return router;
}3. Route Definitions
typescript
// src/router/routes.ts
import type { RouteRecordRaw } from "vue-router";
export const routes: RouteRecordRaw[] = [
{
path: "/",
name: "Home",
component: () => import("../views/Home.vue"),
meta: {
title: "Home",
public: true,
},
},
{
path: "/dashboard",
name: "Dashboard",
component: () => import("../views/Dashboard.vue"),
meta: {
title: "Dashboard",
requiresAuth: true,
},
},
{
path: "/profile",
name: "Profile",
component: () => import("../views/Profile.vue"),
meta: {
title: "Profile",
requiresAuth: true,
},
},
{
path: "/admin",
name: "Admin",
component: () => import("../layouts/AdminLayout.vue"),
meta: {
title: "Admin",
requiresAuth: true,
requiredRoles: ["admin"],
},
children: [
{
path: "",
name: "AdminDashboard",
component: () => import("../views/Admin.vue"),
},
{
path: "users",
name: "AdminUsers",
component: () => import("../views/admin/Users.vue"),
meta: {
requiredRoles: ["admin", "user-manager"],
},
},
],
},
{
path: "/forbidden",
name: "Forbidden",
component: () => import("../views/Forbidden.vue"),
meta: {
title: "Access Denied",
public: true,
},
},
{
path: "/:pathMatch(.*)*",
name: "NotFound",
component: () => import("../views/NotFound.vue"),
meta: {
title: "Page Not Found",
public: true,
},
},
];4. Authentication Guards
typescript
// src/router/guards.ts
import type {
Router,
NavigationGuardNext,
RouteLocationNormalized,
} from "vue-router";
import { useKeycloak } from "keycloak-vue";
export function setupAuthGuards(router: Router) {
// Global before guard
router.beforeEach(
async (
to: RouteLocationNormalized,
from: RouteLocationNormalized,
next: NavigationGuardNext
) => {
const { isAuthenticated, isReady, hasRealmRole } = useKeycloak();
// Ensure Keycloak is ready
if (!isReady.value) {
console.warn("🔄 Router guard executed before Keycloak was ready");
return next(false);
}
// Public routes (no auth required)
if (to.meta.public || !to.meta.requiresAuth) {
return next();
}
// Check authentication for protected routes
if (!isAuthenticated.value) {
console.log("🔒 Route requires auth, but user not authenticated");
// Store intended destination for redirect after login
sessionStorage.setItem("intendedRoute", to.fullPath);
// Let Keycloak handle login
const { login } = useKeycloak();
await login({
redirectUri: `${window.location.origin}${to.fullPath}`,
});
return next(false);
}
// Check role-based access
if (to.meta.requiredRoles) {
const requiredRoles = Array.isArray(to.meta.requiredRoles)
? to.meta.requiredRoles
: [to.meta.requiredRoles];
const hasRequiredRole = requiredRoles.some((role) =>
hasRealmRole(role)
);
if (!hasRequiredRole) {
console.log("🚫 User lacks required roles:", requiredRoles);
return next({ name: "Forbidden" });
}
}
// All checks passed
next();
}
);
// After each navigation
router.afterEach((to) => {
// Update page title
if (to.meta.title) {
document.title = `${to.meta.title} - KeycloakVue Demo`;
}
// Clear intended route after successful navigation
if (sessionStorage.getItem("intendedRoute")) {
sessionStorage.removeItem("intendedRoute");
}
});
}5. Loading Screen Component
vue
<!-- src/components/LoadingScreen.vue -->
<template>
<div class="loading-screen">
<div class="loading-content">
<div class="spinner"></div>
<h2>Initializing Authentication...</h2>
<p>Please wait while we set up your session.</p>
</div>
</div>
</template>
<style scoped>
.loading-screen {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.loading-content {
text-align: center;
max-width: 400px;
padding: 2rem;
}
.spinner {
width: 60px;
height: 60px;
border: 4px solid rgba(255, 255, 255, 0.3);
border-top: 4px solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 1.5rem;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
h2 {
margin: 0 0 0.5rem;
font-size: 1.5rem;
font-weight: 600;
}
p {
margin: 0;
opacity: 0.9;
font-size: 1rem;
}
</style>6. Navigation Component
vue
<!-- src/components/AuthNav.vue -->
<script setup lang="ts">
import { useKeycloak } from "keycloak-vue";
import { useRouter, useRoute } from "vue-router";
import { computed } from "vue";
const { isAuthenticated, username, hasRealmRole, login, logout } =
useKeycloak();
const router = useRouter();
const route = useRoute();
const canAccessAdmin = computed(() => hasRealmRole("admin"));
const handleLogin = async () => {
await login({
redirectUri: window.location.href,
});
};
const handleLogout = async () => {
await logout({
redirectUri: window.location.origin,
});
};
const navigateToProfile = () => {
router.push("/profile");
};
</script>
<template>
<nav class="auth-nav">
<div class="nav-brand">
<router-link to="/" class="brand-link"> 🔐 KeycloakVue Demo </router-link>
</div>
<div class="nav-links">
<router-link to="/" :class="{ active: route.name === 'Home' }">
Home
</router-link>
<template v-if="isAuthenticated">
<router-link
to="/dashboard"
:class="{ active: route.name === 'Dashboard' }"
>
Dashboard
</router-link>
<router-link
v-if="canAccessAdmin"
to="/admin"
:class="{ active: route.path.startsWith('/admin') }"
>
Admin
</router-link>
</template>
</div>
<div class="nav-user">
<div v-if="isAuthenticated" class="user-menu">
<span class="username">{{ username }}</span>
<div class="dropdown">
<button class="dropdown-toggle">⚙️</button>
<div class="dropdown-menu">
<button @click="navigateToProfile">👤 Profile</button>
<button @click="handleLogout">🚪 Logout</button>
</div>
</div>
</div>
<button v-else @click="handleLogin" class="login-btn">🔑 Login</button>
</div>
</nav>
</template>
<style scoped>
.auth-nav {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 2rem;
background: white;
border-bottom: 1px solid #e5e7eb;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.brand-link {
font-size: 1.25rem;
font-weight: bold;
text-decoration: none;
color: #4f46e5;
}
.nav-links {
display: flex;
gap: 1.5rem;
}
.nav-links a {
text-decoration: none;
color: #6b7280;
font-weight: 500;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
transition: all 0.2s;
}
.nav-links a:hover,
.nav-links a.active {
background: #f3f4f6;
color: #4f46e5;
}
.user-menu {
display: flex;
align-items: center;
gap: 1rem;
}
.username {
font-weight: 500;
color: #374151;
}
.dropdown {
position: relative;
}
.dropdown-toggle {
background: none;
border: none;
cursor: pointer;
padding: 0.5rem;
border-radius: 0.375rem;
transition: background 0.2s;
}
.dropdown-toggle:hover {
background: #f3f4f6;
}
.dropdown-menu {
position: absolute;
right: 0;
top: 100%;
background: white;
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
min-width: 150px;
padding: 0.5rem;
display: none;
z-index: 10;
}
.dropdown:hover .dropdown-menu {
display: block;
}
.dropdown-menu button {
display: block;
width: 100%;
text-align: left;
background: none;
border: none;
padding: 0.5rem;
border-radius: 0.25rem;
cursor: pointer;
transition: background 0.2s;
}
.dropdown-menu button:hover {
background: #f3f4f6;
}
.login-btn {
background: #4f46e5;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
cursor: pointer;
font-weight: 500;
transition: background 0.2s;
}
.login-btn:hover {
background: #4338ca;
}
</style>7. Dashboard View
vue
<!-- src/views/Dashboard.vue -->
<script setup lang="ts">
import { useKeycloak } from "keycloak-vue";
import { onMounted, ref } from "vue";
const { username, email, realmRoles, updateToken, loadUserProfile, profile } =
useKeycloak();
const lastTokenRefresh = ref<Date | null>(null);
onMounted(async () => {
// Load user profile on component mount
try {
await loadUserProfile();
console.log("✅ User profile loaded");
} catch (error) {
console.error("❌ Failed to load user profile:", error);
}
});
const refreshToken = async () => {
try {
const refreshed = await updateToken(30);
lastTokenRefresh.value = new Date();
console.log("Token refreshed:", refreshed);
} catch (error) {
console.error("Token refresh failed:", error);
}
};
</script>
<template>
<div class="dashboard">
<div class="dashboard-header">
<h1>👋 Welcome to your Dashboard</h1>
<p>
Hello, <strong>{{ username }}</strong
>!
</p>
</div>
<div class="dashboard-grid">
<div class="card">
<h3>📧 Profile Information</h3>
<div v-if="profile" class="profile-info">
<p>
<strong>Name:</strong> {{ profile.firstName }}
{{ profile.lastName }}
</p>
<p><strong>Email:</strong> {{ profile.email }}</p>
<p><strong>Username:</strong> {{ profile.username }}</p>
</div>
<p v-else class="loading">Loading profile...</p>
</div>
<div class="card">
<h3>🛡️ Your Roles</h3>
<div v-if="realmRoles.length > 0" class="roles-list">
<span v-for="role in realmRoles" :key="role" class="role-badge">
{{ role }}
</span>
</div>
<p v-else class="no-roles">No roles assigned</p>
</div>
<div class="card">
<h3>🔑 Token Management</h3>
<button @click="refreshToken" class="refresh-btn">Refresh Token</button>
<p v-if="lastTokenRefresh" class="refresh-info">
Last refreshed: {{ lastTokenRefresh.toLocaleTimeString() }}
</p>
</div>
<div class="card">
<h3>🎯 Quick Actions</h3>
<div class="actions">
<router-link to="/profile" class="action-btn">
View Profile
</router-link>
<router-link
v-if="realmRoles.includes('admin')"
to="/admin"
class="action-btn admin"
>
Admin Panel
</router-link>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.dashboard {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.dashboard-header {
text-align: center;
margin-bottom: 2rem;
}
.dashboard-header h1 {
color: #1f2937;
margin-bottom: 0.5rem;
}
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
}
.card {
background: white;
border-radius: 0.75rem;
padding: 1.5rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
border: 1px solid #e5e7eb;
}
.card h3 {
margin: 0 0 1rem;
color: #374151;
font-size: 1.125rem;
}
.profile-info p {
margin: 0.5rem 0;
color: #6b7280;
}
.roles-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.role-badge {
background: #dbeafe;
color: #1e40af;
padding: 0.25rem 0.75rem;
border-radius: 1rem;
font-size: 0.875rem;
font-weight: 500;
}
.refresh-btn {
background: #10b981;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
cursor: pointer;
font-weight: 500;
transition: background 0.2s;
}
.refresh-btn:hover {
background: #059669;
}
.refresh-info {
margin-top: 0.5rem;
font-size: 0.875rem;
color: #6b7280;
}
.actions {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.action-btn {
display: inline-block;
text-align: center;
padding: 0.75rem;
background: #f3f4f6;
color: #374151;
text-decoration: none;
border-radius: 0.375rem;
font-weight: 500;
transition: all 0.2s;
}
.action-btn:hover {
background: #e5e7eb;
}
.action-btn.admin {
background: #fef3c7;
color: #92400e;
}
.action-btn.admin:hover {
background: #fde68a;
}
.loading,
.no-roles {
color: #6b7280;
font-style: italic;
}
</style>8. Environment Configuration
bash
# .env.local
VITE_KEYCLOAK_URL=http://localhost:8080
VITE_KEYCLOAK_REALM=demo
VITE_KEYCLOAK_CLIENT_ID=vue-appBest Practices
- IIFE Pattern - Use IIFE for older browser compatibility when top-level await isn't supported
- Loading States - Always show loading indicators during authentication initialization
- Error Boundaries - Implement proper error handling for auth failures and app crashes
- Route Guards - Protect routes at the router level with proper authentication checks
- Role-Based Access - Implement granular permissions using Keycloak roles
- Deep Linking - Support direct navigation to protected routes with automatic login redirect
- Token Management - Ensure tokens are valid before navigation and refresh when needed
- Graceful Degradation - Provide fallback UI for authentication failures
