FEAT: Migrate authentication system from Directus to Keycloak, implementing token refresh and enhancing session management
This commit is contained in:
@@ -21,6 +21,16 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Validate environment variables
|
||||
const clientSecret = process.env.KEYCLOAK_CLIENT_SECRET
|
||||
if (!clientSecret) {
|
||||
console.error('[KEYCLOAK] KEYCLOAK_CLIENT_SECRET not configured')
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Authentication service misconfigured'
|
||||
})
|
||||
}
|
||||
|
||||
// Exchange authorization code for tokens
|
||||
const tokenResponse = await $fetch('https://auth.portnimara.dev/realms/client-portal/protocol/openid-connect/token', {
|
||||
method: 'POST',
|
||||
@@ -30,13 +40,17 @@ export default defineEventHandler(async (event) => {
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
client_id: 'client-portal',
|
||||
client_secret: process.env.KEYCLOAK_CLIENT_SECRET || '',
|
||||
client_secret: clientSecret,
|
||||
code: code as string,
|
||||
redirect_uri: 'https://client.portnimara.dev/api/auth/keycloak/callback'
|
||||
}).toString()
|
||||
}) as any
|
||||
|
||||
console.log('[KEYCLOAK] Token exchange successful')
|
||||
console.log('[KEYCLOAK] Token exchange successful:', {
|
||||
hasAccessToken: !!tokenResponse.access_token,
|
||||
hasRefreshToken: !!tokenResponse.refresh_token,
|
||||
expiresIn: tokenResponse.expires_in
|
||||
})
|
||||
|
||||
// Get user info
|
||||
const userInfo = await $fetch('https://auth.portnimara.dev/realms/client-portal/protocol/openid-connect/userinfo', {
|
||||
@@ -48,35 +62,50 @@ export default defineEventHandler(async (event) => {
|
||||
console.log('[KEYCLOAK] User info retrieved:', {
|
||||
sub: userInfo.sub,
|
||||
email: userInfo.email,
|
||||
username: userInfo.preferred_username
|
||||
username: userInfo.preferred_username,
|
||||
name: userInfo.name
|
||||
})
|
||||
|
||||
// Set session cookie
|
||||
// Set session cookie with proper configuration
|
||||
const sessionData = {
|
||||
user: userInfo,
|
||||
user: {
|
||||
id: userInfo.sub,
|
||||
email: userInfo.email,
|
||||
username: userInfo.preferred_username || userInfo.email,
|
||||
name: userInfo.name || userInfo.preferred_username || userInfo.email,
|
||||
authMethod: 'keycloak'
|
||||
},
|
||||
accessToken: tokenResponse.access_token,
|
||||
refreshToken: tokenResponse.refresh_token,
|
||||
expiresAt: Date.now() + (tokenResponse.expires_in * 1000)
|
||||
expiresAt: Date.now() + (tokenResponse.expires_in * 1000),
|
||||
createdAt: Date.now()
|
||||
}
|
||||
|
||||
// Create a simple session using a secure cookie
|
||||
// Create session cookie with better security settings
|
||||
setCookie(event, 'nuxt-oidc-auth', JSON.stringify(sessionData), {
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'lax',
|
||||
maxAge: tokenResponse.expires_in
|
||||
maxAge: tokenResponse.expires_in,
|
||||
domain: '.portnimara.dev',
|
||||
path: '/'
|
||||
})
|
||||
|
||||
console.log('[OIDC] Session cookie set, redirecting to dashboard')
|
||||
console.log('[KEYCLOAK] Session cookie set successfully')
|
||||
console.log('[KEYCLOAK] Redirecting to dashboard...')
|
||||
|
||||
// Redirect to dashboard
|
||||
await sendRedirect(event, '/dashboard')
|
||||
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('[KEYCLOAK] Token exchange failed:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Authentication failed during token exchange'
|
||||
console.error('[KEYCLOAK] Error details:', {
|
||||
message: error.message,
|
||||
status: error.status,
|
||||
data: error.data
|
||||
})
|
||||
|
||||
// Redirect to login with error
|
||||
await sendRedirect(event, '/login?error=auth_failed')
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,40 +1,41 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
console.log('[LOGOUT] Processing logout request')
|
||||
|
||||
try {
|
||||
// Check which authentication method is being used
|
||||
const directusToken = getCookie(event, 'directus_token')
|
||||
// Check for OIDC session
|
||||
const oidcSession = getCookie(event, 'nuxt-oidc-auth')
|
||||
|
||||
// Clear Directus cookies if they exist
|
||||
if (directusToken) {
|
||||
deleteCookie(event, 'directus_token')
|
||||
deleteCookie(event, 'directus_refresh_token')
|
||||
deleteCookie(event, 'directus_token_expired_at')
|
||||
console.log('[LOGOUT] Directus session cleared')
|
||||
}
|
||||
|
||||
// Clear OIDC session cookie if it exists
|
||||
// Clear OIDC session cookie
|
||||
if (oidcSession) {
|
||||
deleteCookie(event, 'nuxt-oidc-auth')
|
||||
deleteCookie(event, 'nuxt-oidc-auth', {
|
||||
domain: '.portnimara.dev',
|
||||
path: '/'
|
||||
})
|
||||
console.log('[LOGOUT] OIDC session cleared')
|
||||
}
|
||||
|
||||
// If user was authenticated via OIDC/Keycloak, redirect to Keycloak logout
|
||||
if (oidcSession) {
|
||||
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)
|
||||
} else {
|
||||
// For Directus users or others, just redirect to login
|
||||
await sendRedirect(event, '/login')
|
||||
}
|
||||
// Always redirect to Keycloak logout to ensure complete logout
|
||||
const logoutUrl = 'https://auth.portnimara.dev/realms/client-portal/protocol/openid-connect/logout?' +
|
||||
new URLSearchParams({
|
||||
redirect_uri: 'https://client.portnimara.dev/login'
|
||||
}).toString()
|
||||
|
||||
console.log('[LOGOUT] Redirecting to Keycloak logout:', logoutUrl)
|
||||
await sendRedirect(event, logoutUrl)
|
||||
|
||||
} catch (error) {
|
||||
console.error('[LOGOUT] Logout error:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Logout failed'
|
||||
})
|
||||
|
||||
// Fallback: clear cookies and redirect to login
|
||||
try {
|
||||
deleteCookie(event, 'nuxt-oidc-auth', {
|
||||
domain: '.portnimara.dev',
|
||||
path: '/'
|
||||
})
|
||||
} catch (cookieError) {
|
||||
console.error('[LOGOUT] Cookie cleanup error:', cookieError)
|
||||
}
|
||||
|
||||
await sendRedirect(event, '/login')
|
||||
}
|
||||
})
|
||||
|
||||
106
server/api/auth/refresh.ts
Normal file
106
server/api/auth/refresh.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
console.log('[REFRESH] Processing token refresh request')
|
||||
|
||||
try {
|
||||
// Get current session
|
||||
const oidcSession = getCookie(event, 'nuxt-oidc-auth')
|
||||
|
||||
if (!oidcSession) {
|
||||
console.error('[REFRESH] No session found')
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: 'No session found'
|
||||
})
|
||||
}
|
||||
|
||||
let sessionData
|
||||
try {
|
||||
sessionData = JSON.parse(oidcSession)
|
||||
} catch (parseError) {
|
||||
console.error('[REFRESH] Failed to parse session:', parseError)
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: 'Invalid session format'
|
||||
})
|
||||
}
|
||||
|
||||
// Check if we have a refresh token
|
||||
if (!sessionData.refreshToken) {
|
||||
console.error('[REFRESH] No refresh token available')
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: 'No refresh token available'
|
||||
})
|
||||
}
|
||||
|
||||
// Validate environment variables
|
||||
const clientSecret = process.env.KEYCLOAK_CLIENT_SECRET
|
||||
if (!clientSecret) {
|
||||
console.error('[REFRESH] KEYCLOAK_CLIENT_SECRET not configured')
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Authentication service misconfigured'
|
||||
})
|
||||
}
|
||||
|
||||
// Use refresh token to get new access token
|
||||
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: 'refresh_token',
|
||||
client_id: 'client-portal',
|
||||
client_secret: clientSecret,
|
||||
refresh_token: sessionData.refreshToken
|
||||
}).toString()
|
||||
}) as any
|
||||
|
||||
console.log('[REFRESH] Token refresh successful:', {
|
||||
hasAccessToken: !!tokenResponse.access_token,
|
||||
hasRefreshToken: !!tokenResponse.refresh_token,
|
||||
expiresIn: tokenResponse.expires_in
|
||||
})
|
||||
|
||||
// Update session with new tokens
|
||||
const updatedSessionData = {
|
||||
...sessionData,
|
||||
accessToken: tokenResponse.access_token,
|
||||
refreshToken: tokenResponse.refresh_token || sessionData.refreshToken, // Keep old refresh token if new one not provided
|
||||
expiresAt: Date.now() + (tokenResponse.expires_in * 1000),
|
||||
refreshedAt: Date.now()
|
||||
}
|
||||
|
||||
// Set updated session cookie
|
||||
setCookie(event, 'nuxt-oidc-auth', JSON.stringify(updatedSessionData), {
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'lax',
|
||||
maxAge: tokenResponse.expires_in,
|
||||
domain: '.portnimara.dev',
|
||||
path: '/'
|
||||
})
|
||||
|
||||
console.log('[REFRESH] Session updated successfully')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
expiresAt: updatedSessionData.expiresAt
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('[REFRESH] Token refresh failed:', error)
|
||||
|
||||
// Clear invalid session
|
||||
deleteCookie(event, 'nuxt-oidc-auth', {
|
||||
domain: '.portnimara.dev',
|
||||
path: '/'
|
||||
})
|
||||
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: 'Token refresh failed - please login again'
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -1,85 +1,89 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
// Check Directus authentication first
|
||||
try {
|
||||
const directusToken = getCookie(event, 'directus_token')
|
||||
if (directusToken) {
|
||||
// Check if token is expired
|
||||
const directusExpiry = getCookie(event, 'directus_token_expired_at')
|
||||
if (directusExpiry) {
|
||||
const expiryTime = parseInt(directusExpiry)
|
||||
if (Date.now() >= expiryTime) {
|
||||
console.log('[SESSION] Directus token expired')
|
||||
return { user: null, authenticated: false }
|
||||
}
|
||||
}
|
||||
|
||||
// For Directus, we'll use generic user info since we don't decode the token
|
||||
// You can expand this to fetch actual user data from Directus API if needed
|
||||
return {
|
||||
user: {
|
||||
id: 'directus-user',
|
||||
email: 'user@portnimara.com', // Could fetch from Directus API
|
||||
username: 'directus-user',
|
||||
name: 'Directus User',
|
||||
authMethod: 'directus'
|
||||
},
|
||||
authenticated: true
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[SESSION] Directus session check error:', error)
|
||||
}
|
||||
console.log('[SESSION] Checking authentication session...')
|
||||
|
||||
// Check OIDC authentication
|
||||
// Check OIDC/Keycloak authentication only
|
||||
try {
|
||||
const oidcSessionCookie = getCookie(event, 'nuxt-oidc-auth')
|
||||
|
||||
if (!oidcSessionCookie) {
|
||||
console.log('[SESSION] No OIDC session cookie found')
|
||||
return { user: null, authenticated: false }
|
||||
}
|
||||
|
||||
// Handle encrypted OIDC cookies (Fe26.2** format)
|
||||
let sessionData
|
||||
if (oidcSessionCookie.startsWith('Fe26.2**')) {
|
||||
// This is an encrypted cookie - for now we'll assume it's valid
|
||||
// In a full implementation, you'd decrypt it properly
|
||||
console.log('[SESSION] OIDC session found (encrypted)')
|
||||
return {
|
||||
user: {
|
||||
id: 'oidc-user',
|
||||
email: 'oidc-user@portnimara.com',
|
||||
username: 'oidc-user',
|
||||
name: 'OIDC User',
|
||||
authMethod: 'oidc'
|
||||
},
|
||||
authenticated: true
|
||||
}
|
||||
} else {
|
||||
// Try to parse as JSON (unencrypted)
|
||||
sessionData = JSON.parse(oidcSessionCookie)
|
||||
|
||||
// Check if session is still valid
|
||||
if (sessionData.expiresAt && Date.now() > sessionData.expiresAt) {
|
||||
// Session expired, clear cookie
|
||||
deleteCookie(event, 'nuxt-oidc-auth')
|
||||
return { user: null, authenticated: false }
|
||||
}
|
||||
console.log('[SESSION] OIDC session cookie found, parsing...')
|
||||
|
||||
return {
|
||||
user: {
|
||||
id: sessionData.user.sub,
|
||||
email: sessionData.user.email,
|
||||
username: sessionData.user.preferred_username,
|
||||
name: sessionData.user.name || sessionData.user.preferred_username,
|
||||
authMethod: 'oidc'
|
||||
},
|
||||
authenticated: true
|
||||
}
|
||||
let sessionData
|
||||
try {
|
||||
// Parse the session data
|
||||
sessionData = JSON.parse(oidcSessionCookie)
|
||||
console.log('[SESSION] Session data parsed successfully:', {
|
||||
hasUser: !!sessionData.user,
|
||||
hasAccessToken: !!sessionData.accessToken,
|
||||
expiresAt: sessionData.expiresAt,
|
||||
createdAt: sessionData.createdAt,
|
||||
timeUntilExpiry: sessionData.expiresAt ? sessionData.expiresAt - Date.now() : 'unknown'
|
||||
})
|
||||
} catch (parseError) {
|
||||
console.error('[SESSION] Failed to parse session cookie:', parseError)
|
||||
// Clear invalid session
|
||||
deleteCookie(event, 'nuxt-oidc-auth', {
|
||||
domain: '.portnimara.dev',
|
||||
path: '/'
|
||||
})
|
||||
return { user: null, authenticated: false }
|
||||
}
|
||||
|
||||
// Validate session structure
|
||||
if (!sessionData.user || !sessionData.accessToken) {
|
||||
console.error('[SESSION] Invalid session structure:', {
|
||||
hasUser: !!sessionData.user,
|
||||
hasAccessToken: !!sessionData.accessToken
|
||||
})
|
||||
deleteCookie(event, 'nuxt-oidc-auth', {
|
||||
domain: '.portnimara.dev',
|
||||
path: '/'
|
||||
})
|
||||
return { user: null, authenticated: false }
|
||||
}
|
||||
|
||||
// Check if session is still valid
|
||||
if (sessionData.expiresAt && Date.now() > sessionData.expiresAt) {
|
||||
console.log('[SESSION] Session expired:', {
|
||||
expiresAt: sessionData.expiresAt,
|
||||
currentTime: Date.now(),
|
||||
expiredSince: Date.now() - sessionData.expiresAt
|
||||
})
|
||||
// Session expired, clear cookie
|
||||
deleteCookie(event, 'nuxt-oidc-auth', {
|
||||
domain: '.portnimara.dev',
|
||||
path: '/'
|
||||
})
|
||||
return { user: null, authenticated: false }
|
||||
}
|
||||
|
||||
console.log('[SESSION] Valid session found for user:', {
|
||||
id: sessionData.user.id,
|
||||
email: sessionData.user.email,
|
||||
username: sessionData.user.username
|
||||
})
|
||||
|
||||
return {
|
||||
user: {
|
||||
id: sessionData.user.id,
|
||||
email: sessionData.user.email,
|
||||
username: sessionData.user.username,
|
||||
name: sessionData.user.name,
|
||||
authMethod: sessionData.user.authMethod || 'keycloak'
|
||||
},
|
||||
authenticated: true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[SESSION] OIDC session check error:', error)
|
||||
// Clear invalid session
|
||||
deleteCookie(event, 'nuxt-oidc-auth')
|
||||
deleteCookie(event, 'nuxt-oidc-auth', {
|
||||
domain: '.portnimara.dev',
|
||||
path: '/'
|
||||
})
|
||||
return { user: null, authenticated: false }
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user