UNPKG

msexchange-mcp

Version:

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

188 lines • 8.25 kB
import { PublicClientApplication } from '@azure/msal-node'; import { Client } from '@microsoft/microsoft-graph-client'; import * as fs from 'fs'; import * as path from 'path'; import * as dotenv from 'dotenv'; import { KeyVaultTokenCache } from './keyVaultTokenCache.js'; // Load Key Vault configuration dotenv.config({ path: '.env.keyvault' }); export class AuthService { msalClients = new Map(); config; keyVaultUri; constructor(config) { this.config = config; // Get Key Vault URI from config or environment this.keyVaultUri = config.keyVault?.vaultUri || process.env.AZURE_KEY_VAULT_URI; if (this.keyVaultUri && config.keyVault?.enabled !== false) { console.error(`šŸ” Using Azure Key Vault for token persistence: ${this.keyVaultUri}`); } else { console.error(`šŸ“ Key Vault disabled, tokens will not persist across restarts`); } } async getMsalClient(userEmail) { // Check if we already have a client for this user if (this.msalClients.has(userEmail)) { return this.msalClients.get(userEmail); } const msalConfig = { auth: { clientId: this.config.clientId, authority: `https://login.microsoftonline.com/${this.config.tenantId}`, }, system: { loggerOptions: { loggerCallback: (level, message, containsPii) => { if (containsPii) return; if (level <= 1) { console.error(`[MSAL ${level}] ${message}`); } }, piiLoggingEnabled: false, logLevel: 1, }, }, }; // Set up cache plugin if Key Vault is enabled if (this.keyVaultUri && this.config.keyVault?.enabled !== false) { msalConfig.cache = { cachePlugin: new KeyVaultTokenCache(this.keyVaultUri, 'mcp-tokens', userEmail) }; } const client = new PublicClientApplication(msalConfig); this.msalClients.set(userEmail, client); return client; } async getTokenForUser(userEmail) { console.error(`šŸ” Getting token for ${userEmail}`); const msalClient = await this.getMsalClient(userEmail); // Try to get accounts from cache (will load from Key Vault if configured) const accounts = await msalClient.getAllAccounts(); const userAccount = accounts.find(account => account.username?.toLowerCase() === userEmail.toLowerCase()); if (userAccount) { try { console.error(`šŸ”„ Attempting silent token acquisition for ${userEmail}...`); const result = await msalClient.acquireTokenSilent({ scopes: ['https://graph.microsoft.com/.default'], account: userAccount, forceRefresh: false // Use cached token if valid }); if (result) { console.error(`āœ… Got token silently (from cache/refresh)!`); return result.accessToken; } } catch (error) { console.error(`āŒ Silent acquisition failed: ${error.message}`); console.error(`šŸ” Will use device code flow...`); } } else { console.error(`šŸ‘¤ No cached account found for ${userEmail}`); } // Use device code flow for interactive authentication const deviceCodeRequest = { scopes: ['https://graph.microsoft.com/.default'], 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 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 } if (!result) { throw new Error('Failed to acquire token via device code flow'); } console.error(`šŸŽ‰ Authentication completed successfully for ${userEmail}`); console.error(`šŸ” Token cached in Key Vault for future use`); return result.accessToken; } catch (error) { console.error(`āŒ Device code auth failed:`, error); throw error; } } // Force token refresh by clearing cache and getting new token async forceTokenRefresh(userEmail) { console.error(`šŸ”„ Forcing token refresh for ${userEmail}`); const msalClient = await this.getMsalClient(userEmail); // Clear the specific account from cache const accounts = await msalClient.getAllAccounts(); const userAccount = accounts.find(account => account.username?.toLowerCase() === userEmail.toLowerCase()); if (userAccount) { await msalClient.getTokenCache().removeAccount(userAccount); } // Get fresh token return await this.getTokenForUser(userEmail); } // Clear token cache for a specific user async clearTokenCache(userEmail) { console.error(`šŸ—‘ļø Clearing token cache for ${userEmail}`); const msalClient = await this.getMsalClient(userEmail); const accounts = await msalClient.getAllAccounts(); for (const account of accounts) { if (account.username?.toLowerCase() === userEmail.toLowerCase()) { await msalClient.getTokenCache().removeAccount(account); } } // Also clear from Key Vault if configured if (this.keyVaultUri && this.config.keyVault?.enabled !== false) { const cache = new KeyVaultTokenCache(this.keyVaultUri, 'mcp-tokens', userEmail); await cache.clearCache(); } } // Clear all cached tokens async clearAllTokens() { console.error(`šŸ—‘ļø Clearing all cached tokens`); for (const [email, client] of this.msalClients) { const accounts = await client.getAllAccounts(); for (const account of accounts) { await client.getTokenCache().removeAccount(account); } // Clear from Key Vault if (this.keyVaultUri && this.config.keyVault?.enabled !== false) { const cache = new KeyVaultTokenCache(this.keyVaultUri, 'mcp-tokens', email); await cache.clearCache(); } } // Clear the clients map this.msalClients.clear(); } getGraphClient(accessToken) { return Client.init({ authProvider: (done) => { done(null, accessToken); }, }); } } //# sourceMappingURL=authService-keyvault.js.map