From ab1bc1faae51dfdeb703976de3fbf572db1cf7d7 Mon Sep 17 00:00:00 2001 From: alma Date: Sat, 10 Jan 2026 12:19:12 +0100 Subject: [PATCH] Missions/Equipe --- app/api/auth/service-sso/route.ts | 152 ------------------------ app/components/responsive-iframe.tsx | 74 ++---------- lib/services/iframe-auth.ts | 168 --------------------------- 3 files changed, 8 insertions(+), 386 deletions(-) delete mode 100644 app/api/auth/service-sso/route.ts delete mode 100644 lib/services/iframe-auth.ts diff --git a/app/api/auth/service-sso/route.ts b/app/api/auth/service-sso/route.ts deleted file mode 100644 index 2efd2c6..0000000 --- a/app/api/auth/service-sso/route.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getServerSession } from 'next-auth/next'; -import { authOptions } from '../options'; -import { getServiceConfigByUrl, generateAuthenticatedUrl, serviceAuthConfigs } from '@/lib/services/iframe-auth'; - -/** - * API endpoint to generate authenticated URLs for iframe services - * - * This endpoint exchanges the user's Keycloak access token for a - * service-specific authentication URL that can be used in iframes. - * - * Usage: - * GET /api/auth/service-sso?service=nextcloud&path=/apps/files - * GET /api/auth/service-sso?url=https://espace.slm-lab.net/apps/mail - */ -export async function GET(request: NextRequest) { - try { - const session = await getServerSession(authOptions); - - if (!session?.accessToken) { - return NextResponse.json( - { error: 'Unauthorized', message: 'No active session' }, - { status: 401 } - ); - } - - const searchParams = request.nextUrl.searchParams; - const serviceName = searchParams.get('service'); - const targetUrl = searchParams.get('url'); - const targetPath = searchParams.get('path') || '/'; - - const keycloakIssuer = process.env.KEYCLOAK_ISSUER; - if (!keycloakIssuer) { - return NextResponse.json( - { error: 'Configuration error', message: 'Keycloak issuer not configured' }, - { status: 500 } - ); - } - - let authenticatedUrl: string; - - if (serviceName) { - // Get auth URL by service name - const config = serviceAuthConfigs[serviceName.toLowerCase()]; - if (!config) { - return NextResponse.json( - { - error: 'Unknown service', - message: `Service '${serviceName}' not configured`, - availableServices: Object.keys(serviceAuthConfigs) - }, - { status: 400 } - ); - } - - authenticatedUrl = generateAuthenticatedUrl( - serviceName, - session.accessToken, - targetPath, - keycloakIssuer - ); - } else if (targetUrl) { - // Get auth URL by target URL - const config = getServiceConfigByUrl(targetUrl); - if (!config) { - // Service not configured, return original URL - return NextResponse.json({ - success: true, - url: targetUrl, - authType: 'none', - message: 'Service not configured for SSO, using original URL' - }); - } - - // Extract path from target URL - const urlObj = new URL(targetUrl); - authenticatedUrl = generateAuthenticatedUrl( - config.name.toLowerCase(), - session.accessToken, - urlObj.pathname + urlObj.search, - keycloakIssuer - ); - } else { - return NextResponse.json( - { - error: 'Missing parameter', - message: 'Provide either "service" or "url" parameter', - availableServices: Object.keys(serviceAuthConfigs) - }, - { status: 400 } - ); - } - - return NextResponse.json({ - success: true, - url: authenticatedUrl, - expiresIn: 300, // URL is valid for 5 minutes (token expiry) - }); - } catch (error) { - console.error('Error generating service SSO URL:', error); - return NextResponse.json( - { error: 'Internal server error', message: error instanceof Error ? error.message : 'Unknown error' }, - { status: 500 } - ); - } -} - -/** - * POST endpoint for RocketChat iframe authentication - * This generates a login token for RocketChat's iframe auth - */ -export async function POST(request: NextRequest) { - try { - const session = await getServerSession(authOptions); - - if (!session?.accessToken || !session?.user) { - return NextResponse.json( - { error: 'Unauthorized', message: 'No active session' }, - { status: 401 } - ); - } - - const body = await request.json(); - const { service } = body; - - if (service === 'rocketchat') { - // For RocketChat, we need to use their OAuth login or iframe auth - // This returns the token that can be used with postMessage - return NextResponse.json({ - success: true, - authData: { - token: session.accessToken, - userId: session.user.id, - email: session.user.email, - username: session.user.username, - }, - message: 'Use postMessage to send this to RocketChat iframe' - }); - } - - return NextResponse.json( - { error: 'Unsupported service for POST', message: 'Use GET for most services' }, - { status: 400 } - ); - } catch (error) { - console.error('Error in service SSO POST:', error); - return NextResponse.json( - { error: 'Internal server error', message: error instanceof Error ? error.message : 'Unknown error' }, - { status: 500 } - ); - } -} diff --git a/app/components/responsive-iframe.tsx b/app/components/responsive-iframe.tsx index fd0a5b1..c96346e 100644 --- a/app/components/responsive-iframe.tsx +++ b/app/components/responsive-iframe.tsx @@ -8,13 +8,9 @@ interface ResponsiveIframeProps { className?: string; allow?: string; style?: React.CSSProperties; - /** Enable SSO token forwarding for this iframe */ - enableSso?: boolean; - /** Service name for SSO (e.g., 'nextcloud', 'rocketchat') */ - serviceName?: string; } -// Known service domains for automatic SSO detection +// Known service domains for postMessage authentication const SSO_SERVICE_DOMAINS: Record = { 'espace.slm-lab.net': 'nextcloud', 'parole.slm-lab.net': 'rocketchat', @@ -44,54 +40,17 @@ export function ResponsiveIframe({ className = '', allow, style, - enableSso = true, - serviceName: propServiceName, }: ResponsiveIframeProps) { const iframeRef = useRef(null); const { data: session } = useSession(); const [isRefreshing, setIsRefreshing] = useState(false); const [iframeSrc, setIframeSrc] = useState(''); const [hasTriedRefresh, setHasTriedRefresh] = useState(false); - const [ssoStatus, setSsoStatus] = useState<'pending' | 'success' | 'fallback' | 'error'>('pending'); - // Detect service name from URL if not provided - const serviceName = propServiceName || detectServiceFromUrl(src); + // Detect service name from URL for postMessage auth + const serviceName = detectServiceFromUrl(src); - /** - * Attempt to get an SSO-authenticated URL from our API - */ - const getSsoAuthenticatedUrl = useCallback(async (originalUrl: string): Promise => { - if (!enableSso || !session?.accessToken) { - return originalUrl; - } - - try { - // Try to get an authenticated URL from our SSO API - const response = await fetch(`/api/auth/service-sso?url=${encodeURIComponent(originalUrl)}`, { - method: 'GET', - credentials: 'include', - }); - - if (response.ok) { - const data = await response.json(); - if (data.success && data.url) { - console.log(`[SSO] Got authenticated URL for ${serviceName || 'unknown service'}`); - setSsoStatus('success'); - return data.url; - } - } - - console.warn('[SSO] Failed to get authenticated URL, using original'); - setSsoStatus('fallback'); - return originalUrl; - } catch (error) { - console.error('[SSO] Error getting authenticated URL:', error); - setSsoStatus('error'); - return originalUrl; - } - }, [enableSso, session?.accessToken, serviceName]); - - // Refresh NextAuth session and get SSO URL before loading iframe + // Refresh NextAuth session before loading iframe useEffect(() => { // If no src, nothing to do if (!src) { @@ -153,10 +112,7 @@ export function ResponsiveIframe({ if (response.ok) { console.log('Session refreshed before loading iframe'); - - // Try to get SSO-authenticated URL - const authenticatedUrl = await getSsoAuthenticatedUrl(src); - setIframeSrc(authenticatedUrl); + setIframeSrc(src); } else { const errorData = await response.json().catch(() => ({})); @@ -168,22 +124,19 @@ export function ResponsiveIframe({ } console.warn('Failed to refresh session, loading iframe anyway (may require login)'); - // Still try SSO, then fall back to original URL - const authenticatedUrl = await getSsoAuthenticatedUrl(src); - setIframeSrc(authenticatedUrl); + setIframeSrc(src); } } catch (error) { console.error('Error refreshing session:', error); // On error, still try to load iframe with original URL setIframeSrc(src); - setSsoStatus('error'); } finally { setIsRefreshing(false); } }; refreshSession(); - }, [session, src, hasTriedRefresh, iframeSrc, getSsoAuthenticatedUrl]); + }, [session, src, hasTriedRefresh, iframeSrc]); // Listen for messages from iframe applications (logout, auth requests) useEffect(() => { @@ -333,17 +286,7 @@ export function ResponsiveIframe({
-

- {ssoStatus === 'pending' && 'Connexion SSO en cours...'} - {ssoStatus === 'success' && 'Authentification réussie...'} - {ssoStatus === 'fallback' && 'Chargement...'} - {ssoStatus === 'error' && 'Chargement (connexion manuelle peut être requise)...'} -

- {serviceName && ( -

- Service: {serviceName} -

- )} +

Chargement...

)} @@ -372,7 +315,6 @@ export function ResponsiveIframe({
-

En attente de la session...

)} diff --git a/lib/services/iframe-auth.ts b/lib/services/iframe-auth.ts deleted file mode 100644 index c0319f7..0000000 --- a/lib/services/iframe-auth.ts +++ /dev/null @@ -1,168 +0,0 @@ -/** - * 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 = { - // 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; - } - }); -}