161 lines
5.2 KiB
TypeScript
161 lines
5.2 KiB
TypeScript
|
|
// 🤠 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);
|
||
|
|
};
|