ms365-mcp-server
Version:
Microsoft 365 MCP Server for managing Microsoft 365 email through natural language interactions with full OAuth2 authentication support
871 lines (870 loc) ⢠35.8 kB
JavaScript
import { ConfidentialClientApplication, PublicClientApplication } from '@azure/msal-node';
import { Client } from '@microsoft/microsoft-graph-client';
import * as fs from 'fs';
import * as path from 'path';
import open from 'open';
import { createServer } from 'http';
import { URL } from 'url';
import { logger } from './api.js';
import { credentialStore } from './credential-store.js';
import { getConfigDirWithFallback } from './config-dir.js';
// Scopes required for Microsoft 365 operations
const SCOPES = [
'https://graph.microsoft.com/Mail.ReadWrite',
'https://graph.microsoft.com/Mail.Send',
'https://graph.microsoft.com/MailboxSettings.Read',
'https://graph.microsoft.com/Contacts.Read',
'https://graph.microsoft.com/User.Read',
'offline_access'
];
// Built-in application for easier setup (similar to Softeria's approach)
const BUILTIN_CLIENT_ID = "14d82eec-204b-4c2f-b7e8-296a70dab67e"; // Microsoft Graph Command Line Tools
const DEFAULT_TENANT_ID = "common";
// Configuration directory and file paths
const CONFIG_DIR = getConfigDirWithFallback();
const CREDENTIALS_FILE = path.join(CONFIG_DIR, 'credentials.json');
const DEVICE_CODE_FILE = path.join(CONFIG_DIR, 'device-code.json');
const TOKEN_CACHE_FILE = path.join(CONFIG_DIR, 'msal-cache.json');
/**
* Enhanced Microsoft 365 authentication manager with device code flow support
*/
export class EnhancedMS365Auth {
constructor(authMethod = 'auto') {
this.msalClient = null;
this.credentials = null;
this.preferredAuthMethod = 'auto';
this.pendingAuth = null;
this.preferredAuthMethod = authMethod;
this.ensureConfigDir();
}
/**
* Ensure configuration directory exists
*/
ensureConfigDir() {
if (!fs.existsSync(CONFIG_DIR)) {
fs.mkdirSync(CONFIG_DIR, { recursive: true });
logger.log('Created MS365 MCP configuration directory');
}
}
/**
* Load credentials from environment, file, or use built-in app
*/
async loadCredentials() {
try {
// Method 1: Environment variables (highest priority)
if (process.env.MS365_CLIENT_ID && process.env.MS365_TENANT_ID) {
this.credentials = {
clientId: process.env.MS365_CLIENT_ID,
clientSecret: process.env.MS365_CLIENT_SECRET,
tenantId: process.env.MS365_TENANT_ID,
redirectUri: process.env.MS365_REDIRECT_URI || 'http://localhost:44001/oauth2callback',
authType: process.env.MS365_CLIENT_SECRET ? 'redirect' : 'device'
};
logger.log('Loaded MS365 credentials from environment variables');
return true;
}
// Method 2: Credentials file
if (fs.existsSync(CREDENTIALS_FILE)) {
const credentialsData = fs.readFileSync(CREDENTIALS_FILE, 'utf8');
this.credentials = JSON.parse(credentialsData);
logger.log('Loaded MS365 credentials from file');
return true;
}
// Method 3: Built-in application (fallback)
this.credentials = {
clientId: BUILTIN_CLIENT_ID,
tenantId: DEFAULT_TENANT_ID,
authType: 'device'
};
logger.log('Using built-in MS365 application with device code flow');
return true;
}
catch (error) {
logger.error('Error loading MS365 credentials:', error);
return false;
}
}
/**
* Save credentials to file
*/
async saveCredentials(credentials) {
try {
fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2));
logger.log('Saved MS365 credentials to file');
}
catch (error) {
logger.error('Error saving MS365 credentials:', error);
throw new Error('Failed to save credentials');
}
}
/**
* Initialize MSAL client based on auth type with persistent token cache
*/
initializeMsalClient() {
if (!this.credentials) {
throw new Error('Credentials not loaded');
}
// Return existing client if already initialized with same credentials
if (this.msalClient) {
return this.msalClient;
}
const isConfidential = this.credentials.clientSecret && this.credentials.authType === 'redirect';
// Create persistent token cache
const cachePlugin = {
beforeCacheAccess: async (cacheContext) => {
try {
if (fs.existsSync(TOKEN_CACHE_FILE)) {
const cacheData = fs.readFileSync(TOKEN_CACHE_FILE, 'utf8');
cacheContext.tokenCache.deserialize(cacheData);
}
}
catch (error) {
logger.error('Error loading MSAL token cache:', error);
}
},
afterCacheAccess: async (cacheContext) => {
try {
if (cacheContext.cacheHasChanged) {
const cacheData = cacheContext.tokenCache.serialize();
fs.writeFileSync(TOKEN_CACHE_FILE, cacheData);
}
}
catch (error) {
logger.error('Error saving MSAL token cache:', error);
}
}
};
if (isConfidential) {
// Confidential client for redirect-based auth
const config = {
auth: {
clientId: this.credentials.clientId,
clientSecret: this.credentials.clientSecret,
authority: `https://login.microsoftonline.com/${this.credentials.tenantId}`
},
cache: {
cachePlugin
},
system: {
loggerOptions: {
loggerCallback: (level, message, containsPii) => {
if (!containsPii) {
logger.log(`MSAL: ${message}`);
}
},
piiLoggingEnabled: false,
logLevel: 3
}
}
};
this.msalClient = new ConfidentialClientApplication(config);
}
else {
// Public client for device code flow
const config = {
auth: {
clientId: this.credentials.clientId,
authority: `https://login.microsoftonline.com/${this.credentials.tenantId}`
},
cache: {
cachePlugin
},
system: {
loggerOptions: {
loggerCallback: (level, message, containsPii) => {
if (!containsPii) {
logger.log(`MSAL: ${message}`);
}
},
piiLoggingEnabled: false,
logLevel: 3
}
}
};
this.msalClient = new PublicClientApplication(config);
}
logger.log('Initialized MSAL client with persistent token cache');
return this.msalClient;
}
/**
* Device code flow authentication
*/
async authenticateWithDeviceCode() {
if (!await this.loadCredentials()) {
throw new Error('MS365 credentials not configured');
}
const msalClient = this.initializeMsalClient();
const deviceCodeRequest = {
scopes: SCOPES,
deviceCodeCallback: (response) => {
// Display the device code to the user on stderr to avoid JSON-RPC conflicts
console.error('\nš Microsoft 365 Authentication Required');
console.error('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
console.error(`š± Please visit: ${response.verificationUri}`);
console.error(`š Enter this code: ${response.userCode}`);
console.error('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
console.error('ā³ Waiting for authentication...\n');
logger.log(`Device code authentication: ${response.verificationUri} - ${response.userCode}`);
}
};
try {
const tokenResponse = await msalClient.acquireTokenByDeviceCode(deviceCodeRequest);
if (!tokenResponse) {
throw new Error('Failed to acquire token via device code');
}
await this.saveToken(tokenResponse, 'device');
logger.log('MS365 device code authentication successful');
console.error('ā
Authentication successful!\n');
return tokenResponse;
}
catch (error) {
logger.error('Device code authentication failed:', error);
throw error;
}
}
/**
* Redirect-based authentication (original method)
*/
async authenticateWithRedirect() {
if (!await this.loadCredentials()) {
throw new Error('MS365 credentials not configured');
}
if (!this.credentials?.clientSecret) {
throw new Error('Client secret required for redirect authentication');
}
const msalClient = this.initializeMsalClient();
try {
const authUrl = await msalClient.getAuthCodeUrl({
scopes: SCOPES,
redirectUri: this.credentials.redirectUri,
prompt: 'consent'
});
logger.log('Opening browser for authentication...');
const [authCode] = await Promise.all([
this.startCallbackServer(),
open(authUrl)
]);
const tokenResponse = await msalClient.acquireTokenByCode({
code: authCode,
scopes: SCOPES,
redirectUri: this.credentials.redirectUri
});
if (!tokenResponse) {
throw new Error('Failed to acquire token');
}
await this.saveToken(tokenResponse, 'redirect');
logger.log('MS365 redirect authentication successful');
return tokenResponse;
}
catch (error) {
logger.error('Redirect authentication failed:', error);
throw error;
}
}
/**
* Smart authentication that chooses the best method
*/
async authenticate() {
if (!await this.loadCredentials()) {
throw new Error('MS365 credentials not configured');
}
const authType = this.determineAuthType();
if (authType === 'device') {
return await this.authenticateWithDeviceCode();
}
else {
return await this.authenticateWithRedirect();
}
}
/**
* Determine the best authentication type
*/
determineAuthType() {
if (this.preferredAuthMethod === 'device') {
return 'device';
}
if (this.preferredAuthMethod === 'redirect') {
if (!this.credentials?.clientSecret) {
logger.log('No client secret available, falling back to device code flow');
return 'device';
}
return 'redirect';
}
// Auto mode: prefer device code for simplicity, redirect if client secret is available
if (this.credentials?.clientSecret && this.credentials?.redirectUri) {
return 'redirect';
}
return 'device';
}
/**
* Save token using secure credential store (simplified single account)
*/
async saveToken(token, authType) {
try {
const tokenData = {
accessToken: token.accessToken,
refreshToken: '', // MSAL manages refresh tokens internally
expiresOn: token.expiresOn?.getTime() || 0,
account: token.account,
authType: authType
};
// Always use a single account key for simplicity
await credentialStore.setCredentials('ms365-user', tokenData);
logger.log(`Saved MS365 access token securely (expires: ${new Date(tokenData.expiresOn).toLocaleString()})`);
}
catch (error) {
logger.error('Error saving token:', error);
}
}
/**
* Load stored token using secure credential store (simplified single account)
*/
async loadStoredToken() {
try {
return await credentialStore.getCredentials('ms365-user');
}
catch (error) {
logger.error('Error loading stored token:', error);
return null;
}
}
/**
* Start local server for OAuth2 callback (redirect auth)
*/
startCallbackServer() {
return new Promise((resolve, reject) => {
const server = createServer((req, res) => {
if (req.url?.startsWith('/oauth2callback')) {
const url = new URL(req.url, 'http://localhost:44001');
const code = url.searchParams.get('code');
const error = url.searchParams.get('error');
if (error) {
res.end(`<html><body><h1>Authentication Error</h1><p>${error}</p></body></html>`);
server.close();
reject(new Error(`OAuth2 error: ${error}`));
return;
}
if (code) {
res.end(`<html><body><h1>Authentication Successful!</h1><p>You can close this window and return to the terminal.</p></body></html>`);
server.close();
resolve(code);
return;
}
res.end('<html><body><h1>Invalid Request</h1></body></html>');
}
else {
res.end('<html><body><h1>MS365 MCP Server OAuth2</h1><p>Waiting for authentication...</p></body></html>');
}
});
server.listen(44001, () => {
logger.log('OAuth2 callback server started on port 44001');
});
server.on('error', (err) => {
reject(err);
});
});
}
/**
* Get authenticated Microsoft Graph client
*/
async getGraphClient() {
let storedToken = await this.loadStoredToken();
if (!storedToken) {
throw new Error('No stored token found. Please authenticate first.');
}
// Check if token is expired
if (storedToken.expiresOn < Date.now()) {
logger.log('Access token expired, refreshing...');
await this.refreshToken();
// Reload the token after refresh
storedToken = await this.loadStoredToken();
if (!storedToken) {
throw new Error('Failed to refresh token. Please re-authenticate.');
}
}
const client = Client.init({
authProvider: (done) => {
done(null, storedToken.accessToken);
}
});
return client;
}
/**
* Get token expiration information for proactive refresh
*/
async getTokenExpirationInfo() {
try {
const storedToken = await this.loadStoredToken();
if (!storedToken) {
return { expiresInMinutes: 0, needsRefresh: true };
}
const now = Date.now();
const expiresInMs = storedToken.expiresOn - now;
const expiresInMinutes = Math.floor(expiresInMs / (1000 * 60));
return {
expiresInMinutes: Math.max(0, expiresInMinutes),
needsRefresh: expiresInMinutes < 5 // Refresh if expiring within 5 minutes
};
}
catch (error) {
logger.error('Error getting token expiration info:', error);
return { expiresInMinutes: 0, needsRefresh: true };
}
}
/**
* Refresh token if needed (proactive refresh)
*/
async refreshTokenIfNeeded() {
try {
const tokenInfo = await this.getTokenExpirationInfo();
if (tokenInfo.needsRefresh) {
logger.log('Proactively refreshing token to prevent interruption...');
await this.refreshToken();
return true;
}
return false;
}
catch (error) {
logger.error('Proactive token refresh failed:', error);
return false;
}
}
/**
* Enhanced refresh token with better error handling
*/
async refreshToken() {
const storedToken = await this.loadStoredToken();
if (!storedToken?.account) {
throw new Error('No account information available. Please re-authenticate using: authenticate_with_device_code');
}
if (!await this.loadCredentials()) {
throw new Error('MS365 credentials not configured');
}
const msalClient = this.initializeMsalClient();
try {
// Try to get all accounts from MSAL cache first (only available on PublicClientApplication)
let accountToUse = storedToken.account;
if (msalClient instanceof PublicClientApplication) {
const accounts = await msalClient.getAllAccounts();
// If we have accounts in MSAL cache, use the first one that matches
if (accounts.length > 0) {
const matchingAccount = accounts.find((acc) => acc.username === storedToken.account?.username ||
acc.homeAccountId === storedToken.account?.homeAccountId);
if (matchingAccount) {
accountToUse = matchingAccount;
logger.log('Using account from MSAL cache for token refresh');
}
}
}
const tokenResponse = await msalClient.acquireTokenSilent({
scopes: SCOPES,
account: accountToUse
});
if (!tokenResponse) {
throw new Error('Failed to refresh token - please re-authenticate using: authenticate_with_device_code');
}
await this.saveToken(tokenResponse, storedToken.authType);
logger.log('MS365 token refreshed successfully');
}
catch (error) {
// Enhanced error handling with user-friendly messages
if (error.errorCode === 'invalid_grant' || error.errorCode === 'interaction_required') {
throw new Error('Authentication has expired. Please re-authenticate using the "authenticate_with_device_code" tool.');
}
else if (error.errorCode === 'consent_required') {
throw new Error('Additional consent required. Please re-authenticate using the "authenticate_with_device_code" tool.');
}
else if (error.errorCode === 'no_account_in_silent_request') {
throw new Error('No account found in token cache. Please re-authenticate using the "authenticate_with_device_code" tool.');
}
else {
logger.error('Token refresh failed:', error);
throw new Error(`Token refresh failed: ${error.message}. Please re-authenticate using the "authenticate_with_device_code" tool.`);
}
}
}
/**
* Check if user is authenticated
*/
async isAuthenticated() {
const storedToken = await this.loadStoredToken();
if (!storedToken) {
return false;
}
// If token is expired, try to refresh
if (storedToken.expiresOn < Date.now()) {
try {
await this.refreshToken();
return true;
}
catch (error) {
logger.error('Token refresh failed during authentication check:', error);
return false;
}
}
return true;
}
/**
* Check if credentials are configured
*/
async isConfigured() {
return await this.loadCredentials();
}
/**
* Clear stored authentication data
*/
async resetAuth() {
try {
await credentialStore.deleteCredentials('ms365-user');
await this.clearDeviceCodeState();
// Clear MSAL token cache
if (fs.existsSync(TOKEN_CACHE_FILE)) {
fs.unlinkSync(TOKEN_CACHE_FILE);
logger.log('Cleared MSAL token cache');
}
// Reset MSAL client instance to force re-initialization
this.msalClient = null;
logger.log('Cleared stored authentication tokens');
}
catch (error) {
logger.error('Error clearing authentication data:', error);
}
}
/**
* Save device code state to file
*/
async saveDeviceCodeState(state) {
try {
fs.writeFileSync(DEVICE_CODE_FILE, JSON.stringify(state, null, 2));
logger.log('Saved device code state to file');
}
catch (error) {
logger.error('Error saving device code state:', error);
throw new Error('Failed to save device code state');
}
}
/**
* Load device code state from file
*/
async loadDeviceCodeState() {
try {
if (!fs.existsSync(DEVICE_CODE_FILE)) {
return null;
}
const stateData = fs.readFileSync(DEVICE_CODE_FILE, 'utf8');
const state = JSON.parse(stateData);
// Check if device code has expired
const now = Date.now();
const elapsed = (now - state.startTime) / 1000;
if (elapsed > state.expiresIn) {
// Device code has expired, clean up
await this.clearDeviceCodeState();
return null;
}
return state;
}
catch (error) {
logger.error('Error loading device code state:', error);
return null;
}
}
/**
* Clear device code state file
*/
async clearDeviceCodeState() {
try {
if (fs.existsSync(DEVICE_CODE_FILE)) {
fs.unlinkSync(DEVICE_CODE_FILE);
logger.log('Cleared device code state file');
}
}
catch (error) {
logger.error('Error clearing device code state:', error);
}
}
/**
* Complete device code authentication using saved state
*/
async completeDeviceCodeAuth() {
const deviceCodeState = await this.loadDeviceCodeState();
if (!deviceCodeState) {
return false; // No pending device code authentication
}
if (!await this.loadCredentials()) {
throw new Error('MS365 credentials not configured');
}
try {
// Use the raw MSAL token endpoint to check if authentication completed
const tokenUrl = `https://login.microsoftonline.com/${this.credentials.tenantId}/oauth2/v2.0/token`;
const response = await fetch(tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
device_code: deviceCodeState.deviceCode,
client_id: this.credentials.clientId,
}),
});
const result = await response.json();
if (response.ok && result.access_token) {
// Authentication completed successfully
// Microsoft's token response doesn't include account info, so we need to get it from the token
let username = 'authenticated-user';
try {
// Try to decode the access token to get user info
const tokenParts = result.access_token.split('.');
if (tokenParts.length >= 2) {
const payload = JSON.parse(Buffer.from(tokenParts[1], 'base64').toString());
username = payload.upn || payload.unique_name || payload.preferred_username || 'authenticated-user';
}
}
catch (decodeError) {
logger.log('Could not decode token for username, using default');
}
const tokenResponse = {
accessToken: result.access_token,
refreshToken: result.refresh_token || '',
expiresOn: new Date(Date.now() + (result.expires_in * 1000)),
account: {
username: username,
homeAccountId: `${username}.${this.credentials.tenantId}`,
environment: 'login.microsoftonline.com',
tenantId: this.credentials.tenantId,
localAccountId: username
}
};
await this.saveToken(tokenResponse, 'device');
await this.clearDeviceCodeState();
logger.log('MS365 device code authentication completed successfully');
return true;
}
else if (result.error === 'authorization_pending') {
// Still waiting for user to complete authentication
return false;
}
else if (result.error === 'expired_token') {
// Device code has expired
await this.clearDeviceCodeState();
return false;
}
else {
// Other error - don't clear device code state, just return false
logger.error('Device code authentication error:', result);
return false;
}
}
catch (error) {
logger.error('Error completing device code authentication:', error);
// Don't clear device code state on network/other errors
return false;
}
}
/**
* Get pending device code info from saved state
*/
async getPendingDeviceCodeInfo() {
const deviceCodeState = await this.loadDeviceCodeState();
if (!deviceCodeState) {
return null;
}
return {
verificationUri: deviceCodeState.verificationUri,
userCode: deviceCodeState.userCode,
message: deviceCodeState.message
};
}
/**
* Get authentication URL for device code flow
*/
async getDeviceCodeInfo() {
if (!await this.loadCredentials()) {
throw new Error('MS365 credentials not configured');
}
const msalClient = this.initializeMsalClient();
return new Promise((resolve, reject) => {
const deviceCodeRequest = {
scopes: SCOPES,
deviceCodeCallback: (response) => {
resolve({
verificationUri: response.verificationUri,
userCode: response.userCode,
message: response.message
});
}
};
// This will trigger the callback immediately without completing auth
msalClient.acquireTokenByDeviceCode(deviceCodeRequest).catch(reject);
});
}
/**
* Start device code authentication and return device code info immediately
*/
async startDeviceCodeAuth() {
if (!await this.loadCredentials()) {
throw new Error('MS365 credentials not configured');
}
const msalClient = this.initializeMsalClient();
return new Promise((resolve, reject) => {
const deviceCodeRequest = {
scopes: SCOPES,
deviceCodeCallback: async (response) => {
const deviceCodeInfo = {
verificationUri: response.verificationUri,
userCode: response.userCode,
message: response.message
};
// Save device code state for later completion
const deviceCodeState = {
deviceCode: response.deviceCode,
userCode: response.userCode,
verificationUri: response.verificationUri,
expiresIn: response.expiresIn,
startTime: Date.now(),
message: response.message
};
try {
await this.saveDeviceCodeState(deviceCodeState);
logger.log(`Device code authentication started: ${response.verificationUri} - ${response.userCode}`);
// Return device code info immediately
resolve(deviceCodeInfo);
}
catch (error) {
reject(error);
}
}
};
// Start the device code flow - we need this to run to get the device code
// The callback will resolve our promise, and we'll handle the auth later
msalClient.acquireTokenByDeviceCode(deviceCodeRequest).then((result) => {
// If this completes immediately (unlikely but possible), save the token
if (result) {
this.saveToken(result, 'device').catch(() => {
// Ignore save errors here since we primarily care about getting the device code
});
}
}).catch((error) => {
// Only reject if we haven't already resolved with device code info
// The most common case is that the user hasn't completed auth yet
if (error.errorCode !== 'user_cancelled' && error.errorCode !== 'authorization_pending') {
logger.error('Device code flow error:', error);
}
});
});
}
/**
* Wait for pending device code authentication to complete
*/
async waitForDeviceCodeAuth() {
if (!this.pendingAuth) {
throw new Error('No pending device code authentication. Call startDeviceCodeAuth first.');
}
return await this.pendingAuth.authPromise;
}
/**
* Check if there's a pending device code authentication
*/
hasPendingAuth() {
return this.pendingAuth !== null;
}
/**
* Setup credentials interactively
*/
async setupCredentials() {
const readline = await import('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const question = (prompt) => {
return new Promise((resolve) => {
rl.question(prompt, resolve);
});
};
try {
console.log('\nš§ MS365 MCP Server Credential Setup\n');
console.log('Choose authentication method:');
console.log('1. Device Code Flow (Recommended - no app registration needed)');
console.log('2. Custom Azure App (Advanced - requires app registration)\n');
const choice = await question('Enter your choice (1 or 2): ');
if (choice === '1') {
// Use built-in app with device code flow
const credentials = {
clientId: BUILTIN_CLIENT_ID,
tenantId: DEFAULT_TENANT_ID,
authType: 'device'
};
await this.saveCredentials(credentials);
console.log('\nā
Configured for device code authentication!');
console.log('Run: ms365-mcp-server to start the server\n');
}
else if (choice === '2') {
console.log('\nCustom Azure App Setup:');
console.log('1. Go to https://portal.azure.com');
console.log('2. Navigate to Azure Active Directory > App registrations');
console.log('3. Click "New registration"');
console.log('4. Set redirect URI to: http://localhost:44001/oauth2callback');
console.log('5. Grant required API permissions for Microsoft Graph\n');
const clientId = await question('Enter your Client ID: ');
const clientSecret = await question('Enter your Client Secret (optional for device flow): ');
const tenantId = await question('Enter your Tenant ID (or "common" for multi-tenant): ');
const authType = clientSecret ? 'redirect' : 'device';
const credentials = {
clientId: clientId.trim(),
clientSecret: clientSecret.trim() || undefined,
tenantId: tenantId.trim(),
redirectUri: 'http://localhost:44001/oauth2callback',
authType: authType
};
await this.saveCredentials(credentials);
console.log('\nā
Credentials saved successfully!');
console.log('Run: ms365-mcp-server to start the server\n');
}
else {
console.log('Invalid choice. Setup cancelled.');
}
}
finally {
rl.close();
}
}
/**
* Get storage method information
*/
getStorageInfo() {
return {
method: credentialStore.getStorageMethod(),
location: credentialStore.getStorageLocation()
};
}
/**
* Get current authenticated user (secure - only your own info)
*/
async getCurrentUser() {
const storedToken = await this.loadStoredToken();
if (storedToken && storedToken.expiresOn > Date.now()) {
return storedToken.account?.username || 'authenticated-user';
}
return null;
}
/**
* Get authentication URL without opening browser (redirect flow)
*/
async getAuthUrl() {
if (!await this.loadCredentials()) {
throw new Error('MS365 credentials not configured');
}
if (!this.credentials?.clientSecret) {
throw new Error('Client secret required for redirect authentication. Use device code flow instead.');
}
const msalClient = this.initializeMsalClient();
const authUrl = await msalClient.getAuthCodeUrl({
scopes: SCOPES,
redirectUri: this.credentials.redirectUri,
prompt: 'consent'
});
return authUrl;
}
}
export const enhancedMS365Auth = new EnhancedMS365Auth();