diff --git a/composables/useCustomAuth.ts b/composables/useCustomAuth.ts new file mode 100644 index 0000000..4665134 --- /dev/null +++ b/composables/useCustomAuth.ts @@ -0,0 +1,71 @@ +interface User { + id: string + email: string + username: string + name: string +} + +interface AuthState { + user: User | null + authenticated: boolean +} + +export const useCustomAuth = () => { + const user = ref(null) + const authenticated = ref(false) + const loading = ref(true) + + // Check authentication status + const checkAuth = async () => { + try { + loading.value = true + const data = await $fetch('/api/auth/session') + user.value = data.user + authenticated.value = data.authenticated + } catch (error) { + console.error('[AUTH] Session check failed:', error) + user.value = null + authenticated.value = false + } finally { + loading.value = false + } + } + + // Login with Keycloak + const login = () => { + const authUrl = 'https://auth.portnimara.dev/realms/client-portal/protocol/openid-connect/auth?' + + new URLSearchParams({ + client_id: 'client-portal', + redirect_uri: 'https://client.portnimara.dev/auth/keycloak/callback', + response_type: 'code', + scope: 'openid profile email', + state: Math.random().toString(36).substring(2) + }).toString() + + console.log('[AUTH] Redirecting to Keycloak login') + navigateTo(authUrl, { external: true }) + } + + // Logout + const logout = async () => { + try { + await navigateTo('/api/auth/logout', { external: true }) + } catch (error) { + console.error('[AUTH] Logout failed:', error) + } + } + + // Initialize auth state on composable creation + onMounted(() => { + checkAuth() + }) + + return { + user: readonly(user), + authenticated: readonly(authenticated), + loading: readonly(loading), + login, + logout, + checkAuth + } +} diff --git a/nuxt.config.ts b/nuxt.config.ts index c3e2d19..452f86b 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", "vuetify-nuxt-module", "nuxt-oidc-auth", "@vite-pwa/nuxt"], + modules: ["nuxt-directus", "vuetify-nuxt-module", "@vite-pwa/nuxt"], app: { head: { titleTemplate: "%s • Port Nimara Portal", @@ -111,37 +111,6 @@ export default defineNuxtConfig({ wasm: true } }, - oidc: { - middleware: { - globalMiddlewareEnabled: false, // Disable automatic middleware - prevents redirect loops! - }, - session: { - expirationCheck: true, - automaticRefresh: true, - expirationThreshold: 60, // seconds before expiry to refresh - cookie: { - sameSite: 'lax', // Required for cross-domain redirects - secure: true // Required for HTTPS in production - } - }, - providers: { - keycloak: { - audience: 'account', - baseUrl: 'https://auth.portnimara.dev/realms/client-portal', - clientId: 'client-portal', - clientSecret: '', // Will be injected via environment variable - redirectUri: 'https://client.portnimara.dev/auth/keycloak/callback', - userNameClaim: 'preferred_username', - logoutRedirectUri: 'https://client.portnimara.dev', - validateAccessToken: false, // Disable for Keycloak compatibility - exposeIdToken: true, - // Additional session settings for Keycloak - scope: ['openid', 'profile', 'email'], - responseType: 'code', - grantType: 'authorization_code' - } - } - }, runtimeConfig: { nocodb: { url: "", diff --git a/package.json b/package.json index 3c0506d..53b402f 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,6 @@ "nodemailer": "^7.0.3", "nuxt": "^3.15.4", "nuxt-directus": "^5.7.0", - "nuxt-oidc-auth": "^1.0.0-beta.5", "v-phone-input": "^4.4.2", "vue": "latest", "vue-router": "latest", diff --git a/pages/login.vue b/pages/login.vue index d439687..ff94aa2 100644 --- a/pages/login.vue +++ b/pages/login.vue @@ -115,8 +115,8 @@ definePageMeta({ // Directus auth const { login: directusLogin } = useDirectusAuth(); -// OIDC auth (Keycloak) -const { login: oidcLogin } = useOidcAuth(); +// Custom Keycloak auth +const { login: keycloakLogin } = useCustomAuth(); const loading = ref(false); const keycloakLoading = ref(false); @@ -128,14 +128,14 @@ const passwordVisible = ref(false); const valid = ref(false); -// SSO login function using nuxt-oidc-auth +// SSO login function using custom Keycloak auth const loginWithKeycloak = async () => { try { keycloakLoading.value = true; - console.log('[LOGIN] Starting SSO authentication via nuxt-oidc-auth...'); + console.log('[LOGIN] Starting SSO authentication via custom Keycloak auth...'); - // nuxt-oidc-auth handles everything server-side, no CORS issues! - await oidcLogin('keycloak'); + // Use our custom Keycloak authentication + keycloakLogin(); } catch (error) { console.error('[LOGIN] SSO login error:', error); diff --git a/server/api/auth/keycloak/callback.ts b/server/api/auth/keycloak/callback.ts new file mode 100644 index 0000000..c0bb3a4 --- /dev/null +++ b/server/api/auth/keycloak/callback.ts @@ -0,0 +1,82 @@ +export default defineEventHandler(async (event) => { + const query = getQuery(event) + const { code, state, error } = query + + console.log('[KEYCLOAK] Callback received:', { code: !!code, state, error }) + + if (error) { + console.error('[KEYCLOAK] OAuth error:', error) + throw createError({ + statusCode: 400, + statusMessage: `Authentication failed: ${error}` + }) + } + + if (!code) { + console.error('[KEYCLOAK] No authorization code received') + throw createError({ + statusCode: 400, + statusMessage: 'No authorization code received' + }) + } + + try { + // Exchange authorization code for tokens + const tokenResponse = await $fetch('https://auth.portnimara.dev/realms/client-portal/protocol/openid-connect/token', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + grant_type: 'authorization_code', + client_id: 'client-portal', + client_secret: process.env.KEYCLOAK_CLIENT_SECRET || '', + code: code as string, + redirect_uri: 'https://client.portnimara.dev/auth/keycloak/callback' + }).toString() + }) as any + + console.log('[KEYCLOAK] Token exchange successful') + + // Get user info + const userInfo = await $fetch('https://auth.portnimara.dev/realms/client-portal/protocol/openid-connect/userinfo', { + headers: { + 'Authorization': `Bearer ${tokenResponse.access_token}` + } + }) as any + + console.log('[KEYCLOAK] User info retrieved:', { + sub: userInfo.sub, + email: userInfo.email, + username: userInfo.preferred_username + }) + + // Set session cookie + const sessionData = { + user: userInfo, + accessToken: tokenResponse.access_token, + refreshToken: tokenResponse.refresh_token, + expiresAt: Date.now() + (tokenResponse.expires_in * 1000) + } + + // Create a simple session using a secure cookie + setCookie(event, 'keycloak-session', JSON.stringify(sessionData), { + httpOnly: true, + secure: true, + sameSite: 'lax', + maxAge: tokenResponse.expires_in + }) + + console.log('[KEYCLOAK] Session cookie set, redirecting to dashboard') + + // Redirect to dashboard + await sendRedirect(event, '/dashboard') + + } catch (error) { + console.error('[KEYCLOAK] Token exchange failed:', error) + throw createError({ + statusCode: 500, + statusMessage: 'Authentication failed during token exchange' + }) + } +}) diff --git a/server/api/auth/logout.ts b/server/api/auth/logout.ts new file mode 100644 index 0000000..bdc0cbf --- /dev/null +++ b/server/api/auth/logout.ts @@ -0,0 +1,22 @@ +export default defineEventHandler(async (event) => { + try { + // Clear the session cookie + deleteCookie(event, 'keycloak-session') + + console.log('[KEYCLOAK] User logged out, session cleared') + + // Redirect to Keycloak logout to clear SSO session + const logoutUrl = 'https://auth.portnimara.dev/realms/client-portal/protocol/openid-connect/logout?' + + new URLSearchParams({ + redirect_uri: 'https://client.portnimara.dev/login' + }).toString() + + await sendRedirect(event, logoutUrl) + } catch (error) { + console.error('[KEYCLOAK] Logout error:', error) + throw createError({ + statusCode: 500, + statusMessage: 'Logout failed' + }) + } +}) diff --git a/server/api/auth/session.ts b/server/api/auth/session.ts new file mode 100644 index 0000000..da36f3a --- /dev/null +++ b/server/api/auth/session.ts @@ -0,0 +1,33 @@ +export default defineEventHandler(async (event) => { + try { + const sessionCookie = getCookie(event, 'keycloak-session') + + if (!sessionCookie) { + return { user: null, authenticated: false } + } + + const sessionData = JSON.parse(sessionCookie) + + // Check if session is still valid + if (sessionData.expiresAt && Date.now() > sessionData.expiresAt) { + // Session expired, clear cookie + deleteCookie(event, 'keycloak-session') + return { user: null, authenticated: false } + } + + return { + user: { + id: sessionData.user.sub, + email: sessionData.user.email, + username: sessionData.user.preferred_username, + name: sessionData.user.name || sessionData.user.preferred_username + }, + authenticated: true + } + } catch (error) { + console.error('[KEYCLOAK] Session check error:', error) + // Clear invalid session + deleteCookie(event, 'keycloak-session') + return { user: null, authenticated: false } + } +})