NeahStable/lib/services/iframe-auth.ts
2026-01-10 12:12:30 +01:00

169 lines
5.2 KiB
TypeScript

/**
* Iframe Authentication Service
*
* Handles SSO token forwarding for embedded iframe services.
* Since browsers block third-party cookies, we need to pass authentication
* tokens directly to iframe services.
*/
export interface ServiceAuthConfig {
name: string;
baseUrl: string;
authType: 'oidc-redirect' | 'bearer-token' | 'custom-api' | 'iframe-auth' | 'none';
// OIDC redirect: Redirect to Keycloak with service's client_id
// bearer-token: Pass access_token as URL param or header
// custom-api: Service has custom auth endpoint
// iframe-auth: Service supports postMessage authentication
// none: No auth needed or service handles it internally
clientId?: string;
authEndpoint?: string;
tokenParam?: string;
}
// Service configurations - customize based on your Keycloak client setup
export const serviceAuthConfigs: Record<string, ServiceAuthConfig> = {
// NextCloud - supports OIDC and bearer token
nextcloud: {
name: 'NextCloud',
baseUrl: process.env.NEXT_PUBLIC_IFRAME_DRIVE_URL?.split('/apps')[0] || 'https://espace.slm-lab.net',
authType: 'oidc-redirect',
clientId: 'nextcloud', // Your Keycloak client ID for NextCloud
authEndpoint: '/apps/user_oidc/login',
},
// RocketChat - supports iframe authentication
rocketchat: {
name: 'RocketChat',
baseUrl: process.env.NEXT_PUBLIC_IFRAME_PAROLE_URL?.split('/channel')[0] || 'https://parole.slm-lab.net',
authType: 'iframe-auth',
authEndpoint: '/api/v1/login',
},
// Moodle - supports OIDC
moodle: {
name: 'Moodle',
baseUrl: process.env.NEXT_PUBLIC_IFRAME_LEARN_URL || 'https://apprendre.slm-lab.net',
authType: 'oidc-redirect',
clientId: 'moodle',
authEndpoint: '/auth/oidc/',
},
// Penpot - supports OIDC
penpot: {
name: 'Penpot',
baseUrl: process.env.NEXT_PUBLIC_IFRAME_ARTLAB_URL || 'https://artlab.slm-lab.net',
authType: 'oidc-redirect',
clientId: 'penpot',
},
// Open-WebUI - supports Bearer token
openwebui: {
name: 'Open-WebUI',
baseUrl: process.env.NEXT_PUBLIC_IFRAME_AI_ASSISTANT_URL || 'https://alma.slm-lab.net',
authType: 'bearer-token',
tokenParam: 'token', // or use Authorization header
},
// ListMonk - may need custom auth
listmonk: {
name: 'ListMonk',
baseUrl: process.env.NEXT_PUBLIC_IFRAME_THEMESSAGE_URL || 'https://lemessage.slm-lab.net',
authType: 'none', // ListMonk admin typically uses basic auth
},
// Leantime - already has OIDC login endpoint
leantime: {
name: 'Leantime',
baseUrl: 'https://agilite.slm-lab.net',
authType: 'oidc-redirect',
clientId: 'leantime',
authEndpoint: '/oidc/login',
},
// Jitsi - typically uses JWT
jitsi: {
name: 'Jitsi',
baseUrl: process.env.NEXT_PUBLIC_IFRAME_CONFERENCE_URL || 'https://vision.slm-lab.net',
authType: 'bearer-token',
tokenParam: 'jwt',
},
// BookStack - supports OIDC
bookstack: {
name: 'BookStack',
baseUrl: process.env.NEXT_PUBLIC_IFRAME_CHAPTER_URL || 'https://chapitre.slm-lab.net',
authType: 'oidc-redirect',
clientId: 'bookstack',
},
};
/**
* Generate an authenticated URL for a service
* This builds the appropriate auth URL based on the service type
*/
export function generateAuthenticatedUrl(
serviceName: string,
accessToken: string,
targetPath: string = '/',
keycloakIssuer: string
): string {
const config = serviceAuthConfigs[serviceName.toLowerCase()];
if (!config) {
console.warn(`No auth config found for service: ${serviceName}`);
return targetPath;
}
switch (config.authType) {
case 'oidc-redirect': {
// Build Keycloak authorization URL that will redirect to the service
const authUrl = new URL(`${keycloakIssuer}/protocol/openid-connect/auth`);
authUrl.searchParams.set('client_id', config.clientId || serviceName);
authUrl.searchParams.set('redirect_uri', `${config.baseUrl}${config.authEndpoint || '/'}${targetPath}`);
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('scope', 'openid profile email');
// Use login_hint to pre-fill username if available
// Use prompt=none to attempt silent auth (won't show login page)
authUrl.searchParams.set('prompt', 'none');
return authUrl.toString();
}
case 'bearer-token': {
// Add token as URL parameter
const url = new URL(`${config.baseUrl}${targetPath}`);
if (config.tokenParam) {
url.searchParams.set(config.tokenParam, accessToken);
}
return url.toString();
}
case 'iframe-auth': {
// For iframe auth, we'll handle it via postMessage
// Return base URL, authentication happens via JS
return `${config.baseUrl}${targetPath}`;
}
case 'custom-api':
case 'none':
default:
return `${config.baseUrl}${targetPath}`;
}
}
/**
* Get the auth configuration for a service based on its URL
*/
export function getServiceConfigByUrl(url: string): ServiceAuthConfig | undefined {
const urlObj = new URL(url);
const hostname = urlObj.hostname;
return Object.values(serviceAuthConfigs).find(config => {
try {
const configUrl = new URL(config.baseUrl);
return configUrl.hostname === hostname;
} catch {
return false;
}
});
}