port-nimara-client-portal/server/api/auth/refresh.ts

133 lines
4.3 KiB
TypeScript

import { keycloakClient } from '~/server/utils/keycloak-client'
export default defineEventHandler(async (event) => {
const startTime = Date.now()
const requestId = Math.random().toString(36).substring(7)
console.log(`[REFRESH:${requestId}] Processing token refresh request`)
try {
// Get current session
const oidcSession = getCookie(event, 'nuxt-oidc-auth')
if (!oidcSession) {
console.error(`[REFRESH:${requestId}] No session found`)
throw createError({
statusCode: 401,
statusMessage: 'No session found'
})
}
let sessionData
try {
sessionData = JSON.parse(oidcSession)
} catch (parseError) {
console.error(`[REFRESH:${requestId}] 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:${requestId}] 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:${requestId}] KEYCLOAK_CLIENT_SECRET not configured`)
throw createError({
statusCode: 500,
statusMessage: 'Authentication service misconfigured'
})
}
// Use refresh token to get new access token with enhanced error handling
console.log(`[REFRESH:${requestId}] Using Keycloak client for token refresh...`)
const tokenResponse = await keycloakClient.refreshAccessToken(sessionData.refreshToken)
.catch((error: any) => {
// Check if it's a transient error
if (error.statusMessage === 'KEYCLOAK_TEMPORARILY_UNAVAILABLE') {
console.log(`[REFRESH:${requestId}] Keycloak temporarily unavailable, using grace period`)
// Return current session with extended grace period
return {
success: true,
expiresAt: sessionData.expiresAt,
gracePeriod: true
}
}
throw error // Re-throw for permanent failures
})
const refreshDuration = Date.now() - startTime
console.log(`[REFRESH:${requestId}] Token refresh successful in ${refreshDuration}ms:`, {
hasAccessToken: !!tokenResponse.access_token,
hasRefreshToken: !!tokenResponse.refresh_token,
expiresIn: tokenResponse.expires_in,
gracePeriod: tokenResponse.gracePeriod
})
// Handle grace period response
if (tokenResponse.gracePeriod) {
console.log(`[REFRESH:${requestId}] Using grace period - session extended`)
return {
success: true,
expiresAt: tokenResponse.expiresAt,
gracePeriod: true
}
}
// 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 with proper session duration
const sessionDuration = 8 * 60 * 60; // 8 hours in seconds
const cookieDomain = process.env.COOKIE_DOMAIN || '.portnimara.dev';
setCookie(event, 'nuxt-oidc-auth', JSON.stringify(updatedSessionData), {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: sessionDuration,
domain: cookieDomain,
path: '/'
})
console.log(`[REFRESH:${requestId}] Session updated successfully`)
return {
success: true,
expiresAt: updatedSessionData.expiresAt
}
} catch (error: any) {
console.error(`[REFRESH:${requestId}] Token refresh failed:`, error)
// Only clear session for permanent failures
if (error.statusMessage === 'REFRESH_TOKEN_INVALID') {
console.log(`[REFRESH:${requestId}] Clearing session due to invalid refresh token`)
const cookieDomain = process.env.COOKIE_DOMAIN || '.portnimara.dev';
deleteCookie(event, 'nuxt-oidc-auth', {
domain: cookieDomain,
path: '/'
})
}
throw createError({
statusCode: 401,
statusMessage: 'Token refresh failed - please login again'
})
}
})