@softeria/ms-365-mcp-server
Version:
Microsoft 365 MCP Server
221 lines (192 loc) • 6.42 kB
JavaScript
import { PublicClientApplication } from '@azure/msal-node';
import keytar from 'keytar';
import { fileURLToPath } from 'url';
import path from 'path';
import fs from 'fs';
import logger from './logger.mjs';
const SERVICE_NAME = 'ms-365-mcp-server';
const TOKEN_CACHE_ACCOUNT = 'msal-token-cache';
const FALLBACK_DIR = path.dirname(fileURLToPath(import.meta.url));
const FALLBACK_PATH = path.join(FALLBACK_DIR, '..', '.token-cache.json');
const DEFAULT_CONFIG = {
auth: {
clientId: '084a3e9f-a9f4-43f7-89f9-d229cf97853e',
authority: 'https://login.microsoftonline.com/common',
},
};
const DEFAULT_SCOPES = [
'Files.ReadWrite',
'Files.ReadWrite.All',
'Sites.ReadWrite.All',
'User.Read',
'User.ReadBasic.All',
'Calendars.Read',
'Calendars.ReadWrite',
'Mail.Read',
'Mail.ReadWrite',
];
class AuthManager {
constructor(config = DEFAULT_CONFIG, scopes = DEFAULT_SCOPES) {
this.config = config;
this.scopes = scopes;
this.msalApp = new PublicClientApplication(this.config);
this.accessToken = null;
this.tokenExpiry = null;
}
async loadTokenCache() {
try {
let cacheData;
try {
const cachedData = await keytar.getPassword(SERVICE_NAME, TOKEN_CACHE_ACCOUNT);
if (cachedData) {
cacheData = cachedData;
}
} catch (keytarError) {
logger.warn(`Keychain access failed, falling back to file storage: ${keytarError.message}`);
}
if (!cacheData && fs.existsSync(FALLBACK_PATH)) {
cacheData = fs.readFileSync(FALLBACK_PATH, 'utf8');
}
if (cacheData) {
this.msalApp.getTokenCache().deserialize(cacheData);
}
} catch (error) {
logger.error(`Error loading token cache: ${error.message}`);
}
}
async saveTokenCache() {
try {
const cacheData = this.msalApp.getTokenCache().serialize();
try {
await keytar.setPassword(SERVICE_NAME, TOKEN_CACHE_ACCOUNT, cacheData);
} catch (keytarError) {
logger.warn(`Keychain save failed, falling back to file storage: ${keytarError.message}`);
fs.writeFileSync(FALLBACK_PATH, cacheData);
}
} catch (error) {
logger.error(`Error saving token cache: ${error.message}`);
}
}
async getToken(forceRefresh = false) {
if (this.accessToken && this.tokenExpiry && this.tokenExpiry > Date.now() && !forceRefresh) {
return this.accessToken;
}
const accounts = await this.msalApp.getTokenCache().getAllAccounts();
if (accounts.length > 0) {
const silentRequest = {
account: accounts[0],
scopes: this.scopes,
};
try {
const response = await this.msalApp.acquireTokenSilent(silentRequest);
this.accessToken = response.accessToken;
this.tokenExpiry = new Date(response.expiresOn).getTime();
return this.accessToken;
} catch (error) {
logger.info('Silent token acquisition failed, using device code flow');
}
}
throw new Error('No valid token found');
}
async acquireTokenByDeviceCode(hack) {
const deviceCodeRequest = {
scopes: this.scopes,
deviceCodeCallback: (response) => {
const text = ['\n', response.message, '\n'].join('');
if (hack) {
hack(text + 'After login run the "test login" command');
} else {
console.log(text);
}
logger.info('Device code login initiated');
},
};
try {
logger.info('Requesting device code...');
const response = await this.msalApp.acquireTokenByDeviceCode(deviceCodeRequest);
logger.info('Device code login successful');
this.accessToken = response.accessToken;
this.tokenExpiry = new Date(response.expiresOn).getTime();
await this.saveTokenCache();
return this.accessToken;
} catch (error) {
logger.error(`Error in device code flow: ${error.message}`);
throw error;
}
}
async testLogin() {
try {
logger.info('Testing login...');
const token = await this.getToken();
if (!token) {
logger.error('Login test failed - no token received');
return {
success: false,
message: 'Login failed - no token received',
};
}
logger.info('Token retrieved successfully, testing Graph API access...');
try {
const response = await fetch('https://graph.microsoft.com/v1.0/me', {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (response.ok) {
const userData = await response.json();
logger.info('Graph API user data fetch successful');
return {
success: true,
message: 'Login successful',
userData: {
displayName: userData.displayName,
userPrincipalName: userData.userPrincipalName,
},
};
} else {
const errorText = await response.text();
logger.error(`Graph API user data fetch failed: ${response.status} - ${errorText}`);
return {
success: false,
message: `Login successful but Graph API access failed: ${response.status}`,
};
}
} catch (graphError) {
logger.error(`Error fetching user data: ${graphError.message}`);
return {
success: false,
message: `Login successful but Graph API access failed: ${graphError.message}`,
};
}
} catch (error) {
logger.error(`Login test failed: ${error.message}`);
return {
success: false,
message: `Login failed: ${error.message}`,
};
}
}
async logout() {
try {
const accounts = await this.msalApp.getTokenCache().getAllAccounts();
for (const account of accounts) {
await this.msalApp.getTokenCache().removeAccount(account);
}
this.accessToken = null;
this.tokenExpiry = null;
try {
await keytar.deletePassword(SERVICE_NAME, TOKEN_CACHE_ACCOUNT);
} catch (keytarError) {
logger.warn(`Keychain deletion failed: ${keytarError.message}`);
}
if (fs.existsSync(FALLBACK_PATH)) {
fs.unlinkSync(FALLBACK_PATH);
}
return true;
} catch (error) {
logger.error(`Error during logout: ${error.message}`);
throw error;
}
}
}
export default AuthManager;