ms365-mcp-server 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,639 @@
1
+ import { ConfidentialClientApplication, PublicClientApplication } from '@azure/msal-node';
2
+ import { Client } from '@microsoft/microsoft-graph-client';
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+ import * as os from 'os';
6
+ import open from 'open';
7
+ import { createServer } from 'http';
8
+ import { URL } from 'url';
9
+ import { logger } from './api.js';
10
+ import { credentialStore } from './credential-store.js';
11
+ // Scopes required for Microsoft 365 operations
12
+ const SCOPES = [
13
+ 'https://graph.microsoft.com/Mail.ReadWrite',
14
+ 'https://graph.microsoft.com/Mail.Send',
15
+ 'https://graph.microsoft.com/MailboxSettings.Read',
16
+ 'https://graph.microsoft.com/Contacts.Read',
17
+ 'https://graph.microsoft.com/User.Read',
18
+ 'offline_access'
19
+ ];
20
+ // Built-in application for easier setup (similar to Softeria's approach)
21
+ const BUILTIN_CLIENT_ID = "14d82eec-204b-4c2f-b7e8-296a70dab67e"; // Microsoft Graph Command Line Tools
22
+ const DEFAULT_TENANT_ID = "common";
23
+ // Configuration directory and file paths
24
+ const CONFIG_DIR = path.join(os.homedir(), '.ms365-mcp');
25
+ const CREDENTIALS_FILE = path.join(CONFIG_DIR, 'credentials.json');
26
+ /**
27
+ * Enhanced Microsoft 365 authentication manager with device code flow support
28
+ */
29
+ export class EnhancedMS365Auth {
30
+ constructor(authMethod = 'auto') {
31
+ this.msalClient = null;
32
+ this.credentials = null;
33
+ this.preferredAuthMethod = 'auto';
34
+ this.pendingAuth = null;
35
+ this.preferredAuthMethod = authMethod;
36
+ this.ensureConfigDir();
37
+ }
38
+ /**
39
+ * Ensure configuration directory exists
40
+ */
41
+ ensureConfigDir() {
42
+ if (!fs.existsSync(CONFIG_DIR)) {
43
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
44
+ logger.log('Created MS365 MCP configuration directory');
45
+ }
46
+ }
47
+ /**
48
+ * Load credentials from environment, file, or use built-in app
49
+ */
50
+ async loadCredentials() {
51
+ try {
52
+ // Method 1: Environment variables (highest priority)
53
+ if (process.env.MS365_CLIENT_ID && process.env.MS365_TENANT_ID) {
54
+ this.credentials = {
55
+ clientId: process.env.MS365_CLIENT_ID,
56
+ clientSecret: process.env.MS365_CLIENT_SECRET,
57
+ tenantId: process.env.MS365_TENANT_ID,
58
+ redirectUri: process.env.MS365_REDIRECT_URI || 'http://localhost:44001/oauth2callback',
59
+ authType: process.env.MS365_CLIENT_SECRET ? 'redirect' : 'device'
60
+ };
61
+ logger.log('Loaded MS365 credentials from environment variables');
62
+ return true;
63
+ }
64
+ // Method 2: Credentials file
65
+ if (fs.existsSync(CREDENTIALS_FILE)) {
66
+ const credentialsData = fs.readFileSync(CREDENTIALS_FILE, 'utf8');
67
+ this.credentials = JSON.parse(credentialsData);
68
+ logger.log('Loaded MS365 credentials from file');
69
+ return true;
70
+ }
71
+ // Method 3: Built-in application (fallback)
72
+ this.credentials = {
73
+ clientId: BUILTIN_CLIENT_ID,
74
+ tenantId: DEFAULT_TENANT_ID,
75
+ authType: 'device'
76
+ };
77
+ logger.log('Using built-in MS365 application with device code flow');
78
+ return true;
79
+ }
80
+ catch (error) {
81
+ logger.error('Error loading MS365 credentials:', error);
82
+ return false;
83
+ }
84
+ }
85
+ /**
86
+ * Save credentials to file
87
+ */
88
+ async saveCredentials(credentials) {
89
+ try {
90
+ fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2));
91
+ logger.log('Saved MS365 credentials to file');
92
+ }
93
+ catch (error) {
94
+ logger.error('Error saving MS365 credentials:', error);
95
+ throw new Error('Failed to save credentials');
96
+ }
97
+ }
98
+ /**
99
+ * Initialize MSAL client based on auth type
100
+ */
101
+ initializeMsalClient() {
102
+ if (!this.credentials) {
103
+ throw new Error('Credentials not loaded');
104
+ }
105
+ const isConfidential = this.credentials.clientSecret && this.credentials.authType === 'redirect';
106
+ if (isConfidential) {
107
+ // Confidential client for redirect-based auth
108
+ const config = {
109
+ auth: {
110
+ clientId: this.credentials.clientId,
111
+ clientSecret: this.credentials.clientSecret,
112
+ authority: `https://login.microsoftonline.com/${this.credentials.tenantId}`
113
+ },
114
+ system: {
115
+ loggerOptions: {
116
+ loggerCallback: (level, message, containsPii) => {
117
+ if (!containsPii) {
118
+ logger.log(`MSAL: ${message}`);
119
+ }
120
+ },
121
+ piiLoggingEnabled: false,
122
+ logLevel: 3
123
+ }
124
+ }
125
+ };
126
+ this.msalClient = new ConfidentialClientApplication(config);
127
+ }
128
+ else {
129
+ // Public client for device code flow
130
+ const config = {
131
+ auth: {
132
+ clientId: this.credentials.clientId,
133
+ authority: `https://login.microsoftonline.com/${this.credentials.tenantId}`
134
+ },
135
+ system: {
136
+ loggerOptions: {
137
+ loggerCallback: (level, message, containsPii) => {
138
+ if (!containsPii) {
139
+ logger.log(`MSAL: ${message}`);
140
+ }
141
+ },
142
+ piiLoggingEnabled: false,
143
+ logLevel: 3
144
+ }
145
+ }
146
+ };
147
+ this.msalClient = new PublicClientApplication(config);
148
+ }
149
+ return this.msalClient;
150
+ }
151
+ /**
152
+ * Device code flow authentication
153
+ */
154
+ async authenticateWithDeviceCode() {
155
+ if (!await this.loadCredentials()) {
156
+ throw new Error('MS365 credentials not configured');
157
+ }
158
+ const msalClient = this.initializeMsalClient();
159
+ const deviceCodeRequest = {
160
+ scopes: SCOPES,
161
+ deviceCodeCallback: (response) => {
162
+ // Display the device code to the user on stderr to avoid JSON-RPC conflicts
163
+ console.error('\n🔐 Microsoft 365 Authentication Required');
164
+ console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
165
+ console.error(`📱 Please visit: ${response.verificationUri}`);
166
+ console.error(`🔑 Enter this code: ${response.userCode}`);
167
+ console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
168
+ console.error('⏳ Waiting for authentication...\n');
169
+ logger.log(`Device code authentication: ${response.verificationUri} - ${response.userCode}`);
170
+ }
171
+ };
172
+ try {
173
+ const tokenResponse = await msalClient.acquireTokenByDeviceCode(deviceCodeRequest);
174
+ if (!tokenResponse) {
175
+ throw new Error('Failed to acquire token via device code');
176
+ }
177
+ await this.saveToken(tokenResponse, 'device');
178
+ logger.log('MS365 device code authentication successful');
179
+ console.error('✅ Authentication successful!\n');
180
+ return tokenResponse;
181
+ }
182
+ catch (error) {
183
+ logger.error('Device code authentication failed:', error);
184
+ throw error;
185
+ }
186
+ }
187
+ /**
188
+ * Redirect-based authentication (original method)
189
+ */
190
+ async authenticateWithRedirect() {
191
+ if (!await this.loadCredentials()) {
192
+ throw new Error('MS365 credentials not configured');
193
+ }
194
+ if (!this.credentials?.clientSecret) {
195
+ throw new Error('Client secret required for redirect authentication');
196
+ }
197
+ const msalClient = this.initializeMsalClient();
198
+ try {
199
+ const authUrl = await msalClient.getAuthCodeUrl({
200
+ scopes: SCOPES,
201
+ redirectUri: this.credentials.redirectUri,
202
+ prompt: 'consent'
203
+ });
204
+ logger.log('Opening browser for authentication...');
205
+ const [authCode] = await Promise.all([
206
+ this.startCallbackServer(),
207
+ open(authUrl)
208
+ ]);
209
+ const tokenResponse = await msalClient.acquireTokenByCode({
210
+ code: authCode,
211
+ scopes: SCOPES,
212
+ redirectUri: this.credentials.redirectUri
213
+ });
214
+ if (!tokenResponse) {
215
+ throw new Error('Failed to acquire token');
216
+ }
217
+ await this.saveToken(tokenResponse, 'redirect');
218
+ logger.log('MS365 redirect authentication successful');
219
+ return tokenResponse;
220
+ }
221
+ catch (error) {
222
+ logger.error('Redirect authentication failed:', error);
223
+ throw error;
224
+ }
225
+ }
226
+ /**
227
+ * Smart authentication that chooses the best method
228
+ */
229
+ async authenticate() {
230
+ if (!await this.loadCredentials()) {
231
+ throw new Error('MS365 credentials not configured');
232
+ }
233
+ const authType = this.determineAuthType();
234
+ if (authType === 'device') {
235
+ return await this.authenticateWithDeviceCode();
236
+ }
237
+ else {
238
+ return await this.authenticateWithRedirect();
239
+ }
240
+ }
241
+ /**
242
+ * Determine the best authentication type
243
+ */
244
+ determineAuthType() {
245
+ if (this.preferredAuthMethod === 'device') {
246
+ return 'device';
247
+ }
248
+ if (this.preferredAuthMethod === 'redirect') {
249
+ if (!this.credentials?.clientSecret) {
250
+ logger.log('No client secret available, falling back to device code flow');
251
+ return 'device';
252
+ }
253
+ return 'redirect';
254
+ }
255
+ // Auto mode: prefer device code for simplicity, redirect if client secret is available
256
+ if (this.credentials?.clientSecret && this.credentials?.redirectUri) {
257
+ return 'redirect';
258
+ }
259
+ return 'device';
260
+ }
261
+ /**
262
+ * Save token using secure credential store
263
+ */
264
+ async saveToken(token, authType) {
265
+ try {
266
+ const tokenData = {
267
+ accessToken: token.accessToken,
268
+ refreshToken: '',
269
+ expiresOn: token.expiresOn?.getTime() || 0,
270
+ account: token.account,
271
+ authType: authType
272
+ };
273
+ const accountKey = token.account?.username || 'default-user';
274
+ await credentialStore.setCredentials(accountKey, tokenData);
275
+ logger.log('Saved MS365 access token securely');
276
+ }
277
+ catch (error) {
278
+ logger.error('Error saving token:', error);
279
+ }
280
+ }
281
+ /**
282
+ * Load stored token using secure credential store
283
+ */
284
+ async loadStoredToken(accountKey = 'default-user') {
285
+ try {
286
+ return await credentialStore.getCredentials(accountKey);
287
+ }
288
+ catch (error) {
289
+ logger.error('Error loading stored token:', error);
290
+ return null;
291
+ }
292
+ }
293
+ /**
294
+ * Start local server for OAuth2 callback (redirect auth)
295
+ */
296
+ startCallbackServer() {
297
+ return new Promise((resolve, reject) => {
298
+ const server = createServer((req, res) => {
299
+ if (req.url?.startsWith('/oauth2callback')) {
300
+ const url = new URL(req.url, 'http://localhost:44001');
301
+ const code = url.searchParams.get('code');
302
+ const error = url.searchParams.get('error');
303
+ if (error) {
304
+ res.end(`<html><body><h1>Authentication Error</h1><p>${error}</p></body></html>`);
305
+ server.close();
306
+ reject(new Error(`OAuth2 error: ${error}`));
307
+ return;
308
+ }
309
+ if (code) {
310
+ res.end(`<html><body><h1>Authentication Successful!</h1><p>You can close this window and return to the terminal.</p></body></html>`);
311
+ server.close();
312
+ resolve(code);
313
+ return;
314
+ }
315
+ res.end('<html><body><h1>Invalid Request</h1></body></html>');
316
+ }
317
+ else {
318
+ res.end('<html><body><h1>MS365 MCP Server OAuth2</h1><p>Waiting for authentication...</p></body></html>');
319
+ }
320
+ });
321
+ server.listen(44001, () => {
322
+ logger.log('OAuth2 callback server started on port 44001');
323
+ });
324
+ server.on('error', (err) => {
325
+ reject(err);
326
+ });
327
+ });
328
+ }
329
+ /**
330
+ * Get authenticated Microsoft Graph client
331
+ */
332
+ async getGraphClient(accountKey) {
333
+ // If no specific account key provided, use the first available account
334
+ if (!accountKey) {
335
+ const accounts = await this.listAuthenticatedAccounts();
336
+ if (accounts.length === 0) {
337
+ throw new Error('No authenticated accounts found. Please authenticate first.');
338
+ }
339
+ accountKey = accounts[0];
340
+ }
341
+ const storedToken = await this.loadStoredToken(accountKey);
342
+ if (!storedToken) {
343
+ throw new Error('No stored token found. Please authenticate first.');
344
+ }
345
+ // Check if token is expired
346
+ if (storedToken.expiresOn < Date.now()) {
347
+ await this.refreshToken(accountKey);
348
+ }
349
+ const client = Client.init({
350
+ authProvider: (done) => {
351
+ done(null, storedToken.accessToken);
352
+ }
353
+ });
354
+ return client;
355
+ }
356
+ /**
357
+ * Refresh access token
358
+ */
359
+ async refreshToken(accountKey = 'default-user') {
360
+ const storedToken = await this.loadStoredToken(accountKey);
361
+ if (!storedToken?.account) {
362
+ throw new Error('No account information available. Please re-authenticate.');
363
+ }
364
+ if (!await this.loadCredentials()) {
365
+ throw new Error('MS365 credentials not configured');
366
+ }
367
+ const msalClient = this.initializeMsalClient();
368
+ try {
369
+ const tokenResponse = await msalClient.acquireTokenSilent({
370
+ scopes: SCOPES,
371
+ account: storedToken.account
372
+ });
373
+ if (!tokenResponse) {
374
+ throw new Error('Failed to refresh token');
375
+ }
376
+ await this.saveToken(tokenResponse, storedToken.authType);
377
+ logger.log('MS365 token refreshed successfully');
378
+ }
379
+ catch (error) {
380
+ logger.error('Token refresh failed:', error);
381
+ throw error;
382
+ }
383
+ }
384
+ /**
385
+ * Check if user is authenticated
386
+ */
387
+ async isAuthenticated(accountKey) {
388
+ // If no specific account key provided, check all available accounts
389
+ if (!accountKey) {
390
+ const accounts = await this.listAuthenticatedAccounts();
391
+ if (accounts.length === 0) {
392
+ return false;
393
+ }
394
+ // Check if any account has valid authentication
395
+ for (const account of accounts) {
396
+ if (await this.isAuthenticated(account)) {
397
+ return true;
398
+ }
399
+ }
400
+ return false;
401
+ }
402
+ const storedToken = await this.loadStoredToken(accountKey);
403
+ if (!storedToken) {
404
+ return false;
405
+ }
406
+ // If token is expired, try to refresh
407
+ if (storedToken.expiresOn < Date.now()) {
408
+ try {
409
+ await this.refreshToken(accountKey);
410
+ return true;
411
+ }
412
+ catch (error) {
413
+ logger.error('Token refresh failed during authentication check:', error);
414
+ return false;
415
+ }
416
+ }
417
+ return true;
418
+ }
419
+ /**
420
+ * Check if credentials are configured
421
+ */
422
+ async isConfigured() {
423
+ return await this.loadCredentials();
424
+ }
425
+ /**
426
+ * Clear stored authentication data
427
+ */
428
+ async resetAuth(accountKey) {
429
+ try {
430
+ if (accountKey) {
431
+ // Delete specific account
432
+ await credentialStore.deleteCredentials(accountKey);
433
+ logger.log(`Cleared stored authentication tokens for account: ${accountKey}`);
434
+ }
435
+ else {
436
+ // Delete all authenticated accounts
437
+ const authenticatedAccounts = await this.listAuthenticatedAccounts();
438
+ if (authenticatedAccounts.length === 0) {
439
+ // If no accounts found by listing, try deleting the default-user key as fallback
440
+ await credentialStore.deleteCredentials('default-user');
441
+ logger.log('Cleared stored authentication tokens (fallback to default-user)');
442
+ }
443
+ else {
444
+ // Delete all found accounts
445
+ for (const account of authenticatedAccounts) {
446
+ await credentialStore.deleteCredentials(account);
447
+ logger.log(`Cleared stored authentication tokens for account: ${account}`);
448
+ }
449
+ }
450
+ }
451
+ }
452
+ catch (error) {
453
+ logger.error('Error clearing authentication data:', error);
454
+ }
455
+ }
456
+ /**
457
+ * Get authentication URL for device code flow
458
+ */
459
+ async getDeviceCodeInfo() {
460
+ if (!await this.loadCredentials()) {
461
+ throw new Error('MS365 credentials not configured');
462
+ }
463
+ const msalClient = this.initializeMsalClient();
464
+ return new Promise((resolve, reject) => {
465
+ const deviceCodeRequest = {
466
+ scopes: SCOPES,
467
+ deviceCodeCallback: (response) => {
468
+ resolve({
469
+ verificationUri: response.verificationUri,
470
+ userCode: response.userCode,
471
+ message: response.message
472
+ });
473
+ }
474
+ };
475
+ // This will trigger the callback immediately without completing auth
476
+ msalClient.acquireTokenByDeviceCode(deviceCodeRequest).catch(reject);
477
+ });
478
+ }
479
+ /**
480
+ * Start device code authentication and return device code info immediately
481
+ */
482
+ async startDeviceCodeAuth() {
483
+ if (!await this.loadCredentials()) {
484
+ throw new Error('MS365 credentials not configured');
485
+ }
486
+ const msalClient = this.initializeMsalClient();
487
+ return new Promise((resolve, reject) => {
488
+ const deviceCodeRequest = {
489
+ scopes: SCOPES,
490
+ deviceCodeCallback: (response) => {
491
+ const deviceCodeInfo = {
492
+ verificationUri: response.verificationUri,
493
+ userCode: response.userCode,
494
+ message: response.message
495
+ };
496
+ // Store the pending auth promise
497
+ const authPromise = msalClient.acquireTokenByDeviceCode(deviceCodeRequest)
498
+ .then(async (tokenResponse) => {
499
+ if (!tokenResponse) {
500
+ throw new Error('Failed to acquire token via device code');
501
+ }
502
+ await this.saveToken(tokenResponse, 'device');
503
+ logger.log('MS365 device code authentication successful');
504
+ this.pendingAuth = null; // Clear pending auth
505
+ return tokenResponse;
506
+ })
507
+ .catch((error) => {
508
+ this.pendingAuth = null; // Clear pending auth on error
509
+ throw error;
510
+ });
511
+ this.pendingAuth = { authPromise, deviceCodeInfo };
512
+ logger.log(`Device code authentication: ${response.verificationUri} - ${response.userCode}`);
513
+ // Return device code info immediately
514
+ resolve(deviceCodeInfo);
515
+ }
516
+ };
517
+ // This will never complete, but will call the callback immediately
518
+ msalClient.acquireTokenByDeviceCode(deviceCodeRequest).catch(() => {
519
+ // Ignore the error from this call since we're handling it in the stored promise
520
+ });
521
+ });
522
+ }
523
+ /**
524
+ * Wait for pending device code authentication to complete
525
+ */
526
+ async waitForDeviceCodeAuth() {
527
+ if (!this.pendingAuth) {
528
+ throw new Error('No pending device code authentication. Call startDeviceCodeAuth first.');
529
+ }
530
+ return await this.pendingAuth.authPromise;
531
+ }
532
+ /**
533
+ * Check if there's a pending device code authentication
534
+ */
535
+ hasPendingAuth() {
536
+ return this.pendingAuth !== null;
537
+ }
538
+ /**
539
+ * Get pending device code info
540
+ */
541
+ getPendingDeviceCodeInfo() {
542
+ return this.pendingAuth?.deviceCodeInfo || null;
543
+ }
544
+ /**
545
+ * Setup credentials interactively
546
+ */
547
+ async setupCredentials() {
548
+ const readline = await import('readline');
549
+ const rl = readline.createInterface({
550
+ input: process.stdin,
551
+ output: process.stdout
552
+ });
553
+ const question = (prompt) => {
554
+ return new Promise((resolve) => {
555
+ rl.question(prompt, resolve);
556
+ });
557
+ };
558
+ try {
559
+ console.log('\n🔧 MS365 MCP Server Credential Setup\n');
560
+ console.log('Choose authentication method:');
561
+ console.log('1. Device Code Flow (Recommended - no app registration needed)');
562
+ console.log('2. Custom Azure App (Advanced - requires app registration)\n');
563
+ const choice = await question('Enter your choice (1 or 2): ');
564
+ if (choice === '1') {
565
+ // Use built-in app with device code flow
566
+ const credentials = {
567
+ clientId: BUILTIN_CLIENT_ID,
568
+ tenantId: DEFAULT_TENANT_ID,
569
+ authType: 'device'
570
+ };
571
+ await this.saveCredentials(credentials);
572
+ console.log('\n✅ Configured for device code authentication!');
573
+ console.log('Run: ms365-mcp-server to start the server\n');
574
+ }
575
+ else if (choice === '2') {
576
+ console.log('\nCustom Azure App Setup:');
577
+ console.log('1. Go to https://portal.azure.com');
578
+ console.log('2. Navigate to Azure Active Directory > App registrations');
579
+ console.log('3. Click "New registration"');
580
+ console.log('4. Set redirect URI to: http://localhost:44001/oauth2callback');
581
+ console.log('5. Grant required API permissions for Microsoft Graph\n');
582
+ const clientId = await question('Enter your Client ID: ');
583
+ const clientSecret = await question('Enter your Client Secret (optional for device flow): ');
584
+ const tenantId = await question('Enter your Tenant ID (or "common" for multi-tenant): ');
585
+ const authType = clientSecret ? 'redirect' : 'device';
586
+ const credentials = {
587
+ clientId: clientId.trim(),
588
+ clientSecret: clientSecret.trim() || undefined,
589
+ tenantId: tenantId.trim(),
590
+ redirectUri: 'http://localhost:44001/oauth2callback',
591
+ authType: authType
592
+ };
593
+ await this.saveCredentials(credentials);
594
+ console.log('\n✅ Credentials saved successfully!');
595
+ console.log('Run: ms365-mcp-server to start the server\n');
596
+ }
597
+ else {
598
+ console.log('Invalid choice. Setup cancelled.');
599
+ }
600
+ }
601
+ finally {
602
+ rl.close();
603
+ }
604
+ }
605
+ /**
606
+ * Get storage method information
607
+ */
608
+ getStorageInfo() {
609
+ return {
610
+ method: credentialStore.getStorageMethod(),
611
+ location: credentialStore.isKeychainAvailable() ? 'OS Keychain' : CONFIG_DIR
612
+ };
613
+ }
614
+ /**
615
+ * List all authenticated accounts
616
+ */
617
+ async listAuthenticatedAccounts() {
618
+ return await credentialStore.listAccounts();
619
+ }
620
+ /**
621
+ * Get authentication URL without opening browser (redirect flow)
622
+ */
623
+ async getAuthUrl() {
624
+ if (!await this.loadCredentials()) {
625
+ throw new Error('MS365 credentials not configured');
626
+ }
627
+ if (!this.credentials?.clientSecret) {
628
+ throw new Error('Client secret required for redirect authentication. Use device code flow instead.');
629
+ }
630
+ const msalClient = this.initializeMsalClient();
631
+ const authUrl = await msalClient.getAuthCodeUrl({
632
+ scopes: SCOPES,
633
+ redirectUri: this.credentials.redirectUri,
634
+ prompt: 'consent'
635
+ });
636
+ return authUrl;
637
+ }
638
+ }
639
+ export const enhancedMS365Auth = new EnhancedMS365Auth();