// 🤠 Heady OIDC Callback Endpoint - Complete Authentik Authentication /** * Handles OIDC callback from Authentik * Exchanges authorization code for tokens, creates session, and redirects to dashboard */ import type { APIRoute } from 'astro'; import { AuthentikOIDCClient } from '../../../lib/auth/oidc-client.js'; import { mapAuthentikGroups } from '../../../lib/auth/role-mapper.js'; import { getSessionManager } from '../../../lib/auth/session-manager.js'; import { loadAuthentikConfig } from '../../../lib/config/authentik.js'; import { stateStore } from './login.js'; export const GET: APIRoute = async ({ url, redirect, cookies }) => { try { // During build/prerender, return error for missing parameters if ( process.env.NODE_ENV === 'development' && !process.env.CI && !process.env.AUTHENTIK_ISSUER ) { return redirect('/login?error=callback_not_configured'); } console.log('🤠 Heady OIDC Callback received'); // Extract parameters from callback const code = url.searchParams.get('code'); const state = url.searchParams.get('state'); const error = url.searchParams.get('error'); const errorDescription = url.searchParams.get('error_description'); // Handle Authentik error responses if (error) { console.error(`❌ Authentik returned error: ${error}`); console.error(` Description: ${errorDescription}`); return redirect( `/login?error=${encodeURIComponent(error)}&description=${encodeURIComponent(errorDescription || '')}`, ); } // Validate required parameters if (!code || !state) { console.error('❌ Missing required parameters in callback'); return redirect('/login?error=invalid_callback'); } console.log(`✓ Callback parameters received - State: ${state}`); // Validate state and retrieve PKCE code verifier const stateData = stateStore.get(state); if (!stateData) { console.error('❌ Invalid or expired state parameter'); return redirect('/login?error=invalid_state'); } // Clean up used state stateStore.delete(state); console.log(`✓ State validated and PKCE verifier retrieved`); // Load Authentik configuration const config = loadAuthentikConfig(); // Create OIDC client const oidcClient = new AuthentikOIDCClient(config); // Exchange authorization code for tokens console.log(' → Exchanging code for tokens...'); const tokens = await oidcClient.exchangeCodeForTokens( code, stateData.codeVerifier, ); console.log( `✓ Tokens received (access token: ${tokens.access_token.substring(0, 20)}...)`, ); // Fetch user information from Authentik console.log(' → Fetching user information...'); const userInfo = await oidcClient.fetchUserInfo(tokens.access_token); console.log( `✓ User info received: ${userInfo.email} (${userInfo.groups.length} groups)`, ); // Map Authentik groups to Heady role const roleMapping = mapAuthentikGroups(userInfo.groups); console.log(`✓ Role mapping completed:`); console.log(` User groups: ${userInfo.groups.join(', ') || 'none'}`); console.log(` Mapped role: ${roleMapping.role} (${roleMapping.method})`); console.log(` Matched group: ${roleMapping.matchedGroup || 'none'}`); // Create user data for session const sessionUser = { email: userInfo.email, name: userInfo.name, role: roleMapping.role, role_description: roleMapping.role_mapping.description, picture: userInfo.picture, groups: userInfo.groups, capabilities: roleMapping.role_mapping.capabilities, session: { session_id: '', // Will be filled by session manager expires_at: '', // Will be filled by session manager last_activity: '', // Will be filled by session manager }, }; // Create secure session console.log(' → Creating session...'); const sessionMgr = getSessionManager(); const sessionResult = await sessionMgr.createSession( { url, cookies, redirect, clientAddress: 'unknown' }, sessionUser, ); console.log( `✓ Session created: ${sessionResult.session_id.substring(0, 16)}...`, ); console.log(` Expires: ${sessionResult.expires_at}`); console.log(`🎉 Authentication successful for ${userInfo.email}`); console.log(` Role: ${roleMapping.role}`); console.log(` Groups: ${userInfo.groups.join(', ') || 'none'}`); console.log( ` Session expires: ${new Date(sessionResult.expires_at).toLocaleString()}`, ); // Redirect to dashboard (session cookie was set by session manager) return redirect('/', 302); } catch (error) { console.error('❌ OIDC callback error:', error); // Detailed error logging for troubleshooting if (error instanceof Error) { console.error(` Error type: ${error.constructor.name}`); console.error(` Message: ${error.message}`); if (error.stack) { console.error( ` Stack: ${error.stack.split('\n').slice(0, 3).join('\n')}`, ); } } // User-friendly error redirect const errorParam = error instanceof Error ? encodeURIComponent(error.message) : 'authentication_failed'; return redirect(`/login?error=callback_failed&description=${errorParam}`); } }; // Handle POST requests (not typical for OIDC, but included for completeness) export const POST: APIRoute = async (context) => { return context.redirect('/api/auth/login', 302); };