public/utils/authProvider.ts (154 lines of code) (raw):

/*********************************************************** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License **********************************************************/ import { PublicClientApplication, Configuration, LogLevel, AccountInfo, AuthorizationCodeRequest, AuthorizationUrlRequest, AuthenticationResult, SilentFlowRequest } from '@azure/msal-node'; import { BrowserWindow } from 'electron'; const MSAL_CONFIG: Configuration = { auth: { clientId: '67ccd9d7-f5c7-475c-9da0-9700c24b2e66', authority: 'https://login.microsoftonline.com/common', }, system: { loggerOptions: { loggerCallback(loglevel, message, containsPii) { console.log(message); }, piiLoggingEnabled: false, logLevel: LogLevel.Verbose, } } }; export class AuthProvider { private clientApplication: PublicClientApplication; private account: AccountInfo; private authCodeUrlParams: AuthorizationUrlRequest; private authCodeRequest: AuthorizationCodeRequest; private silentProfileRequest: SilentFlowRequest; constructor() { this.clientApplication = new PublicClientApplication(MSAL_CONFIG); this.account = null; this.setRequestObjects(); } public get currentAccount(): AccountInfo | null { return this.account; } /** * Initialize request objects used by this AuthModule. */ private setRequestObjects(): void { const requestScopes = ['openid', 'profile', 'https://management.azure.com/user_impersonation']; const redirectUri = 'https://login.microsoftonline.com/oauth2/nativeclient'; const baseSilentRequest = { account: null, forceRefresh: false }; this.authCodeUrlParams = { scopes: requestScopes, redirectUri: redirectUri }; this.authCodeRequest = { scopes: requestScopes, redirectUri: redirectUri, code: null } this.silentProfileRequest = { ...baseSilentRequest, scopes: [], }; } async getProfileTokenIfPresent(): Promise<string> { let authResponse: AuthenticationResult; const account = this.account || await this.getAccount(); if (account) { this.silentProfileRequest.account = account; try { authResponse = await this.clientApplication.acquireTokenSilent(this.silentProfileRequest); } catch (error) { // do nothing } } return authResponse?.accessToken || null; } async getToken(authWindow: BrowserWindow, request: SilentFlowRequest): Promise<string> { let authResponse: AuthenticationResult; const account = this.account || await this.getAccount(); if (account) { request.account = account; authResponse = await this.getTokenSilent(authWindow, request); } else { authResponse = await this.getTokenInteractive(authWindow, this.authCodeRequest); } return authResponse.accessToken || null; } async getTokenSilent(authWindow: BrowserWindow, tokenRequest: SilentFlowRequest): Promise<AuthenticationResult> { try { return await this.clientApplication.acquireTokenSilent(tokenRequest); } catch (error) { console.log('Silent token acquisition failed, acquiring token using redirect'); return await this.getTokenInteractive(authWindow, this.authCodeRequest); } } async getTokenInteractive(authWindow: BrowserWindow, tokenRequest: AuthorizationUrlRequest ): Promise<AuthenticationResult> { const authCodeUrlParams = { ...this.authCodeUrlParams, scopes: tokenRequest.scopes }; const authCodeUrl = await this.clientApplication.getAuthCodeUrl(authCodeUrlParams); const authCode = await this.listenForAuthCode(authCodeUrl, authWindow); const authResult = await this.clientApplication.acquireTokenByCode({ ...this.authCodeRequest, scopes: tokenRequest.scopes, code: authCode}); return authResult; } async login(authWindow: BrowserWindow): Promise<void> { const authResult = await this.getTokenInteractive(authWindow, this.authCodeUrlParams); return this.handleResponse(authResult); } async loginSilent(): Promise<AccountInfo> { if (!this.account) { this.account = await this.getAccount(); } return this.account; } async logout(): Promise<void> { if (this.account) { await this.clientApplication.getTokenCache().removeAccount(this.account); this.account = null; } } private async listenForAuthCode(navigateUrl: string, authWindow: BrowserWindow): Promise<string> { authWindow.loadURL(navigateUrl); return new Promise((resolve, reject) => { authWindow.webContents.on('will-redirect', (event, responseUrl) => { try { const parsedUrl = new URL(responseUrl); const authCode = parsedUrl.searchParams.get('code'); if(authCode) { resolve(authCode); } } catch (err) { reject(err); } }); }); } /** * Handles the response from a popup or redirect. If response is null, will check if we have any accounts and attempt to sign in. * @param response */ private async handleResponse(response: AuthenticationResult) { if (response !== null) { this.account = response.account; } else { this.account = await this.getAccount(); } } /** * Calls getAllAccounts and determines the correct account to sign into, currently defaults to first account found in cache. * TODO: Add account chooser code * * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-common/docs/Accounts.md */ private async getAccount(): Promise<AccountInfo> { // need to call getAccount here? const cache = this.clientApplication.getTokenCache(); const currentAccounts = await cache.getAllAccounts(); if (currentAccounts === null) { console.log('No accounts detected'); return null; } if (currentAccounts.length > 1) { // Add choose account code here console.log('Multiple accounts detected, need to add choose account code.'); return currentAccounts[0]; } else if (currentAccounts.length === 1) { return currentAccounts[0]; } else { return null; } } }