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
This commit is contained in:
Matt 2025-06-14 14:50:29 +02:00
parent bd8f1d9926
commit a797c13867
7 changed files with 159 additions and 128 deletions

129
composables/useKeycloak.ts Normal file
View File

@ -0,0 +1,129 @@
import type Keycloak from 'keycloak-js'
export const useKeycloak = () => {
const config = useRuntimeConfig()
const keycloak = ref<Keycloak | null>(null)
const isAuthenticated = ref(false)
const user = ref<any>(null)
const token = ref<string | null>(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,
}
}

View File

@ -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<UnifiedUser | null>(() => {
// 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();

View File

@ -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;
}

View File

@ -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",
},
},
},

93
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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