UNPKG

teams-mcp-server

Version:

Microsoft Teams MCP server with direct messaging support

195 lines (194 loc) 9.17 kB
import { debugLog } from '../utils/logger.js'; import { getAccessToken } from '../utils/tokenHelper.js'; import { PublicClientApplication, ConfidentialClientApplication } from '@azure/msal-node'; import * as crypto from 'crypto'; // Store code verifiers for ongoing auth flows const authFlows = new Map(); export function createIsAuthenticatedTool() { return { name: 'is_authenticated', description: 'Check if the provided tokens are valid and refresh if needed. If no tokens provided, generate auth URL.', inputSchema: { type: 'object', properties: { tokens: { type: 'object', properties: { access_token: { type: ['string', 'null'] }, refresh_token: { type: ['string', 'null'] } }, description: 'OAuth tokens (optional). If not provided, will generate auth URL' }, callback_url: { type: 'string', description: 'Callback URL for OAuth flow (required if tokens not provided)' }, callback_state: { type: 'object', description: 'State data to include in OAuth flow (optional)' } }, required: [] }, handler: async (args) => { try { const CLIENT_ID = process.env.AZURE_CLIENT_ID; const TENANT_ID = process.env.AZURE_TENANT_ID; const CLIENT_SECRET = process.env.AZURE_CLIENT_SECRET; if (!CLIENT_ID || !TENANT_ID) { return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'Missing required environment variables: AZURE_CLIENT_ID, AZURE_TENANT_ID' }, null, 2) }] }; } const msalConfig = { auth: { clientId: CLIENT_ID, authority: `https://login.microsoftonline.com/${TENANT_ID}`, clientSecret: CLIENT_SECRET } }; const msalClient = CLIENT_SECRET ? new ConfidentialClientApplication(msalConfig) : new PublicClientApplication(msalConfig); const scopes = ['Chat.ReadWrite', 'ChatMessage.Send', 'User.Read', 'offline_access']; // If no tokens provided or tokens are empty/null, generate auth URL if (!args.tokens || args.tokens === null || (typeof args.tokens === 'object' && Object.keys(args.tokens).length === 0)) { debugLog('No tokens or empty tokens provided, generating auth URL'); if (!args.callback_url) { return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'callback_url is required when tokens are not provided' }, null, 2) }] }; } // Use callback_state if provided, otherwise generate random state const stateData = args.callback_state || {}; const stateId = crypto.randomBytes(16).toString('hex'); const state = JSON.stringify({ id: stateId, ...stateData }); const codeVerifier = crypto.randomBytes(32).toString('base64url'); const codeChallenge = crypto .createHash('sha256') .update(codeVerifier) .digest('base64url'); // Store code verifier associated with state ID authFlows.set(stateId, codeVerifier); // Clean up old flows after 10 minutes setTimeout(() => authFlows.delete(stateId), 10 * 60 * 1000); const authUrl = await msalClient.getAuthCodeUrl({ scopes, redirectUri: args.callback_url, state, prompt: 'select_account', codeChallenge: CLIENT_SECRET ? undefined : codeChallenge, codeChallengeMethod: CLIENT_SECRET ? undefined : 'S256' }); return { content: [{ type: 'text', text: JSON.stringify({ status: 'need_auth', auth_url: authUrl }, null, 2) }] }; } // Check if tokens are valid try { // For Teams/Azure AD, we primarily work with access tokens // The refresh token handling is done internally by MSAL const tokenData = args.tokens; const accessToken = getAccessToken(tokenData); if (!accessToken) { return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'No access token provided' }, null, 2) }] }; } // Parse the JWT to check expiration const tokenParts = accessToken.split('.'); if (tokenParts.length !== 3) { return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'Invalid token format' }, null, 2) }] }; } const payload = JSON.parse(Buffer.from(tokenParts[1], 'base64').toString()); const expiresAt = payload.exp * 1000; // Convert to milliseconds const now = Date.now(); // Check if token is expired or will expire in next 5 minutes if (expiresAt <= now + (5 * 60 * 1000)) { // Token is expired or expiring soon // With Azure AD, we can't refresh tokens the same way as Google // The client needs to re-authenticate return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'Token expired or expiring soon' }, null, 2) }] }; } // Token is valid return { content: [{ type: 'text', text: JSON.stringify({ success: true }, null, 2) }] }; } catch (error) { debugLog(`Token validation failed: ${error.message}`); return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: `Token validation failed: ${error.message}` }, null, 2) }] }; } } catch (error) { debugLog(`Authentication check failed: ${error}`); return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: error.message }, null, 2) }] }; } } }; } // Export the auth flows map so authorize tool can access it export { authFlows };