import axios from 'axios'; import { logger } from '@/lib/logger'; // Get tenant ID from env var or use a default const tenantId = process.env.MICROSOFT_TENANT_ID || 'common'; // Use 'organizations' or actual tenant ID // Microsoft OAuth URLs with configurable tenant const MICROSOFT_AUTHORIZE_URL = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize`; const MICROSOFT_TOKEN_URL = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`; // Client configuration from environment variables const clientId = process.env.MICROSOFT_CLIENT_ID; const clientSecret = process.env.MICROSOFT_CLIENT_SECRET; const redirectUri = process.env.MICROSOFT_REDIRECT_URI; // NOTE: In production we do not log Microsoft OAuth configuration to avoid noise and potential leakage. // Required scopes for Microsoft Graph API (Mail and Calendar) // All scopes use the same resource (graph.microsoft.com) to avoid multi-resource errors const REQUIRED_SCOPES = [ 'offline_access', 'https://graph.microsoft.com/Mail.Read', // Read mail via Graph API 'https://graph.microsoft.com/Mail.Send', // Send mail via Graph API 'https://graph.microsoft.com/Calendars.Read', // Read calendars via Graph API 'https://graph.microsoft.com/Calendars.ReadWrite', // Read and write calendars via Graph API (for updates/deletes) ].join(' '); /** * Generates the authorization URL for Microsoft OAuth */ export function getMicrosoftAuthUrl(state: string): string { const params = new URLSearchParams({ client_id: clientId!, response_type: 'code', redirect_uri: redirectUri!, scope: REQUIRED_SCOPES, state, response_mode: 'query' }); return `${MICROSOFT_AUTHORIZE_URL}?${params.toString()}`; } /** * Exchange authorization code for tokens */ export async function exchangeCodeForTokens(code: string): Promise<{ access_token: string; refresh_token: string; expires_in: number; }> { const params = new URLSearchParams({ client_id: clientId!, client_secret: clientSecret!, code, redirect_uri: redirectUri!, grant_type: 'authorization_code', scope: REQUIRED_SCOPES // Include scopes in token exchange }); try { logger.debug('[MICROSOFT_OAUTH] Exchanging code for tokens', { url: MICROSOFT_TOKEN_URL }); const response = await axios.post(MICROSOFT_TOKEN_URL, params.toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); logger.debug('[MICROSOFT_OAUTH] Token exchange successful'); return { access_token: response.data.access_token, refresh_token: response.data.refresh_token, expires_in: response.data.expires_in }; } catch (error: any) { logger.error('[MICROSOFT_OAUTH] Error exchanging code for tokens', { error: error instanceof Error ? error.message : String(error) }); // Enhanced error logging if (error.response) { logger.error('[MICROSOFT_OAUTH] Response details', { data: error.response.data, status: error.response.status, headers: error.response.headers }); // Extract the error message from Microsoft's response format const errorData = error.response.data; if (errorData && errorData.error_description) { throw new Error(`Token exchange failed: ${errorData.error_description}`); } } throw new Error('Failed to exchange authorization code for tokens'); } } /** * Refresh an access token using a refresh token */ export async function refreshAccessToken(refreshToken: string): Promise<{ access_token: string; refresh_token?: string; expires_in: number; }> { const params = new URLSearchParams({ client_id: clientId!, client_secret: clientSecret!, refresh_token: refreshToken, grant_type: 'refresh_token', scope: REQUIRED_SCOPES }); try { logger.debug('[MICROSOFT_OAUTH] Refreshing access token', { url: MICROSOFT_TOKEN_URL }); const response = await axios.post(MICROSOFT_TOKEN_URL, params.toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); logger.debug('[MICROSOFT_OAUTH] Token refresh successful'); return { access_token: response.data.access_token, refresh_token: response.data.refresh_token, expires_in: response.data.expires_in }; } catch (error: any) { logger.error('[MICROSOFT_OAUTH] Error refreshing token', { error: error instanceof Error ? error.message : String(error) }); // Enhanced error logging if (error.response) { logger.error('[MICROSOFT_OAUTH] Response details', { data: error.response.data, status: error.response.status, headers: error.response.headers }); // Extract the error message from Microsoft's response format const errorData = error.response.data; if (errorData && errorData.error_description) { throw new Error(`Token refresh failed: ${errorData.error_description}`); } } throw new Error('Failed to refresh access token'); } } /** * Create special XOAUTH2 string for IMAP authentication */ export function createXOAuth2Token(email: string, accessToken: string): string { // This creates the XOAUTH2 token in the required format for ImapFlow // Format: user=\x01auth=Bearer \x01\x01 const auth = `user=${email}\x01auth=Bearer ${accessToken}\x01\x01`; const base64Auth = Buffer.from(auth).toString('base64'); logger.debug('[MICROSOFT_OAUTH] Generated XOAUTH2 token', { length: base64Auth.length }); return base64Auth; }