msexchange-mcp
Version:
MCP server for Microsoft Exchange/Outlook email operations via Microsoft Graph API
188 lines ⢠8.25 kB
JavaScript
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