import { Request, Response } from 'express'; import { AuthService } from '../services/auth.service'; import { validateSSOCallback, validateRefreshToken, validateTokenExchange } from '../validators/auth.validator'; import { ResponseHandler } from '../utils/responseHandler'; import type { AuthenticatedRequest } from '../types/express'; import logger from '../utils/logger'; export class AuthController { private authService: AuthService; constructor() { this.authService = new AuthService(); } /** * Handle SSO callback from frontend * POST /api/v1/auth/sso-callback */ async handleSSOCallback(req: Request, res: Response): Promise { try { // Validate request body const validatedData = validateSSOCallback(req.body); const result = await this.authService.handleSSOCallback(validatedData as any); ResponseHandler.success(res, { user: result.user, accessToken: result.accessToken, refreshToken: result.refreshToken }, 'Authentication successful'); } catch (error) { logger.error('SSO callback failed:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; ResponseHandler.error(res, 'Authentication failed', 400, errorMessage); } } /** * Get current user profile * GET /api/v1/auth/me */ async getCurrentUser(req: AuthenticatedRequest, res: Response): Promise { try { const user = await this.authService.getUserProfile(req.user.userId); if (!user) { ResponseHandler.notFound(res, 'User not found'); return; } ResponseHandler.success(res, { userId: user.userId, employeeId: user.employeeId, email: user.email, firstName: user.firstName, lastName: user.lastName, displayName: user.displayName, department: user.department, designation: user.designation, phone: user.phone, location: user.location, isAdmin: user.isAdmin, isActive: user.isActive, lastLogin: user.lastLogin, createdAt: user.createdAt, updatedAt: user.updatedAt }, 'User profile retrieved successfully'); } catch (error) { logger.error('Failed to get current user:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; ResponseHandler.error(res, 'Failed to get user profile', 500, errorMessage); } } /** * Refresh access token * POST /api/v1/auth/refresh * Supports both request body and cookie-based refresh tokens */ async refreshToken(req: Request, res: Response): Promise { try { // Try to get refresh token from request body first, then from cookies let refreshToken: string; if (req.body?.refreshToken) { const validated = validateRefreshToken(req.body); refreshToken = validated.refreshToken; } else if ((req as any).cookies?.refreshToken) { // Fallback to cookie if available (requires cookie-parser middleware) refreshToken = (req as any).cookies.refreshToken; } else { throw new Error('Refresh token is required'); } const newAccessToken = await this.authService.refreshAccessToken(refreshToken); // Set new access token in cookie if using cookie-based auth const isProduction = process.env.NODE_ENV === 'production'; const cookieOptions = { httpOnly: true, secure: isProduction, sameSite: 'lax' as const, maxAge: 24 * 60 * 60 * 1000, // 24 hours }; res.cookie('accessToken', newAccessToken, cookieOptions); ResponseHandler.success(res, { accessToken: newAccessToken }, 'Token refreshed successfully'); } catch (error) { logger.error('Token refresh failed:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; ResponseHandler.error(res, 'Token refresh failed', 401, errorMessage); } } /** * Logout user * POST /api/v1/auth/logout * Clears all authentication cookies and tokens * IMPORTANT: Must use EXACT same cookie options as when setting cookies */ async logout(req: Request, res: Response): Promise { const isProduction = process.env.NODE_ENV === 'production'; // Helper function to clear cookies with all possible option combinations const clearCookiesCompletely = () => { const cookieNames = ['accessToken', 'refreshToken']; // Get the EXACT options used when setting cookies (from exchangeToken) // These MUST match exactly: httpOnly, secure, sameSite, path const cookieOptions = { httpOnly: true, secure: isProduction, sameSite: 'lax' as const, path: '/', }; logger.info('Attempting to clear cookies with options:', { httpOnly: cookieOptions.httpOnly, secure: cookieOptions.secure, sameSite: cookieOptions.sameSite, path: cookieOptions.path, isProduction, }); // Method 1: Set expired cookie with exact same options // This is the most reliable method - sets cookie to expire immediately const expiredDate = new Date(0); // Jan 1, 1970 cookieNames.forEach(name => { res.cookie(name, '', { httpOnly: cookieOptions.httpOnly, secure: cookieOptions.secure, sameSite: cookieOptions.sameSite, path: cookieOptions.path, expires: expiredDate, maxAge: 0, }); logger.info(`Set expired cookie: ${name}`); }); // Method 2: Use clearCookie with exact same options // clearCookie requires same options that were used to set the cookie cookieNames.forEach(name => { res.clearCookie(name, cookieOptions); logger.info(`Called clearCookie for: ${name}`); }); // Method 3: Try without secure flag (for localhost/development) if (!isProduction) { cookieNames.forEach(name => { res.clearCookie(name, { httpOnly: true, secure: false, sameSite: 'lax', path: '/', }); }); } // Method 4: Try with all possible path variations const paths = ['/', '/api', '/api/v1']; paths.forEach(path => { cookieNames.forEach(name => { res.clearCookie(name, { httpOnly: true, secure: isProduction, sameSite: 'lax', path: path, }); }); }); logger.info('Cookies clearing attempted with all methods', { cookieNames, isProduction, paths: ['/', '/api', '/api/v1'], }); }; try { // Logout should work even without authentication (to clear cookies) // User might be null if token was invalid/expired const userId = req.user?.userId || 'unknown'; const email = req.user?.email || 'unknown'; logger.info('User logout initiated', { userId, email, hasUser: !!req.user, hasCookies: !!req.cookies?.accessToken || !!req.cookies?.refreshToken, hasHeaderToken: !!req.headers.authorization, }); // Clear all cookies using multiple methods clearCookiesCompletely(); logger.info('User logout successful - cookies cleared', { userId: req.user?.userId || 'unknown', email: req.user?.email || 'unknown', }); // Return success response ResponseHandler.success(res, { message: 'Logout successful, cookies cleared' }, 'Logout successful'); } catch (error) { logger.error('Logout failed:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; // Even on error, try to clear cookies as last resort try { clearCookiesCompletely(); } catch (cookieError) { logger.error('Error clearing cookies in catch block:', cookieError); } ResponseHandler.error(res, 'Logout failed', 500, errorMessage); } } /** * Validate token endpoint * GET /api/v1/auth/validate */ async validateToken(req: AuthenticatedRequest, res: Response): Promise { try { ResponseHandler.success(res, { valid: true, user: req.user }, 'Token is valid'); } catch (error) { logger.error('Token validation failed:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; ResponseHandler.error(res, 'Token validation failed', 401, errorMessage); } } /** * Exchange authorization code for tokens * POST /api/v1/auth/token-exchange */ async exchangeToken(req: Request, res: Response): Promise { try { logger.info('Token exchange request received', { body: { code: req.body?.code ? `${req.body.code.substring(0, 10)}...` : 'MISSING', redirectUri: req.body?.redirectUri, }, headers: req.headers, }); const { code, redirectUri } = validateTokenExchange(req.body); logger.info('Token exchange validation passed', { redirectUri }); const result = await this.authService.exchangeCodeForTokens(code, redirectUri); // Set cookies with httpOnly flag for security const isProduction = process.env.NODE_ENV === 'production'; const cookieOptions = { httpOnly: true, secure: isProduction, sameSite: 'lax' as const, maxAge: 24 * 60 * 60 * 1000, // 24 hours for access token }; res.cookie('accessToken', result.accessToken, cookieOptions); const refreshCookieOptions = { ...cookieOptions, maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days for refresh token }; res.cookie('refreshToken', result.refreshToken, refreshCookieOptions); // Ensure Content-Type is set to JSON res.setHeader('Content-Type', 'application/json'); logger.info('Sending token exchange response', { hasUser: !!result.user, hasAccessToken: !!result.accessToken, hasRefreshToken: !!result.refreshToken, }); ResponseHandler.success(res, { user: result.user, accessToken: result.accessToken, refreshToken: result.refreshToken, idToken: result.oktaIdToken // Include id_token for frontend logout }, 'Token exchange successful'); } catch (error) { logger.error('Token exchange failed:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; ResponseHandler.error(res, 'Token exchange failed', 400, errorMessage); } } }