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:
parent
bd8f1d9926
commit
a797c13867
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue