243 lines
7.1 KiB
TypeScript
243 lines
7.1 KiB
TypeScript
export interface AuthenticatedUser {
|
|
id: string;
|
|
email: string;
|
|
username: string;
|
|
name: string;
|
|
authMethod: string;
|
|
groups: string[];
|
|
}
|
|
|
|
export interface AuthSession {
|
|
user: AuthenticatedUser | null;
|
|
authenticated: boolean;
|
|
groups: string[];
|
|
}
|
|
|
|
/**
|
|
* Check if the request is authenticated via Keycloak OIDC session
|
|
*/
|
|
export const isAuthenticated = async (event: any): Promise<boolean> => {
|
|
console.log('[auth] Checking authentication for:', event.node.req.url);
|
|
|
|
// Check OIDC session authentication
|
|
try {
|
|
const oidcSession = getCookie(event, 'nuxt-oidc-auth');
|
|
console.log('[auth] OIDC session cookie:', oidcSession ? 'present' : 'not found');
|
|
|
|
if (!oidcSession) {
|
|
console.log('[auth] No OIDC session found');
|
|
return false;
|
|
}
|
|
|
|
// Parse and validate session data
|
|
let sessionData;
|
|
try {
|
|
sessionData = JSON.parse(oidcSession);
|
|
} catch (parseError) {
|
|
console.error('[auth] Failed to parse session cookie:', parseError);
|
|
return false;
|
|
}
|
|
|
|
// Validate session structure
|
|
if (!sessionData.user || !sessionData.accessToken) {
|
|
console.error('[auth] Invalid session structure:', {
|
|
hasUser: !!sessionData.user,
|
|
hasAccessToken: !!sessionData.accessToken
|
|
});
|
|
return false;
|
|
}
|
|
|
|
// Check if session is still valid
|
|
if (sessionData.expiresAt && Date.now() > sessionData.expiresAt) {
|
|
console.log('[auth] Session expired:', {
|
|
expiresAt: sessionData.expiresAt,
|
|
currentTime: Date.now(),
|
|
expiredSince: Date.now() - sessionData.expiresAt
|
|
});
|
|
return false;
|
|
}
|
|
|
|
console.log('[auth] Valid OIDC session found for user:', {
|
|
id: sessionData.user.id,
|
|
email: sessionData.user.email
|
|
});
|
|
return true;
|
|
|
|
} catch (error) {
|
|
console.error('[auth] OIDC session check failed:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the full authenticated session with user and groups
|
|
*/
|
|
export const getAuthSession = async (event: any): Promise<AuthSession> => {
|
|
try {
|
|
const sessionData = await $fetch('/api/auth/session', {
|
|
headers: {
|
|
cookie: getHeader(event, 'cookie') || ''
|
|
}
|
|
}) as AuthSession;
|
|
|
|
return sessionData;
|
|
} catch (error) {
|
|
console.error('[auth] Failed to get auth session:', error);
|
|
return { user: null, authenticated: false, groups: [] };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get user groups from the session
|
|
*/
|
|
export const getUserGroups = async (event: any): Promise<string[]> => {
|
|
const session = await getAuthSession(event);
|
|
return session.groups || [];
|
|
}
|
|
|
|
/**
|
|
* Check if user has specific role/group
|
|
*/
|
|
export const hasRole = async (event: any, role: string): Promise<boolean> => {
|
|
const groups = await getUserGroups(event);
|
|
return groups.includes(role);
|
|
}
|
|
|
|
/**
|
|
* Check if user has any of the specified roles
|
|
*/
|
|
export const hasAnyRole = async (event: any, roles: string[]): Promise<boolean> => {
|
|
const groups = await getUserGroups(event);
|
|
return roles.some(role => groups.includes(role));
|
|
}
|
|
|
|
/**
|
|
* Require authentication and optionally specific roles
|
|
*/
|
|
export const requireAuth = async (event: any, requiredRoles?: string[]): Promise<AuthSession> => {
|
|
console.log('[requireAuth] Checking authentication for:', event.node.req.url, 'Required roles:', requiredRoles);
|
|
|
|
// First check for internal API authentication
|
|
const internalAuth = checkInternalAuth(event);
|
|
if (internalAuth) {
|
|
console.log('[requireAuth] Internal API authentication successful');
|
|
return {
|
|
user: {
|
|
id: 'system',
|
|
email: 'system@internal',
|
|
username: 'system',
|
|
name: 'System',
|
|
authMethod: 'internal',
|
|
groups: ['admin'] // Internal calls have admin privileges
|
|
},
|
|
authenticated: true,
|
|
groups: ['admin']
|
|
};
|
|
}
|
|
|
|
// Get full session with groups
|
|
const session = await getAuthSession(event);
|
|
|
|
if (!session.authenticated || !session.user) {
|
|
console.log('[requireAuth] Authentication failed for:', event.node.req.url);
|
|
throw createError({
|
|
statusCode: 401,
|
|
statusMessage: "Authentication required. Please login."
|
|
});
|
|
}
|
|
|
|
// Check role requirements if specified
|
|
if (requiredRoles && requiredRoles.length > 0) {
|
|
const userGroups = session.groups || [];
|
|
const hasRequiredRole = requiredRoles.some(role => userGroups.includes(role));
|
|
|
|
if (!hasRequiredRole) {
|
|
console.log('[requireAuth] Access denied. User groups:', userGroups, 'Required roles:', requiredRoles);
|
|
throw createError({
|
|
statusCode: 403,
|
|
statusMessage: 'Insufficient permissions. This action requires one of the following roles: ' + requiredRoles.join(', ')
|
|
});
|
|
}
|
|
}
|
|
|
|
console.log('[requireAuth] Authentication successful for user:', session.user.email, 'Groups:', session.groups);
|
|
return session;
|
|
}
|
|
|
|
/**
|
|
* Check if the request is from an internal service/background task
|
|
*/
|
|
const checkInternalAuth = (event: any): boolean => {
|
|
const headers = event.node.req.headers;
|
|
|
|
// Check for internal service header with the system tag
|
|
const xTag = headers['x-tag'];
|
|
const xInternalSecret = headers['x-internal-secret'];
|
|
|
|
// System tag authentication (for background tasks)
|
|
if (xTag === '094ut234') {
|
|
console.log('[auth] Internal system tag authentication successful');
|
|
return true;
|
|
}
|
|
|
|
// Internal secret authentication (if set in environment)
|
|
const internalSecret = process.env.INTERNAL_API_SECRET;
|
|
if (internalSecret && xInternalSecret === internalSecret) {
|
|
console.log('[auth] Internal secret authentication successful');
|
|
return true;
|
|
}
|
|
|
|
// Check if request is from localhost (same container)
|
|
const xForwardedFor = headers['x-forwarded-for'];
|
|
const xRealIp = headers['x-real-ip'];
|
|
const remoteAddress = event.node.req.socket?.remoteAddress;
|
|
|
|
const isLocalhost = !xForwardedFor && !xRealIp &&
|
|
(remoteAddress === '127.0.0.1' || remoteAddress === '::1' || remoteAddress === '::ffff:127.0.0.1');
|
|
|
|
if (isLocalhost && xTag) {
|
|
console.log('[auth] Localhost with system tag authentication successful');
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get the authenticated user from the session
|
|
*/
|
|
export const getAuthenticatedUser = async (event: any): Promise<AuthenticatedUser | null> => {
|
|
try {
|
|
const session = await getAuthSession(event);
|
|
return session.user;
|
|
} catch (error) {
|
|
console.error('[getAuthenticatedUser] Error:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Authorization helper functions for common roles
|
|
*/
|
|
export const requireAdmin = async (event: any): Promise<AuthSession> => {
|
|
return requireAuth(event, ['admin']);
|
|
}
|
|
|
|
export const requireSalesOrAdmin = async (event: any): Promise<AuthSession> => {
|
|
return requireAuth(event, ['sales', 'admin']);
|
|
}
|
|
|
|
export const requireUserOrAbove = async (event: any): Promise<AuthSession> => {
|
|
return requireAuth(event, ['user', 'sales', 'admin']);
|
|
}
|
|
|
|
function parseCookies(cookieString: string): Record<string, string> {
|
|
return cookieString.split(';').reduce((cookies: Record<string, string>, cookie) => {
|
|
const [name, value] = cookie.trim().split('=');
|
|
if (name && value) {
|
|
cookies[name] = value;
|
|
}
|
|
return cookies;
|
|
}, {});
|
|
}
|