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