UNPKG

msexchange-mcp

Version:

MCP server for Microsoft Exchange/Outlook email operations via Microsoft Graph API

267 lines • 12.3 kB
import { PublicClientApplication } from '@azure/msal-node'; import { Client } from '@microsoft/microsoft-graph-client'; import * as fs from 'fs'; import * as path from 'path'; export class AuthService { msalClient; tokenCache = new Map(); cacheFilePath; refreshTimer; config; constructor(config) { this.config = config; this.msalClient = new PublicClientApplication({ auth: { clientId: config.clientId, authority: `https://login.microsoftonline.com/${config.tenantId}`, }, system: { loggerOptions: { loggerCallback: (level, message, containsPii) => { if (containsPii) return; // Only log errors and warnings to reduce noise if (level <= 1) { console.error(`[MSAL ${level}] ${message}`); } }, piiLoggingEnabled: false, logLevel: 1, // Only errors and warnings }, }, }); // Set up persistent token cache this.cacheFilePath = path.join(process.cwd(), '.token-cache.json'); this.loadTokenCache(); } loadTokenCache() { try { if (fs.existsSync(this.cacheFilePath)) { const cacheData = JSON.parse(fs.readFileSync(this.cacheFilePath, 'utf8')); // Convert object entries back to Map with proper TokenCache structure for (const [key, value] of Object.entries(cacheData)) { if (value && typeof value === 'object' && 'accessToken' in value) { this.tokenCache.set(key, value); } } } } catch (error) { console.warn('Failed to load token cache:', error); // Clear corrupted cache if (fs.existsSync(this.cacheFilePath)) { fs.unlinkSync(this.cacheFilePath); } } } saveTokenCache() { try { const cacheObj = Object.fromEntries(this.tokenCache); fs.writeFileSync(this.cacheFilePath, JSON.stringify(cacheObj, null, 2)); console.error(`šŸ’¾ Token cache saved to ${this.cacheFilePath}`); console.error(`šŸ“Š Cached tokens for: ${Array.from(this.tokenCache.keys()).join(', ')}`); } catch (error) { console.error('āŒ Failed to save token cache:', error); } } async getTokenForUser(userEmail) { console.error(`šŸ” Checking token cache for ${userEmail}`); // Check cache first const cached = this.tokenCache.get(userEmail); if (cached && cached.expiresAt > Date.now()) { console.error(`āœ… Using cached token for ${userEmail}`); return cached.accessToken; } if (cached) { console.error(`ā° Cached token expired for ${userEmail}`); } else { console.error(`āŒ No cached token found for ${userEmail}`); } // Try to get accounts for silent token refresh (simplified approach) const accounts = await this.msalClient.getAllAccounts(); const userAccount = accounts.find(account => account.username === userEmail); if (userAccount) { try { const result = await this.msalClient.acquireTokenSilent({ scopes: ['https://graph.microsoft.com/.default', 'offline_access'], account: userAccount }); if (result) { this.cacheToken(userEmail, result.accessToken, result.expiresOn); return result.accessToken; } } catch (error) { console.error('Silent token acquisition failed, proceeding with device code flow'); } } // Use device code flow for interactive authentication const deviceCodeRequest = { scopes: ['https://graph.microsoft.com/.default', 'offline_access'], deviceCodeCallback: (response) => { const authInfo = { verificationUri: response.verificationUri || 'https://microsoft.com/devicelogin', userCode: response.userCode || 'Code not available', message: response.message || 'Please authenticate' }; // Write auth info to file for Claude to read try { fs.writeFileSync(path.join(process.cwd(), '.auth-pending.json'), JSON.stringify(authInfo, null, 2)); } catch (error) { console.error('Warning: Could not write auth file'); } console.error('\nšŸ” Azure AD Authentication Required'); console.error('='.repeat(50)); console.error(`Please visit: ${authInfo.verificationUri}`); console.error(`Enter code: ${authInfo.userCode}`); console.error('='.repeat(50)); console.error('Waiting for authentication...\n'); }, }; console.error(`šŸ”„ Starting device code authentication for ${userEmail}...`); try { const result = await this.msalClient.acquireTokenByDeviceCode(deviceCodeRequest); // Clean up auth pending file try { const authPendingFile = path.join(process.cwd(), '.auth-pending.json'); if (fs.existsSync(authPendingFile)) { fs.unlinkSync(authPendingFile); } } catch (error) { // Ignore cleanup errors } console.error(`šŸ“„ Device code result received:`, result ? 'SUCCESS' : 'FAILED'); if (!result) { throw new Error('Failed to acquire token via device code flow'); } console.error(`šŸŽÆ About to cache token for ${userEmail}`); console.error(`šŸ“Š Token details: expires at ${result.expiresOn}, has accessToken: ${!!result.accessToken}`); this.cacheToken(userEmail, result.accessToken, result.expiresOn); console.error(`šŸŽ‰ Authentication completed successfully for ${userEmail}`); return result.accessToken; } catch (error) { console.error(`āŒ Device code auth failed:`, error); if (error instanceof Error && error.message.includes('post_request_failed')) { console.error(`šŸ” This often indicates Azure AD app configuration issues`); console.error(` - Check if app is set as Public Client`); console.error(` - Verify redirect URI is configured`); console.error(` - Ensure all permissions are granted`); } throw error; } } cacheToken(userEmail, accessToken, expiresOn) { const expiresAt = expiresOn ? expiresOn.getTime() : Date.now() + 3600 * 1000; console.error(`šŸ’¾ Caching token for ${userEmail}, expires: ${new Date(expiresAt).toISOString()}`); this.tokenCache.set(userEmail, { accessToken, expiresAt, userEmail }); this.saveTokenCache(); console.error(`āœ… Token cached and saved to disk for ${userEmail}`); } // Force token refresh by clearing cache and getting new token async forceTokenRefresh(userEmail) { console.error(`šŸ”„ Forcing token refresh for ${userEmail}`); // Clear cached token this.tokenCache.delete(userEmail); // Get fresh token (will use silent refresh if available, otherwise device code) return await this.getTokenForUser(userEmail); } // Clear token cache for a specific user async clearTokenCache(userEmail) { console.error(`šŸ—‘ļø Clearing token cache for ${userEmail}`); this.tokenCache.delete(userEmail); this.saveTokenCache(); } // Clear all cached tokens async clearAllTokens() { console.error(`šŸ—‘ļø Clearing all cached tokens`); this.tokenCache.clear(); this.saveTokenCache(); } // Start proactive token refresh monitoring startProactiveRefresh(bufferMinutes = this.config.tokenRefresh.bufferMinutes, checkIntervalMinutes = this.config.tokenRefresh.checkIntervalMinutes) { if (!this.config.tokenRefresh.autoRefreshEnabled) { console.error('ā° Auto refresh disabled in configuration, skipping proactive refresh'); return; } if (this.refreshTimer) { console.error('ā° Proactive refresh already running'); return; } console.error(`ā° Starting proactive token refresh (buffer: ${bufferMinutes}min, check: ${checkIntervalMinutes}min)`); this.refreshTimer = setInterval(async () => { await this.refreshExpiringTokens(bufferMinutes); }, checkIntervalMinutes * 60 * 1000); } // Stop proactive token refresh monitoring stopProactiveRefresh() { if (this.refreshTimer) { clearInterval(this.refreshTimer); this.refreshTimer = undefined; console.error('ā¹ļø Stopped proactive token refresh'); } } // Check and refresh tokens that are about to expire async refreshExpiringTokens(bufferMinutes) { const bufferTime = bufferMinutes * 60 * 1000; // Convert to milliseconds const refreshThreshold = Date.now() + bufferTime; console.error(`šŸ” Checking ${this.tokenCache.size} cached tokens for expiration...`); for (const [userEmail, tokenData] of this.tokenCache) { try { if (tokenData.expiresAt <= refreshThreshold) { const expiresInMinutes = Math.round((tokenData.expiresAt - Date.now()) / (60 * 1000)); console.error(`ā° Token for ${userEmail} expires in ${expiresInMinutes} minutes, attempting refresh...`); // Try silent refresh first const accounts = await this.msalClient.getAllAccounts(); const userAccount = accounts.find(account => account.username === userEmail); if (userAccount) { try { const result = await this.msalClient.acquireTokenSilent({ scopes: ['https://graph.microsoft.com/.default', 'offline_access'], account: userAccount }); if (result) { this.cacheToken(userEmail, result.accessToken, result.expiresOn); console.error(`āœ… Successfully refreshed token for ${userEmail}`); } } catch (error) { console.error(`āŒ Failed to refresh token for ${userEmail}:`, error); // Token will remain in cache and user will be prompted to re-authenticate when needed } } else { console.error(`āŒ No account found for ${userEmail}, skipping refresh`); } } } catch (error) { console.error(`āŒ Error checking token for ${userEmail}:`, error); } } } // Check if any tokens need refresh soon getTokensNeedingRefresh(bufferMinutes = this.config.tokenRefresh.bufferMinutes) { const bufferTime = bufferMinutes * 60 * 1000; const refreshThreshold = Date.now() + bufferTime; return Array.from(this.tokenCache.entries()) .filter(([_, tokenData]) => tokenData.expiresAt <= refreshThreshold) .map(([userEmail]) => userEmail); } getGraphClient(accessToken) { return Client.init({ authProvider: (done) => { done(null, accessToken); }, }); } } //# sourceMappingURL=authService.js.map