/** * Label Studio SSO Route * * Implements subdomain-based cookie sharing for Label Studio SSO authentication. * This approach eliminates the need for proxy by using shared domain cookies. * * ## Architecture: * 1. Client calls /label-studio/sso/setup * 2. Backend gets JWT token from Label Studio using API Token * 3. Backend sets cookie with shared domain (e.g., .nubison.localhost) * 4. Client loads Label Studio directly in iframe * 5. Cookie is automatically sent with iframe requests * * ## Configuration: * Label Studio .env must include: * - JWT_SSO_SECRET: Shared secret for JWT signing * - JWT_SSO_COOKIE_NAME: Cookie name (default: ls_auth_token) * - CSRF_TRUSTED_ORIGINS: Include all frontend domains * - ALLOWED_HOSTS: Include all subdomain patterns */ import Koa from 'koa' import Router from 'koa-router' import { config } from '@things-factory/env' import { LabelStudioSSOService } from '../service/label-studio-sso-service.js' const ssoRouter = new Router() /** * Get Label Studio configuration */ function getLabelStudioConfig() { const labelStudioConfig = config.get('labelStudio', { serverUrl: 'http://localhost:8080', apiToken: '', cookieDomain: '' // e.g., '.nubison.localhost' for subdomain sharing }) return { serverUrl: labelStudioConfig.serverUrl, apiToken: labelStudioConfig.apiToken, cookieDomain: labelStudioConfig.cookieDomain || '' } } /** * SSO Setup Endpoint * * This endpoint must be called by the client before loading Label Studio iframe * to establish SSO authentication using subdomain cookie sharing. * * Flow: * 1. Client calls /label-studio/sso/setup * 2. Backend requests JWT token from Label Studio * 3. Backend sets ls_auth_token cookie with shared domain * 4. Client loads Label Studio iframe - auto-login succeeds * * @example * fetch('/label-studio/sso/setup', { credentials: 'include' }) */ ssoRouter.get('/label-studio/sso/setup', async (ctx: Koa.Context) => { try { const user = ctx.state.user if (!user || !user.email) { ctx.status = 401 ctx.body = { success: false, error: 'Unauthorized', message: 'User authentication required' } return } const { cookieDomain } = getLabelStudioConfig() // Cookie name must match Label Studio's JWT_SSO_COOKIE_NAME setting const cookieName = 'ls_auth_token' const existingToken = ctx.cookies.get(cookieName) if (existingToken) { // Token already exists ctx.status = 200 ctx.body = { success: true, message: 'SSO token already exists', user: user.email } return } console.log(`[Label Studio SSO] Setting up token for ${user.email}`) // Request JWT token from Label Studio const tokenData = await LabelStudioSSOService.getSSOToken(user.email) if (tokenData) { // Clear existing sessionid to prevent conflict with SSO token ctx.cookies.set('sessionid', '', { domain: cookieDomain || undefined, path: '/', maxAge: 0 // Expire immediately }) // Set cookie with shared domain for subdomain access const cookieOptions: any = { httpOnly: false, // Allow client-side access for debugging secure: ctx.protocol === 'https', sameSite: 'lax', path: '/', maxAge: tokenData.expires_in * 1000 // Convert seconds to milliseconds } // Only set domain if cookieDomain is configured // This allows same-origin cookie for single domain setup if (cookieDomain) { cookieOptions.domain = cookieDomain console.log(`[Label Studio SSO] Using shared cookie domain: ${cookieDomain}`) } ctx.cookies.set(cookieName, tokenData.token, cookieOptions) console.log( `[Label Studio SSO] Token set for ${user.email} (expires in ${tokenData.expires_in}s, domain: ${cookieDomain || 'same-origin'})` ) ctx.status = 200 ctx.body = { success: true, message: 'SSO token setup complete', user: user.email, expiresIn: tokenData.expires_in, cookieDomain: cookieDomain || 'same-origin' } } else { console.error(`[Label Studio SSO] Failed to acquire token for ${user.email}`) ctx.status = 500 ctx.body = { success: false, error: 'Token Acquisition Failed', message: 'Failed to acquire SSO token from Label Studio' } } } catch (error: any) { console.error('[Label Studio SSO] Setup error:', error.message) ctx.status = 500 ctx.body = { success: false, error: 'Internal Server Error', message: error.message } } }) /** * Health check endpoint */ ssoRouter.get('/label-studio/sso/health', async (ctx: Koa.Context) => { try { const { serverUrl, cookieDomain } = getLabelStudioConfig() ctx.status = 200 ctx.body = { status: 'ok', labelStudioUrl: serverUrl || 'not configured', cookieDomain: cookieDomain || 'same-origin', message: 'Label Studio SSO is running' } } catch (error: any) { ctx.status = 503 ctx.body = { status: 'error', message: error.message } } }) /** * Configuration endpoint */ ssoRouter.get('/label-studio/sso/config', async (ctx: Koa.Context) => { const { serverUrl, apiToken, cookieDomain } = getLabelStudioConfig() ctx.status = 200 ctx.body = { labelStudioUrl: serverUrl || 'not configured', cookieDomain: cookieDomain || 'same-origin', hasApiToken: !!apiToken, ssoConfigured: LabelStudioSSOService.verifyConfig() } }) export { ssoRouter }