UNPKG

teams-mcp-server

Version:

Microsoft Teams MCP server with direct messaging support

238 lines (237 loc) 9.29 kB
import { PublicClientApplication, ConfidentialClientApplication } from '@azure/msal-node'; import express from 'express'; import open from 'open'; import { TokenCache } from './tokenCache.js'; import * as crypto from 'crypto'; import { debugLog } from '../utils/logger.js'; // Global to ensure only one auth server runs let globalAuthServer = null; let authServerPort = null; let globalAuthCodeResolver = null; let globalAuthCodeRejecter = null; export class AuthManager { msalClient; tokenCache; serverPort; redirectUri; clientSecret; scopes = [ 'Chat.ReadWrite', 'ChatMessage.Send', 'User.Read', 'offline_access' ]; constructor(clientId, tenantId, redirectUri, port = 3000, clientSecret) { const msalConfig = { auth: { clientId, authority: `https://login.microsoftonline.com/${tenantId}`, clientSecret: clientSecret }, system: { loggerOptions: { loggerCallback: (_level, message, containsPii) => { if (process.env.DEBUG === 'true' && !containsPii) { debugLog(message); } }, piiLoggingEnabled: false, logLevel: 3 } } }; this.msalClient = clientSecret ? new ConfidentialClientApplication(msalConfig) : new PublicClientApplication(msalConfig); this.tokenCache = new TokenCache(); this.serverPort = port; this.redirectUri = redirectUri; this.clientSecret = clientSecret; // Don't start auth server immediately - will start when needed } async getToken(forceRefresh = false) { if (!forceRefresh) { const cachedToken = await this.tokenCache.getToken(); if (cachedToken) { return cachedToken; } } try { const accounts = await this.msalClient.getTokenCache().getAllAccounts(); if (accounts.length > 0) { const silentRequest = { account: accounts[0], scopes: this.scopes, forceRefresh }; try { const result = await this.msalClient.acquireTokenSilent(silentRequest); return await this.saveAuthResult(result); } catch (error) { debugLog('Silent token acquisition failed, initiating interactive flow'); } } return await this.acquireTokenInteractive(); } catch (error) { debugLog(`Token acquisition failed: ${error}`); return null; } } async acquireTokenInteractive() { // Ensure auth server is running before starting interactive flow this.ensureAuthServer(); const state = crypto.randomBytes(16).toString('hex'); const codeVerifier = crypto.randomBytes(32).toString('base64url'); const codeChallenge = crypto .createHash('sha256') .update(codeVerifier) .digest('base64url'); const authUrl = await this.msalClient.getAuthCodeUrl({ scopes: this.scopes, redirectUri: this.redirectUri, state, prompt: 'select_account', codeChallenge: this.clientSecret ? undefined : codeChallenge, codeChallengeMethod: this.clientSecret ? undefined : 'S256' }); debugLog('Opening browser for authentication...'); await open(authUrl); const authCode = await this.waitForAuthCode(); const tokenRequest = { code: authCode, scopes: this.scopes, redirectUri: this.redirectUri, codeVerifier: this.clientSecret ? undefined : codeVerifier }; try { const result = await this.msalClient.acquireTokenByCode(tokenRequest); return await this.saveAuthResult(result); } catch (error) { debugLog(`Failed to exchange authorization code: ${error}`); // Check for specific Azure AD error about client configuration if (error.message?.includes('AADSTS7000218') || error.message?.includes('client_secret')) { console.error('\n❌ Azure App Configuration Error:'); console.error('Your Azure app requires a client secret but this is a public client application.'); console.error('\nTo fix this:'); console.error('1. Go to Azure Portal → Azure Active Directory → App registrations'); console.error('2. Find your app and go to Authentication section'); console.error('3. Under Advanced settings, enable "Allow public client flows"'); console.error('4. Save the changes and try again\n'); } return null; } } async waitForAuthCode() { return new Promise((resolve, reject) => { // Update global resolvers globalAuthCodeResolver = resolve; globalAuthCodeRejecter = reject; setTimeout(() => { globalAuthCodeResolver = null; globalAuthCodeRejecter = null; reject(new Error('Authentication timeout - no response received')); }, 300000); // 5 minute timeout }); } ensureAuthServer() { // Use global auth server to ensure only one instance if (!globalAuthServer || authServerPort !== this.serverPort) { if (globalAuthServer) { // Close existing server if port changed globalAuthServer.removeAllListeners(); } globalAuthServer = express(); authServerPort = this.serverPort; globalAuthServer.get('/auth/callback', (req, res) => { const code = req.query.code; const error = req.query.error; if (error) { res.send(` <html> <body> <h2>Authentication Failed</h2> <p>Error: ${error}</p> <p>You can close this window.</p> </body> </html> `); if (globalAuthCodeRejecter) { globalAuthCodeRejecter(new Error(error)); } return; } if (code) { res.send(` <html> <body> <h2>Authentication Successful!</h2> <p>You can close this window and return to your terminal.</p> </body> </html> `); if (globalAuthCodeResolver) { globalAuthCodeResolver(code); } } else { res.status(400).send('No authorization code received'); if (globalAuthCodeRejecter) { globalAuthCodeRejecter(new Error('No authorization code received')); } } }); try { const server = globalAuthServer.listen(this.serverPort); server.on('error', (error) => { if (error.code === 'EADDRINUSE') { debugLog(`Warning: Port ${this.serverPort} is already in use. Auth server not started.`); } else { debugLog(`Failed to start auth server: ${error}`); } }); server.on('listening', () => { debugLog(`Auth server started on port ${this.serverPort}`); }); } catch (error) { debugLog(`Failed to start auth server: ${error}`); // Don't throw the error, just log it. The server will only be needed during auth flow } } } async saveAuthResult(result) { const token = { accessToken: result.accessToken, expiresOn: result.expiresOn || new Date(Date.now() + 3600000), account: result.account ? { username: result.account.username, tenantId: result.account.tenantId, localAccountId: result.account.localAccountId } : undefined }; try { await this.tokenCache.saveToken(token); debugLog('Token saved successfully'); } catch (error) { debugLog(`Failed to save token: ${error}`); throw error; } return token; } async clearToken() { await this.tokenCache.clearToken(); const accounts = await this.msalClient.getTokenCache().getAllAccounts(); for (const account of accounts) { await this.msalClient.getTokenCache().removeAccount(account); } } async isAuthenticated() { const token = await this.tokenCache.getToken(); return token !== null; } }