msexchange-mcp
Version:
MCP server for Microsoft Exchange/Outlook email operations via Microsoft Graph API
267 lines ⢠12.3 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';
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