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:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user