Refactor authentication to use centralized session manager

Extract session management logic from middleware into reusable SessionManager utility to improve reliability, reduce code duplication, and prevent thundering herd issues with jittered cache expiry.
This commit is contained in:
2025-07-11 14:43:50 -04:00
parent bf2361050f
commit c6f81a6686
8 changed files with 1051 additions and 139 deletions

View File

@@ -2,14 +2,15 @@ import { keycloakClient } from '~/server/utils/keycloak-client'
export default defineEventHandler(async (event) => {
const startTime = Date.now()
console.log('[REFRESH] Processing token refresh request')
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] No session found')
console.error(`[REFRESH:${requestId}] No session found`)
throw createError({
statusCode: 401,
statusMessage: 'No session found'
@@ -20,7 +21,7 @@ export default defineEventHandler(async (event) => {
try {
sessionData = JSON.parse(oidcSession)
} catch (parseError) {
console.error('[REFRESH] Failed to parse session:', parseError)
console.error(`[REFRESH:${requestId}] Failed to parse session:`, parseError)
throw createError({
statusCode: 401,
statusMessage: 'Invalid session format'
@@ -29,7 +30,7 @@ export default defineEventHandler(async (event) => {
// Check if we have a refresh token
if (!sessionData.refreshToken) {
console.error('[REFRESH] No refresh token available')
console.error(`[REFRESH:${requestId}] No refresh token available`)
throw createError({
statusCode: 401,
statusMessage: 'No refresh token available'
@@ -39,24 +40,48 @@ export default defineEventHandler(async (event) => {
// Validate environment variables
const clientSecret = process.env.KEYCLOAK_CLIENT_SECRET
if (!clientSecret) {
console.error('[REFRESH] KEYCLOAK_CLIENT_SECRET not configured')
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 retry logic
console.log('[REFRESH] Using Keycloak client for token refresh...')
// 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] Token refresh successful in ${refreshDuration}ms:`, {
console.log(`[REFRESH:${requestId}] Token refresh successful in ${refreshDuration}ms:`, {
hasAccessToken: !!tokenResponse.access_token,
hasRefreshToken: !!tokenResponse.refresh_token,
expiresIn: tokenResponse.expires_in
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,
@@ -79,7 +104,7 @@ export default defineEventHandler(async (event) => {
path: '/'
})
console.log('[REFRESH] Session updated successfully')
console.log(`[REFRESH:${requestId}] Session updated successfully`)
return {
success: true,
@@ -87,14 +112,17 @@ export default defineEventHandler(async (event) => {
}
} catch (error: any) {
console.error('[REFRESH] Token refresh failed:', error)
console.error(`[REFRESH:${requestId}] Token refresh failed:`, error)
// Clear invalid session
const cookieDomain = process.env.COOKIE_DOMAIN || '.portnimara.dev';
deleteCookie(event, 'nuxt-oidc-auth', {
domain: cookieDomain,
path: '/'
})
// 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,

View File

@@ -1,22 +1,33 @@
export default defineEventHandler(async (event) => {
console.log('[SESSION] Checking authentication session...')
const requestId = Math.random().toString(36).substring(7)
const startTime = Date.now()
console.log(`[SESSION:${requestId}] Checking authentication session...`)
// 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, groups: [] }
console.log(`[SESSION:${requestId}] No OIDC session cookie found`)
return {
user: null,
authenticated: false,
groups: [],
reason: 'NO_SESSION_COOKIE',
requestId
}
}
console.log('[SESSION] OIDC session cookie found, parsing...')
console.log(`[SESSION:${requestId}] OIDC session cookie found, parsing...`)
let sessionData
try {
// Parse the session data
const parseStart = Date.now()
sessionData = JSON.parse(oidcSessionCookie)
console.log('[SESSION] Session data parsed successfully:', {
const parseTime = Date.now() - parseStart
console.log(`[SESSION:${requestId}] Session data parsed successfully in ${parseTime}ms:`, {
hasUser: !!sessionData.user,
hasAccessToken: !!sessionData.accessToken,
hasIdToken: !!sessionData.idToken,
@@ -25,19 +36,25 @@ export default defineEventHandler(async (event) => {
timeUntilExpiry: sessionData.expiresAt ? sessionData.expiresAt - Date.now() : 'unknown'
})
} catch (parseError) {
console.error('[SESSION] Failed to parse session cookie:', parseError)
console.error(`[SESSION:${requestId}] Failed to parse session cookie:`, parseError)
// Clear invalid session
const cookieDomain = process.env.COOKIE_DOMAIN || '.portnimara.dev';
deleteCookie(event, 'nuxt-oidc-auth', {
domain: cookieDomain,
path: '/'
})
return { user: null, authenticated: false, groups: [] }
return {
user: null,
authenticated: false,
groups: [],
reason: 'INVALID_SESSION_FORMAT',
requestId
}
}
// Validate session structure
if (!sessionData.user || !sessionData.accessToken) {
console.error('[SESSION] Invalid session structure:', {
console.error(`[SESSION:${requestId}] Invalid session structure:`, {
hasUser: !!sessionData.user,
hasAccessToken: !!sessionData.accessToken
})
@@ -46,12 +63,18 @@ export default defineEventHandler(async (event) => {
domain: cookieDomain,
path: '/'
})
return { user: null, authenticated: false, groups: [] }
return {
user: null,
authenticated: false,
groups: [],
reason: 'INVALID_SESSION_STRUCTURE',
requestId
}
}
// Check if session is still valid
if (sessionData.expiresAt && Date.now() > sessionData.expiresAt) {
console.log('[SESSION] Session expired:', {
console.log(`[SESSION:${requestId}] Session expired:`, {
expiresAt: sessionData.expiresAt,
currentTime: Date.now(),
expiredSince: Date.now() - sessionData.expiresAt
@@ -62,7 +85,13 @@ export default defineEventHandler(async (event) => {
domain: cookieDomain,
path: '/'
})
return { user: null, authenticated: false, groups: [] }
return {
user: null,
authenticated: false,
groups: [],
reason: 'SESSION_EXPIRED',
requestId
}
}
// Extract groups from ID token