diff --git a/apps/calendar/apps/web/src/lib/stores/auth.svelte.ts b/apps/calendar/apps/web/src/lib/stores/auth.svelte.ts index bc566beb6..d1a69f0d5 100644 --- a/apps/calendar/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/calendar/apps/web/src/lib/stores/auth.svelte.ts @@ -76,6 +76,7 @@ export const authStore = { /** * Initialize auth state from stored tokens + * Also tries SSO if no local tokens exist (cross-domain authentication) */ async initialize() { if (initialized) return; @@ -89,7 +90,19 @@ export const authStore = { loading = true; try { - const authenticated = await authService.isAuthenticated(); + // First, check if we have valid local tokens + let authenticated = await authService.isAuthenticated(); + + // If not authenticated locally, try SSO (shared session cookie) + if (!authenticated) { + console.log('No local tokens, trying SSO...'); + const ssoResult = await authService.trySSO(); + if (ssoResult.success) { + console.log('SSO successful, user authenticated via shared session'); + authenticated = true; + } + } + if (authenticated) { const userData = await authService.getUserFromToken(); user = userData; diff --git a/apps/chat/apps/web/src/lib/stores/auth.svelte.ts b/apps/chat/apps/web/src/lib/stores/auth.svelte.ts index c1b3aa87c..3d8ebdf82 100644 --- a/apps/chat/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/chat/apps/web/src/lib/stores/auth.svelte.ts @@ -76,6 +76,7 @@ export const authStore = { /** * Initialize auth state from stored tokens + * Also tries SSO if no local tokens exist (cross-domain authentication) */ async initialize() { if (initialized) return; @@ -89,7 +90,19 @@ export const authStore = { loading = true; try { - const authenticated = await authService.isAuthenticated(); + // First, check if we have valid local tokens + let authenticated = await authService.isAuthenticated(); + + // If not authenticated locally, try SSO (shared session cookie) + if (!authenticated) { + console.log('No local tokens, trying SSO...'); + const ssoResult = await authService.trySSO(); + if (ssoResult.success) { + console.log('SSO successful, user authenticated via shared session'); + authenticated = true; + } + } + if (authenticated) { const userData = await authService.getUserFromToken(); user = userData; diff --git a/apps/clock/apps/web/src/lib/stores/auth.svelte.ts b/apps/clock/apps/web/src/lib/stores/auth.svelte.ts index b684571aa..48051c207 100644 --- a/apps/clock/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/clock/apps/web/src/lib/stores/auth.svelte.ts @@ -75,6 +75,7 @@ export const authStore = { /** * Initialize auth state from stored tokens + * Also tries SSO if no local tokens exist (cross-domain authentication) */ async initialize() { if (initialized) return; @@ -88,7 +89,19 @@ export const authStore = { loading = true; try { - const authenticated = await authService.isAuthenticated(); + // First, check if we have valid local tokens + let authenticated = await authService.isAuthenticated(); + + // If not authenticated locally, try SSO (shared session cookie) + if (!authenticated) { + console.log('No local tokens, trying SSO...'); + const ssoResult = await authService.trySSO(); + if (ssoResult.success) { + console.log('SSO successful, user authenticated via shared session'); + authenticated = true; + } + } + if (authenticated) { const userData = await authService.getUserFromToken(); user = userData; diff --git a/apps/contacts/apps/web/src/lib/stores/auth.svelte.ts b/apps/contacts/apps/web/src/lib/stores/auth.svelte.ts index 29f52d548..a87d09ca9 100644 --- a/apps/contacts/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/contacts/apps/web/src/lib/stores/auth.svelte.ts @@ -76,6 +76,7 @@ export const authStore = { /** * Initialize auth state from stored tokens + * Also tries SSO if no local tokens exist (cross-domain authentication) */ async initialize() { if (initialized) return; @@ -89,7 +90,19 @@ export const authStore = { loading = true; try { - const authenticated = await authService.isAuthenticated(); + // First, check if we have valid local tokens + let authenticated = await authService.isAuthenticated(); + + // If not authenticated locally, try SSO (shared session cookie) + if (!authenticated) { + console.log('No local tokens, trying SSO...'); + const ssoResult = await authService.trySSO(); + if (ssoResult.success) { + console.log('SSO successful, user authenticated via shared session'); + authenticated = true; + } + } + if (authenticated) { const userData = await authService.getUserFromToken(); user = userData; diff --git a/apps/manacore/apps/web/src/lib/stores/auth.svelte.ts b/apps/manacore/apps/web/src/lib/stores/auth.svelte.ts index 705248c54..8533c899d 100644 --- a/apps/manacore/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/manacore/apps/web/src/lib/stores/auth.svelte.ts @@ -63,6 +63,7 @@ export const authStore = { /** * Initialize auth state from stored tokens + * Also tries SSO if no local tokens exist (cross-domain authentication) */ async initialize() { if (initialized) return; @@ -76,7 +77,19 @@ export const authStore = { loading = true; try { - const authenticated = await authService.isAuthenticated(); + // First, check if we have valid local tokens + let authenticated = await authService.isAuthenticated(); + + // If not authenticated locally, try SSO (shared session cookie) + if (!authenticated) { + console.log('No local tokens, trying SSO...'); + const ssoResult = await authService.trySSO(); + if (ssoResult.success) { + console.log('SSO successful, user authenticated via shared session'); + authenticated = true; + } + } + if (authenticated) { const userData = await authService.getUserFromToken(); user = userData; diff --git a/apps/manadeck/apps/web/src/lib/stores/auth.svelte.ts b/apps/manadeck/apps/web/src/lib/stores/auth.svelte.ts index 44b5a455d..757363cda 100644 --- a/apps/manadeck/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/manadeck/apps/web/src/lib/stores/auth.svelte.ts @@ -32,11 +32,24 @@ export const authStore = { /** * Initialize auth state from stored tokens + * Also tries SSO if no local tokens exist (cross-domain authentication) */ async initialize() { loading = true; try { - const isAuth = await authService.isAuthenticated(); + // First, check if we have valid local tokens + let isAuth = await authService.isAuthenticated(); + + // If not authenticated locally, try SSO (shared session cookie) + if (!isAuth) { + console.log('No local tokens, trying SSO...'); + const ssoResult = await authService.trySSO(); + if (ssoResult.success) { + console.log('SSO successful, user authenticated via shared session'); + isAuth = true; + } + } + if (isAuth) { const userData = await authService.getUserFromToken(); user = toManaUser(userData); diff --git a/apps/nutriphi/apps/web/src/lib/stores/auth.svelte.ts b/apps/nutriphi/apps/web/src/lib/stores/auth.svelte.ts index adf5c835b..376666fec 100644 --- a/apps/nutriphi/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/nutriphi/apps/web/src/lib/stores/auth.svelte.ts @@ -63,6 +63,7 @@ export const authStore = { /** * Initialize auth state from stored tokens + * Also tries SSO if no local tokens exist (cross-domain authentication) */ async initialize() { if (initialized) return; @@ -76,7 +77,19 @@ export const authStore = { loading = true; try { - const authenticated = await authService.isAuthenticated(); + // First, check if we have valid local tokens + let authenticated = await authService.isAuthenticated(); + + // If not authenticated locally, try SSO (shared session cookie) + if (!authenticated) { + console.log('No local tokens, trying SSO...'); + const ssoResult = await authService.trySSO(); + if (ssoResult.success) { + console.log('SSO successful, user authenticated via shared session'); + authenticated = true; + } + } + if (authenticated) { const userData = await authService.getUserFromToken(); user = userData; diff --git a/apps/picture/apps/web/src/lib/stores/auth.svelte.ts b/apps/picture/apps/web/src/lib/stores/auth.svelte.ts index 4905f5842..cec72778e 100644 --- a/apps/picture/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/picture/apps/web/src/lib/stores/auth.svelte.ts @@ -77,8 +77,23 @@ export const authStore = { try { const authService = await getAuthService(); if (authService) { - const userData = await authService.getUserFromToken(); - user = userData; + // First, check if we have valid local tokens + let authenticated = await authService.isAuthenticated(); + + // If not authenticated locally, try SSO (shared session cookie) + if (!authenticated) { + console.log('No local tokens, trying SSO...'); + const ssoResult = await authService.trySSO(); + if (ssoResult.success) { + console.log('SSO successful, user authenticated via shared session'); + authenticated = true; + } + } + + if (authenticated) { + const userData = await authService.getUserFromToken(); + user = userData; + } } } catch (error) { console.error('Auth initialization error:', error); diff --git a/apps/planta/apps/web/src/lib/stores/auth.svelte.ts b/apps/planta/apps/web/src/lib/stores/auth.svelte.ts index 7774b83e8..6d34dfaee 100644 --- a/apps/planta/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/planta/apps/web/src/lib/stores/auth.svelte.ts @@ -69,6 +69,10 @@ export const authStore = { return initialized; }, + /** + * Initialize auth state from stored tokens + * Also tries SSO if no local tokens exist (cross-domain authentication) + */ async initialize() { if (initialized) return; @@ -81,7 +85,19 @@ export const authStore = { loading = true; try { - const authenticated = await authService.isAuthenticated(); + // First, check if we have valid local tokens + let authenticated = await authService.isAuthenticated(); + + // If not authenticated locally, try SSO (shared session cookie) + if (!authenticated) { + console.log('No local tokens, trying SSO...'); + const ssoResult = await authService.trySSO(); + if (ssoResult.success) { + console.log('SSO successful, user authenticated via shared session'); + authenticated = true; + } + } + if (authenticated) { const userData = await authService.getUserFromToken(); user = userData; diff --git a/apps/presi/apps/web/src/lib/stores/auth.svelte.ts b/apps/presi/apps/web/src/lib/stores/auth.svelte.ts index ae60fbc2f..d1789af66 100644 --- a/apps/presi/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/presi/apps/web/src/lib/stores/auth.svelte.ts @@ -47,6 +47,7 @@ export const auth = { /** * Initialize auth state from stored tokens + * Also tries SSO if no local tokens exist (cross-domain authentication) */ async init() { if (initialized) return; @@ -60,7 +61,19 @@ export const auth = { loading = true; try { - const authenticated = await authService.isAuthenticated(); + // First, check if we have valid local tokens + let authenticated = await authService.isAuthenticated(); + + // If not authenticated locally, try SSO (shared session cookie) + if (!authenticated) { + console.log('No local tokens, trying SSO...'); + const ssoResult = await authService.trySSO(); + if (ssoResult.success) { + console.log('SSO successful, user authenticated via shared session'); + authenticated = true; + } + } + if (authenticated) { const userData = await authService.getUserFromToken(); user = userData; diff --git a/apps/questions/apps/web/src/lib/stores/auth.svelte.ts b/apps/questions/apps/web/src/lib/stores/auth.svelte.ts index 5074f5f24..0e608e48f 100644 --- a/apps/questions/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/questions/apps/web/src/lib/stores/auth.svelte.ts @@ -65,6 +65,10 @@ export const authStore = { return initialized; }, + /** + * Initialize auth state from stored tokens + * Also tries SSO if no local tokens exist (cross-domain authentication) + */ async initialize() { if (initialized) return; @@ -77,7 +81,19 @@ export const authStore = { loading = true; try { - const authenticated = await authService.isAuthenticated(); + // First, check if we have valid local tokens + let authenticated = await authService.isAuthenticated(); + + // If not authenticated locally, try SSO (shared session cookie) + if (!authenticated) { + console.log('No local tokens, trying SSO...'); + const ssoResult = await authService.trySSO(); + if (ssoResult.success) { + console.log('SSO successful, user authenticated via shared session'); + authenticated = true; + } + } + if (authenticated) { const userData = await authService.getUserFromToken(); user = userData; diff --git a/apps/skilltree/apps/web/src/lib/stores/auth.svelte.ts b/apps/skilltree/apps/web/src/lib/stores/auth.svelte.ts index 313eca06e..b7c81c175 100644 --- a/apps/skilltree/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/skilltree/apps/web/src/lib/stores/auth.svelte.ts @@ -70,6 +70,10 @@ export const authStore = { return initialized; }, + /** + * Initialize auth state from stored tokens + * Also tries SSO if no local tokens exist (cross-domain authentication) + */ async initialize() { if (initialized) return; @@ -82,7 +86,19 @@ export const authStore = { loading = true; try { - const authenticated = await authService.isAuthenticated(); + // First, check if we have valid local tokens + let authenticated = await authService.isAuthenticated(); + + // If not authenticated locally, try SSO (shared session cookie) + if (!authenticated) { + console.log('No local tokens, trying SSO...'); + const ssoResult = await authService.trySSO(); + if (ssoResult.success) { + console.log('SSO successful, user authenticated via shared session'); + authenticated = true; + } + } + if (authenticated) { const userData = await authService.getUserFromToken(); user = userData; diff --git a/apps/storage/apps/web/src/lib/stores/auth.svelte.ts b/apps/storage/apps/web/src/lib/stores/auth.svelte.ts index a45cb3054..c61494102 100644 --- a/apps/storage/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/storage/apps/web/src/lib/stores/auth.svelte.ts @@ -39,6 +39,10 @@ export const authStore = { return initialized; }, + /** + * Initialize auth state from stored tokens + * Also tries SSO if no local tokens exist (cross-domain authentication) + */ async initialize() { if (initialized) return; @@ -51,7 +55,19 @@ export const authStore = { loading = true; try { - const authenticated = await authService.isAuthenticated(); + // First, check if we have valid local tokens + let authenticated = await authService.isAuthenticated(); + + // If not authenticated locally, try SSO (shared session cookie) + if (!authenticated) { + console.log('No local tokens, trying SSO...'); + const ssoResult = await authService.trySSO(); + if (ssoResult.success) { + console.log('SSO successful, user authenticated via shared session'); + authenticated = true; + } + } + if (authenticated) { const userData = await authService.getUserFromToken(); user = userData; diff --git a/apps/todo/apps/web/src/lib/stores/auth.svelte.ts b/apps/todo/apps/web/src/lib/stores/auth.svelte.ts index 797ff603b..ce4278b44 100644 --- a/apps/todo/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/todo/apps/web/src/lib/stores/auth.svelte.ts @@ -83,6 +83,7 @@ export const authStore = { /** * Initialize auth state from stored tokens + * Also tries SSO if no local tokens exist (cross-domain authentication) */ async initialize() { if (initialized) return; @@ -96,7 +97,19 @@ export const authStore = { loading = true; try { - const authenticated = await authService.isAuthenticated(); + // First, check if we have valid local tokens + let authenticated = await authService.isAuthenticated(); + + // If not authenticated locally, try SSO (shared session cookie) + if (!authenticated) { + console.log('No local tokens, trying SSO...'); + const ssoResult = await authService.trySSO(); + if (ssoResult.success) { + console.log('SSO successful, user authenticated via shared session'); + authenticated = true; + } + } + if (authenticated) { const userData = await authService.getUserFromToken(); user = userData; diff --git a/apps/zitare/apps/web/src/lib/stores/auth.svelte.ts b/apps/zitare/apps/web/src/lib/stores/auth.svelte.ts index e8d02a327..b45926b5a 100644 --- a/apps/zitare/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/zitare/apps/web/src/lib/stores/auth.svelte.ts @@ -76,6 +76,7 @@ export const authStore = { /** * Initialize auth state from stored tokens + * Also tries SSO if no local tokens exist (cross-domain authentication) */ async initialize() { if (initialized) return; @@ -89,7 +90,19 @@ export const authStore = { loading = true; try { - const authenticated = await authService.isAuthenticated(); + // First, check if we have valid local tokens + let authenticated = await authService.isAuthenticated(); + + // If not authenticated locally, try SSO (shared session cookie) + if (!authenticated) { + console.log('No local tokens, trying SSO...'); + const ssoResult = await authService.trySSO(); + if (ssoResult.success) { + console.log('SSO successful, user authenticated via shared session'); + authenticated = true; + } + } + if (authenticated) { const userData = await authService.getUserFromToken(); user = userData; diff --git a/packages/shared-auth/src/core/authService.ts b/packages/shared-auth/src/core/authService.ts index 2f29c137e..adc050b68 100644 --- a/packages/shared-auth/src/core/authService.ts +++ b/packages/shared-auth/src/core/authService.ts @@ -44,6 +44,8 @@ const DEFAULT_ENDPOINTS: AuthEndpoints = { googleSignIn: '/api/v1/auth/google-signin', appleSignIn: '/api/v1/auth/apple-signin', credits: '/api/v1/credits/balance', + // Better Auth native endpoints for SSO + getSession: '/api/auth/get-session', }; /** @@ -613,6 +615,90 @@ export function createAuthService(config: AuthServiceConfig) { getStorageKeys(): StorageKeys { return storageKeys; }, + + /** + * Try to authenticate using SSO session cookie + * + * This enables cross-domain SSO: If the user is logged in on another app + * (e.g., calendar.mana.how), they will automatically be logged in here + * via the shared session cookie on .mana.how + * + * @returns AuthResult with success=true if SSO succeeded + */ + async trySSO(): Promise { + try { + const storage = getStorageAdapter(); + + // Check if we already have valid tokens - skip SSO if so + const existingToken = await storage.getItem(storageKeys.APP_TOKEN); + if (existingToken && isTokenValidLocally(existingToken)) { + return { success: true }; + } + + // Try to get session from cookie (credentials: 'include' sends cookies) + const response = await fetch(`${baseUrl}${endpoints.getSession}`, { + method: 'GET', + credentials: 'include', // Send cookies cross-origin + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + // No valid session cookie - user needs to login manually + return { success: false, error: 'No SSO session found' }; + } + + const data = await response.json(); + + // Better Auth returns session with user info + if (!data.session || !data.user) { + return { success: false, error: 'Invalid session response' }; + } + + // Now get tokens by signing in with the session + // We need to exchange the session for JWT tokens + const tokenResponse = await fetch(`${baseUrl}/api/v1/auth/session-to-token`, { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!tokenResponse.ok) { + // Fallback: Session exists but no token endpoint + // Store session info for display, but user may need to re-authenticate for API calls + console.warn('SSO: Session found but token exchange not available'); + return { success: false, error: 'Token exchange not available' }; + } + + const tokenData = await tokenResponse.json(); + const appToken = tokenData.accessToken; + const refreshToken = tokenData.refreshToken; + + if (!appToken || !refreshToken) { + return { success: false, error: 'Invalid token response' }; + } + + // Store the tokens + await Promise.all([ + storage.setItem(storageKeys.APP_TOKEN, appToken), + storage.setItem(storageKeys.REFRESH_TOKEN, refreshToken), + storage.setItem(storageKeys.USER_EMAIL, data.user.email || ''), + ]); + + console.log('SSO: Successfully authenticated via session cookie'); + return { success: true }; + } catch (error) { + // SSO failed - this is expected if user hasn't logged in anywhere + console.debug('SSO check failed:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'SSO check failed', + }; + } + }, }; return service; diff --git a/packages/shared-auth/src/types/index.ts b/packages/shared-auth/src/types/index.ts index 668a52d02..cdc1dcb8b 100644 --- a/packages/shared-auth/src/types/index.ts +++ b/packages/shared-auth/src/types/index.ts @@ -133,6 +133,8 @@ export interface AuthEndpoints { googleSignIn: string; appleSignIn: string; credits: string; + /** Better Auth native endpoint for SSO session check */ + getSession: string; } /** diff --git a/services/mana-core-auth/src/auth/auth.controller.ts b/services/mana-core-auth/src/auth/auth.controller.ts index fe3c0c8bc..7214ca547 100644 --- a/services/mana-core-auth/src/auth/auth.controller.ts +++ b/services/mana-core-auth/src/auth/auth.controller.ts @@ -9,7 +9,10 @@ import { Headers, HttpCode, HttpStatus, + Req, + Res, } from '@nestjs/common'; +import type { Request, Response } from 'express'; import { Throttle, ThrottlerGuard } from '@nestjs/throttler'; import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiBody } from '@nestjs/swagger'; import { BetterAuthService } from './services/better-auth.service'; @@ -181,6 +184,51 @@ export class AuthController { return this.betterAuthService.validateToken(body.token); } + /** + * Exchange session cookie for JWT tokens (SSO) + * + * This endpoint enables cross-domain Single Sign-On (SSO). + * If the user has a valid session cookie (from logging in on another app), + * this returns JWT tokens that the app can use for API calls. + * + * The session cookie is set on .mana.how domain, so it's shared across: + * - calendar.mana.how + * - todo.mana.how + * - contacts.mana.how + * - etc. + */ + @Post('session-to-token') + @HttpCode(HttpStatus.OK) + @ApiOperation({ + summary: 'Exchange session cookie for JWT tokens', + description: + 'SSO endpoint: If user has a valid session cookie, returns JWT access and refresh tokens.', + }) + @ApiResponse({ + status: 200, + description: 'Tokens generated successfully', + schema: { + type: 'object', + properties: { + user: { + type: 'object', + properties: { + id: { type: 'string' }, + email: { type: 'string' }, + name: { type: 'string' }, + }, + }, + accessToken: { type: 'string' }, + refreshToken: { type: 'string' }, + expiresIn: { type: 'number', example: 900 }, + }, + }, + }) + @ApiResponse({ status: 401, description: 'No valid session cookie' }) + async sessionToToken(@Req() req: Request, @Res({ passthrough: true }) res: Response) { + return this.betterAuthService.sessionToToken(req, res); + } + /** * Get JWKS (JSON Web Key Set) * diff --git a/services/mana-core-auth/src/auth/services/better-auth.service.ts b/services/mana-core-auth/src/auth/services/better-auth.service.ts index 2f2a98407..431f96bef 100644 --- a/services/mana-core-auth/src/auth/services/better-auth.service.ts +++ b/services/mana-core-auth/src/auth/services/better-auth.service.ts @@ -1345,4 +1345,134 @@ export class BetterAuthService { throw error; } } + + // ========================================================================= + // SSO Methods + // ========================================================================= + + /** + * Exchange session cookie for JWT tokens (SSO) + * + * This enables cross-domain Single Sign-On. When a user is logged in + * on one app (e.g., calendar.mana.how), they have a session cookie on + * .mana.how domain. This method allows other apps to exchange that + * cookie for JWT tokens they can use for API calls. + * + * @param req - Express request with cookies + * @param res - Express response for setting headers + * @returns JWT tokens or throws UnauthorizedException + */ + async sessionToToken( + req: import('express').Request, + res: import('express').Response + ): Promise { + try { + // Get session cookie name (Better Auth uses this format with our prefix) + const cookiePrefix = process.env.COOKIE_DOMAIN ? 'mana' : 'better-auth'; + const sessionCookieName = `__Secure-${cookiePrefix}.session_token`; + const fallbackCookieName = `${cookiePrefix}.session_token`; + + // Try to get session token from cookies + const sessionToken = req.cookies?.[sessionCookieName] || req.cookies?.[fallbackCookieName]; + + if (!sessionToken) { + this.logger.debug('SSO: No session cookie found', { + cookies: Object.keys(req.cookies || {}), + }); + throw new UnauthorizedException('No session cookie found'); + } + + this.logger.debug('SSO: Found session cookie, validating...'); + + // Use Better Auth's getSession to validate the cookie + // We need to create a Request object that Better Auth can process + const baseUrl = this.configService.get('BASE_URL') || 'http://localhost:3001'; + const url = new URL('/api/auth/get-session', baseUrl); + + const headers = new Headers({ + Cookie: `${sessionCookieName}=${sessionToken}`, + }); + + const fetchRequest = new Request(url.toString(), { + method: 'GET', + headers, + }); + + const response = await this.auth.handler(fetchRequest); + + if (!response.ok) { + this.logger.debug('SSO: Session validation failed', { status: response.status }); + throw new UnauthorizedException('Invalid or expired session'); + } + + const sessionData = await response.json(); + + if (!sessionData?.user || !sessionData?.session) { + this.logger.debug('SSO: Invalid session response', { sessionData }); + throw new UnauthorizedException('Invalid session data'); + } + + const { user, session } = sessionData; + + this.logger.debug('SSO: Session validated, generating JWT tokens', { + userId: user.id, + email: user.email, + }); + + // Generate JWT access token using Better Auth's JWT plugin + let accessToken = ''; + try { + const api = this.auth.api as any; + + const jwtResult = await api.signJWT({ + body: { + payload: { + sub: user.id, + email: user.email, + role: user.role || 'user', + sid: session.id || '', + }, + }, + }); + + accessToken = jwtResult?.token || ''; + + if (!accessToken) { + throw new Error('Better Auth signJWT returned empty token'); + } + } catch (jwtError) { + this.logger.warn('SSO: JWT generation failed, using session token', { + error: jwtError instanceof Error ? jwtError.message : 'Unknown error', + }); + // Use session token as fallback + accessToken = session.token || sessionToken; + } + + this.logger.info('SSO: Successfully exchanged session cookie for JWT tokens', { + userId: user.id, + }); + + return { + user: { + id: user.id, + email: user.email, + name: user.name, + role: user.role, + }, + accessToken, + refreshToken: session.token || sessionToken, + expiresIn: 15 * 60, // 15 minutes in seconds + }; + } catch (error) { + if (error instanceof UnauthorizedException) { + throw error; + } + + this.logger.error( + 'SSO: Token exchange failed', + error instanceof Error ? error.stack : undefined + ); + throw new UnauthorizedException('Failed to exchange session for tokens'); + } + } }