teams-mcp-server
Version:
Microsoft Teams MCP server with direct messaging support
238 lines (237 loc) • 9.29 kB
JavaScript
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;
}
}