From a797c13867cc2426f51f97f2548ce354a5c8a95c Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 14 Jun 2025 14:50:29 +0200 Subject: [PATCH] MAJOR: Replace nuxt-openid-connect with official Keycloak JS adapter - Remove problematic nuxt-openid-connect module that was causing OAuth issues - Install and implement official keycloak-js adapter for better reliability - Create new useKeycloak composable with proper token management - Update useUnifiedAuth to work with new Keycloak implementation - Fix authentication middleware to support both auth methods - Update login page to use new Keycloak login function - Clean up configuration and remove deprecated OIDC settings - This should resolve all the HTTP/HTTPS redirect and token exchange issues --- composables/useKeycloak.ts | 129 ++++++++++++++++++++++++++++++++++ composables/useUnifiedAuth.ts | 8 +-- middleware/authentication.ts | 10 ++- nuxt.config.ts | 35 ++------- package-lock.json | 93 ++---------------------- package.json | 2 +- pages/login.vue | 10 +-- 7 files changed, 159 insertions(+), 128 deletions(-) create mode 100644 composables/useKeycloak.ts diff --git a/composables/useKeycloak.ts b/composables/useKeycloak.ts new file mode 100644 index 0000000..2f258e8 --- /dev/null +++ b/composables/useKeycloak.ts @@ -0,0 +1,129 @@ +import type Keycloak from 'keycloak-js' + +export const useKeycloak = () => { + const config = useRuntimeConfig() + const keycloak = ref(null) + const isAuthenticated = ref(false) + const user = ref(null) + const token = ref(null) + const isInitialized = ref(false) + + const initKeycloak = async () => { + if (process.server) return + + try { + // Dynamically import keycloak-js + const KeycloakModule = await import('keycloak-js') + const KeycloakConstructor = KeycloakModule.default + + keycloak.value = new KeycloakConstructor({ + url: config.public.keycloak.url, + realm: config.public.keycloak.realm, + clientId: config.public.keycloak.clientId, + }) + + const authenticated = await keycloak.value.init({ + onLoad: 'check-sso', + silentCheckSsoRedirectUri: window.location.origin + '/silent-check-sso.html', + checkLoginIframe: false, // Disable iframe checks for better compatibility + }) + + isAuthenticated.value = authenticated + isInitialized.value = true + + if (authenticated && keycloak.value.token) { + token.value = keycloak.value.token || null + user.value = { + id: keycloak.value.subject, + username: keycloak.value.tokenParsed?.preferred_username, + email: keycloak.value.tokenParsed?.email, + firstName: keycloak.value.tokenParsed?.given_name, + lastName: keycloak.value.tokenParsed?.family_name, + fullName: keycloak.value.tokenParsed?.name, + roles: keycloak.value.tokenParsed?.realm_access?.roles || [], + } + + // Set up token refresh + keycloak.value.onTokenExpired = () => { + keycloak.value?.updateToken(30).then((refreshed) => { + if (refreshed) { + token.value = keycloak.value?.token || null + console.log('Token refreshed') + } else { + console.log('Token still valid') + } + }).catch(() => { + console.log('Failed to refresh token') + logout() + }) + } + } + + return authenticated + } catch (error) { + console.error('Failed to initialize Keycloak:', error) + isInitialized.value = true + return false + } + } + + const login = async () => { + if (!keycloak.value) { + await initKeycloak() + } + + if (keycloak.value) { + try { + await keycloak.value.login({ + redirectUri: window.location.origin + '/dashboard' + }) + } catch (error) { + console.error('Login failed:', error) + throw error + } + } + } + + const logout = async () => { + if (keycloak.value) { + try { + await keycloak.value.logout({ + redirectUri: window.location.origin + }) + } catch (error) { + console.error('Logout failed:', error) + } + } + + // Clear local state + isAuthenticated.value = false + user.value = null + token.value = null + } + + const getToken = () => { + return token.value + } + + const hasRole = (role: string) => { + return user.value?.roles?.includes(role) || false + } + + const hasAnyRole = (roles: string[]) => { + return roles.some(role => hasRole(role)) + } + + return { + keycloak: readonly(keycloak), + isAuthenticated: readonly(isAuthenticated), + user: readonly(user), + token: readonly(token), + isInitialized: readonly(isInitialized), + initKeycloak, + login, + logout, + getToken, + hasRole, + hasAnyRole, + } +} diff --git a/composables/useUnifiedAuth.ts b/composables/useUnifiedAuth.ts index b65392c..217df9c 100644 --- a/composables/useUnifiedAuth.ts +++ b/composables/useUnifiedAuth.ts @@ -11,13 +11,13 @@ export const useUnifiedAuth = () => { // Get both auth systems const directusAuth = useDirectusAuth(); const directusUser = useDirectusUser(); - const oidc = useOidc(); + const keycloak = useKeycloak(); // Create unified user object const user = computed(() => { // Check Keycloak user first - if (oidc.user?.value) { - const keycloakUser = oidc.user.value; + if (keycloak.user.value) { + const keycloakUser = keycloak.user.value; // Construct name from firstName and lastName if available let name = keycloakUser.name; if (!name && (keycloakUser.given_name || keycloakUser.family_name)) { @@ -58,7 +58,7 @@ export const useUnifiedAuth = () => { const logout = async () => { if (user.value?.authSource === 'keycloak') { // Keycloak logout - await oidc.logout(); + await keycloak.logout(); } else if (user.value?.authSource === 'directus') { // Directus logout await directusAuth.logout(); diff --git a/middleware/authentication.ts b/middleware/authentication.ts index 1e7a083..7ec4275 100644 --- a/middleware/authentication.ts +++ b/middleware/authentication.ts @@ -6,8 +6,14 @@ export default defineNuxtRouteMiddleware(async (to) => { const isAuthRequired = to.meta.auth !== false; // Check Keycloak auth first - const oidc = useOidc(); - if (oidc.isLoggedIn) { + const keycloak = useKeycloak(); + + // Initialize Keycloak if not already initialized + if (!keycloak.isInitialized.value) { + await keycloak.initKeycloak(); + } + + if (keycloak.isAuthenticated.value) { // User authenticated with Keycloak return; } diff --git a/nuxt.config.ts b/nuxt.config.ts index dc9eead..ee62c26 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -2,7 +2,7 @@ export default defineNuxtConfig({ ssr: false, compatibilityDate: "2024-11-01", devtools: { enabled: true }, - modules: ["nuxt-directus", "nuxt-openid-connect", "vuetify-nuxt-module", "@vite-pwa/nuxt"], + modules: ["nuxt-directus", "vuetify-nuxt-module", "@vite-pwa/nuxt"], app: { head: { titleTemplate: "%s • Port Nimara Portal", @@ -122,35 +122,10 @@ export default defineNuxtConfig({ directus: { url: "https://cms.portnimara.dev", }, - }, - }, - openidConnect: { - addPlugin: true, - op: { - issuer: process.env.KEYCLOAK_ISSUER || "https://auth.portnimara.dev/realms/client-portal", - clientId: process.env.KEYCLOAK_CLIENT_ID || "client-portal", - clientSecret: process.env.KEYCLOAK_CLIENT_SECRET!, // Environment variable must be set - callbackUrl: "", // Deprecated in v0.8.0+ but required by types - module uses /oidc/cb automatically - scope: ["openid", "email", "profile"], - }, - config: { - debug: true, // Enable debug mode to see what's happening - response_type: "code", - secret: process.env.OIDC_SESSION_SECRET || "default-session-secret-change-in-production", - cookie: { - loginName: "keycloak-login", - }, - cookiePrefix: "keycloak._", - cookieEncrypt: true, - cookieEncryptKey: process.env.OIDC_ENCRYPT_KEY || "default-encrypt-key-change-in-prod", - cookieEncryptIV: process.env.OIDC_ENCRYPT_IV || "default-iv-12345", - cookieEncryptALGO: "aes-256-cbc", - cookieMaxAge: 24 * 60 * 60, // 1 day - cookieFlags: { - access_token: { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - }, + keycloak: { + url: "https://auth.portnimara.dev", + realm: "client-portal", + clientId: "client-portal", }, }, }, diff --git a/package-lock.json b/package-lock.json index 9f18173..a258f7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@vite-pwa/nuxt": "^0.10.6", "formidable": "^3.5.4", "imap": "^0.8.19", + "keycloak-js": "^26.2.0", "libphonenumber-js": "^1.12.9", "lodash-es": "^4.17.21", "mailparser": "^3.7.3", @@ -20,7 +21,6 @@ "nodemailer": "^7.0.3", "nuxt": "^3.15.4", "nuxt-directus": "^5.7.0", - "nuxt-openid-connect": "^0.8.1", "v-phone-input": "^4.4.2", "vue": "latest", "vue-router": "latest", @@ -8571,15 +8571,6 @@ "jiti": "bin/jiti.js" } }, - "node_modules/jose": { - "version": "4.15.9", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", - "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, "node_modules/js-levenshtein": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", @@ -8664,6 +8655,12 @@ "node": ">=0.10.0" } }, + "node_modules/keycloak-js": { + "version": "26.2.0", + "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-26.2.0.tgz", + "integrity": "sha512-CrFcXTN+d6J0V/1v3Zpioys6qHNWE6yUzVVIsCUAmFn9H14GZ0vuYod+lt+SSpMgWGPuneDZBSGBAeLBFuqjsw==", + "license": "Apache-2.0" + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -9732,18 +9729,6 @@ "@nuxt/kit": "^3.0.0" } }, - "node_modules/nuxt-openid-connect": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/nuxt-openid-connect/-/nuxt-openid-connect-0.8.1.tgz", - "integrity": "sha512-b/pMCZ4ZnY6VHqQ5clnjRnrz7OQ1wsydjcZIQ0c5VQq7BCgb5VBtp/9HETRqRArTjDiIqxc/Xu3hhddbHAfakA==", - "license": "MIT", - "dependencies": { - "@nuxt/kit": "^3.11.2", - "defu": "^6.0.0", - "openid-client": "^5.1.6", - "uuid": "^10.0.0" - } - }, "node_modules/nypm": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.5.2.tgz", @@ -9764,15 +9749,6 @@ "node": "^14.16.0 || >=16.10.0" } }, - "node_modules/object-hash": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", - "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/object-inspect": { "version": "1.13.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", @@ -9831,15 +9807,6 @@ "integrity": "sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==", "license": "MIT" }, - "node_modules/oidc-token-hash": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.1.0.tgz", - "integrity": "sha512-y0W+X7Ppo7oZX6eovsRkuzcSM40Bicg2JEJkDJ4irIt1wsYAP5MLSNv+QAogO8xivMffw/9OvV3um1pxXgt1uA==", - "license": "MIT", - "engines": { - "node": "^10.13.0 || >=12.0.0" - } - }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -9940,39 +9907,6 @@ "typescript": "^5.x" } }, - "node_modules/openid-client": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz", - "integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==", - "license": "MIT", - "dependencies": { - "jose": "^4.15.9", - "lru-cache": "^6.0.0", - "object-hash": "^2.2.0", - "oidc-token-hash": "^5.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/openid-client/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/openid-client/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -13614,19 +13548,6 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, - "node_modules/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/v-phone-input": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/v-phone-input/-/v-phone-input-4.4.2.tgz", diff --git a/package.json b/package.json index cd06100..80eea71 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@vite-pwa/nuxt": "^0.10.6", "formidable": "^3.5.4", "imap": "^0.8.19", + "keycloak-js": "^26.2.0", "libphonenumber-js": "^1.12.9", "lodash-es": "^4.17.21", "mailparser": "^3.7.3", @@ -22,7 +23,6 @@ "nodemailer": "^7.0.3", "nuxt": "^3.15.4", "nuxt-directus": "^5.7.0", - "nuxt-openid-connect": "^0.8.1", "v-phone-input": "^4.4.2", "vue": "latest", "vue-router": "latest", diff --git a/pages/login.vue b/pages/login.vue index cd1aa43..d02f5c8 100644 --- a/pages/login.vue +++ b/pages/login.vue @@ -114,8 +114,8 @@ definePageMeta({ // Directus auth const { login } = useDirectusAuth(); -// OIDC auth for Keycloak -const oidc = useOidc(); +// Keycloak auth for SSO +const keycloak = useKeycloak(); const loading = ref(false); const errorThrown = ref(false); @@ -127,9 +127,9 @@ const passwordVisible = ref(false); const valid = ref(false); // Keycloak login function -const loginWithKeycloak = () => { - // Redirect to dashboard after login - oidc.login('/dashboard'); +const loginWithKeycloak = async () => { + // Initialize and login with Keycloak + await keycloak.login(); }; // Directus login function