From 7f5e8c1bb7dc09262c9b12081a1c801cc8022787 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Mon, 3 Aug 2020 15:29:17 -0700 Subject: [PATCH 1/5] Update to @azure/msal-browser@2.0.0 --- .../src/Interop/AuthenticationService.ts | 183 +++++++++++------- .../src/Interop/package.json | 2 +- .../Authentication.Msal/src/Interop/yarn.lock | 33 +++- 3 files changed, 138 insertions(+), 80 deletions(-) diff --git a/src/Components/WebAssembly/Authentication.Msal/src/Interop/AuthenticationService.ts b/src/Components/WebAssembly/Authentication.Msal/src/Interop/AuthenticationService.ts index b06e1892d5c6..e646182dd805 100644 --- a/src/Components/WebAssembly/Authentication.Msal/src/Interop/AuthenticationService.ts +++ b/src/Components/WebAssembly/Authentication.Msal/src/Interop/AuthenticationService.ts @@ -1,6 +1,5 @@ -import * as Msal from 'msal'; -import { StringDict } from 'msal/lib-commonjs/MsalTypes'; -import { ClientAuthErrorMessage } from 'msal/lib-commonjs/error/ClientAuthError'; +import * as Msal from '@azure/msal-browser'; +import { StringDict } from '@azure/msal-common'; interface AccessTokenRequestOptions { scopes: string[]; @@ -52,34 +51,50 @@ interface AuthorizeServiceConfiguration extends Msal.Configuration { } class MsalAuthorizeService implements AuthorizeService { - readonly _msalApplication: Msal.UserAgentApplication; - readonly _callbackPromise: Promise; + private readonly _msalApplication: Msal.PublicClientApplication; + private _account: Msal.AccountInfo | undefined; constructor(private readonly _settings: AuthorizeServiceConfiguration) { - - // It is important that we capture the callback-url here as msal will remove the auth parameters - // from the url as soon as it gets initialized. const callbackUrl = location.href; - this._msalApplication = new Msal.UserAgentApplication(this._settings); + this._msalApplication = new Msal.PublicClientApplication(this._settings); + } + + getAccount() { + if (this._account) { + return this._account; + } - // This promise will only resolve in callback-paths, which is where we check it. - this._callbackPromise = this.createCallbackResult(callbackUrl); + const accounts = this._msalApplication.getAllAccounts(); + if (accounts && accounts.length) { + return accounts[0]; + } + + return null; } async getUser() { - const account = this._msalApplication.getAccount(); - return account?.idTokenClaims; + const account = this.getAccount(); + if (!account) { + return; + } + + const silentRequest = { + redirectUri: this._settings.auth?.redirectUri, + account: account, + scopes: this._settings.defaultAccessTokenScopes + }; + + const response = await this._msalApplication.acquireTokenSilent(silentRequest); + return response.idTokenClaims; } async getAccessToken(request?: AccessTokenRequestOptions): Promise { try { const newToken = await this.getTokenCore(request?.scopes); - return { status: AccessTokenResultStatus.Success, token: newToken }; - } catch (e) { return { status: AccessTokenResultStatus.RequiresRedirect @@ -88,12 +103,18 @@ class MsalAuthorizeService implements AuthorizeService { } async getTokenCore(scopes?: string[]): Promise { - const tokenScopes = { - redirectUri: this._settings.auth.redirectUri as string, + const account = this.getAccount(); + if (!account) { + return; + } + + const silentRequest = { + redirectUri: this._settings.auth?.redirectUri, + account: account, scopes: scopes || this._settings.defaultAccessTokenScopes }; - const response = await this._msalApplication.acquireTokenSilent(tokenScopes); + const response = await this._msalApplication.acquireTokenSilent(silentRequest); return { value: response.accessToken, grantedScopes: response.scopes, @@ -106,9 +127,10 @@ class MsalAuthorizeService implements AuthorizeService { // Before we start any sign-in flow, clear out any previous state so that it doesn't pile up. this.purgeState(); - const request: Msal.AuthenticationParameters = { - redirectUri: this._settings.auth.redirectUri as string, - state: await this.saveState(state) + const request: Msal.AuthorizationUrlRequest = { + redirectUri: this._settings.auth?.redirectUri, + state: await this.saveState(state), + scopes: [] }; if (this._settings.defaultAccessTokenScopes && this._settings.defaultAccessTokenScopes.length > 0) { @@ -130,7 +152,16 @@ class MsalAuthorizeService implements AuthorizeService { if (this._settings.defaultAccessTokenScopes?.length > 0) { // This provisions the token as part of the sign-in flow eagerly so that is already in the cache // when the app asks for it. - await this._msalApplication.acquireTokenSilent(request); + const account = this.getAccount(); + if (!account) { + return this.error("No account to get tokens with."); + } + const silentRequest = { + redirectUri: request.redirectUri, + account: account, + scopes: request.scopes, + }; + await this._msalApplication.acquireTokenSilent(silentRequest); } } catch (e) { return this.error(e.errorMessage); @@ -142,33 +173,42 @@ class MsalAuthorizeService implements AuthorizeService { } } - async signInCore(request: Msal.AuthenticationParameters): Promise { - if (this._settings.loginMode.toLowerCase() === "redirect") { - try { - this._msalApplication.loginRedirect(request); - } catch (e) { - return e; - } + async signInCore(request: Msal.AuthorizationUrlRequest): Promise { + const loginMode = this._settings.loginMode.toLowerCase(); + if (loginMode === 'redirect') { + return this.signInWithRedirect(request); } else { - try { - return await this._msalApplication.loginPopup(request); - } catch (e) { - // If the user explicitly cancelled the pop-up, avoid performing a redirect. - if (this.isMsalError(e) && e.errorCode !== ClientAuthErrorMessage.userCancelledError.code) { - try { - this._msalApplication.loginRedirect(request); - } catch (e) { - return e; - } - } else { - return e; - } + return this.signInWithPopup(request); + } + } + + private async signInWithRedirect(request: Msal.RedirectRequest) { + try { + this._msalApplication.loginRedirect(request); + } catch (e) { + return e; + } + } + + private async signInWithPopup(request: Msal.PopupRequest) { + try { + return await this._msalApplication.loginPopup(request); + } catch (e) { + // If the user explicitly cancelled the pop-up, avoid performing a redirect. + if (this.isMsalError(e) && e.errorCode !== Msal.BrowserAuthErrorMessage.userCancelledError.code) { + this.signInWithRedirect(request); + } else { + return e; } } } - completeSignIn() { - return this._callbackPromise; + async completeSignIn() { + const account = this.getAccount(); + if (account) { + return this.success(account); + } + return this.operationCompleted(); } async signOut(state: any) { @@ -241,7 +281,7 @@ class MsalAuthorizeService implements AuthorizeService { // msal.js doesn't support the state parameter on logout flows, which forces us to shim our own logout state. // The format then is different, as msal follows the pattern state=<>|<> and our format // simple uses <>. - const appState = !isLogout ? this._msalApplication.getAccountState(state[0]) : state[0]; + const appState = !isLogout ? this.getAccountState(state[0]) : state[0]; const stateKey = `${AuthenticationService._infrastructureKey}.AuthorizeService.${appState}`; const stateString = sessionStorage.getItem(stateKey); if (stateString) { @@ -262,37 +302,35 @@ class MsalAuthorizeService implements AuthorizeService { } } - private async createCallbackResult(callbackUrl: string): Promise { - // msal.js requires a callback to be registered during app initialization to handle redirect flows. - // To map that behavior to our API we register a callback early and store the result of that callback - // as a promise on an instance field to be able to serve the state back to the main app. - const promiseFactory = (resolve: (result: Msal.AuthResponse) => void, reject: (error: Msal.AuthError) => void): void => { - this._msalApplication.handleRedirectCallback( - authenticationResponse => { - resolve(authenticationResponse); - }, - authenticationError => { - reject(authenticationError); - }); - } - - try { - // Evaluate the promise to capture any authentication errors - await new Promise(promiseFactory); - // See https://github.com/AzureAD/microsoft-authentication-library-for-js/wiki/FAQs#q6-how-to-avoid-page-reloads-when-acquiring-and-renewing-tokens-silently - if (window !== window.parent && !window.opener) { - return this.operationCompleted(); + async initializeMsalHandler() { + this._msalApplication.handleRedirectPromise().then( + (result: Msal.AuthenticationResult | null) => this.handleResult(result) + ).catch((error: any) => { + if (this.isMsalError(error)) { + return this.error(error.errorMessage); } else { - const state = await this.retrieveState(callbackUrl); - return this.success(state); + return this.error(error); } - } catch (e) { - if (this.isMsalError(e)) { - return this.error(e.errorMessage); - } else { - return this.error(e); + }) + } + + private handleResult(result: Msal.AuthenticationResult | null) { + if (result != null) { + this._account = result.account; + return this.success(result.state); + } else { + return this.operationCompleted(); + } + } + + private getAccountState(state: string) { + if (state) { + const splitIndex = state.indexOf("|"); + if (splitIndex > -1 && splitIndex + 1 < state.length) { + return state.substring(splitIndex + 1); } } + return state; } private isMsalError(resultOrError: any): resultOrError is Msal.AuthError { @@ -326,6 +364,7 @@ export class AuthenticationService { if (!AuthenticationService._initialized) { AuthenticationService._initialized = true; AuthenticationService.instance = new MsalAuthorizeService(settings); + await AuthenticationService.instance.initializeMsalHandler(); } } diff --git a/src/Components/WebAssembly/Authentication.Msal/src/Interop/package.json b/src/Components/WebAssembly/Authentication.Msal/src/Interop/package.json index 68a27c2e1c46..e834f0cf6365 100644 --- a/src/Components/WebAssembly/Authentication.Msal/src/Interop/package.json +++ b/src/Components/WebAssembly/Authentication.Msal/src/Interop/package.json @@ -13,6 +13,6 @@ "webpack-cli": "^3.3.10" }, "dependencies": { - "msal": "^1.2.1" + "@azure/msal-browser": "^2.0.0" } } diff --git a/src/Components/WebAssembly/Authentication.Msal/src/Interop/yarn.lock b/src/Components/WebAssembly/Authentication.Msal/src/Interop/yarn.lock index 865e68eec452..16d23f8c4b89 100644 --- a/src/Components/WebAssembly/Authentication.Msal/src/Interop/yarn.lock +++ b/src/Components/WebAssembly/Authentication.Msal/src/Interop/yarn.lock @@ -2,6 +2,20 @@ # yarn lockfile v1 +"@azure/msal-browser@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@azure/msal-browser/-/msal-browser-2.0.0.tgz#09eb3ed2112bdcd11c751f1c0b9cec588b49b8c6" + integrity sha512-0L4XksaXmtl870bQTPxbHCkxMEMmSbsgkkVpb6bvXg8ngOLWnkxvV6tstj84JtQHcJPjNYkYY41jgBQxgC4/KQ== + dependencies: + "@azure/msal-common" "1.0.0" + +"@azure/msal-common@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@azure/msal-common/-/msal-common-1.0.0.tgz#421f4859e6cb68cfacb03bb2c6c873efdffc76f0" + integrity sha512-l/+1Z9kQWLAlMwJ/c3MGhy4ujtEAK/3CMUaUXHvjUIsQknLFRb9+b3id5YSuToPfAvdUkAQGDZiQXosv1I+eLA== + dependencies: + debug "^4.1.1" + "@webassemblyjs/ast@1.8.5": version "1.8.5" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.8.5.tgz#51b1c5fe6576a34953bf4b253df9f0d490d9e359" @@ -690,6 +704,13 @@ debug@^2.2.0, debug@^2.3.3: dependencies: ms "2.0.0" +debug@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" + decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -1667,12 +1688,10 @@ ms@2.0.0: resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= -msal@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/msal/-/msal-1.2.1.tgz#08133e37ab0b9741866c89a3fadc55aadb980723" - integrity sha512-Zo28eyRtT/Un+zcpMfPtTPD+eo/OqzsRER0k5dyk8Mje/K1oLlaEOAgZHlJs59Y2xyuVg8OrcKqSn/1MeNjZYw== - dependencies: - tslib "^1.9.3" +ms@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== nan@^2.12.1: version "2.14.0" @@ -2485,7 +2504,7 @@ ts-loader@^6.2.1: micromatch "^4.0.0" semver "^6.0.0" -tslib@^1.9.0, tslib@^1.9.3: +tslib@^1.9.0: version "1.10.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== From 1d294017ff2d8f56070df86f0c6bb65cf083f7af Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Thu, 13 Aug 2020 10:05:15 -0700 Subject: [PATCH 2/5] Retain promise during initialization --- .../src/Interop/AuthenticationService.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Components/WebAssembly/Authentication.Msal/src/Interop/AuthenticationService.ts b/src/Components/WebAssembly/Authentication.Msal/src/Interop/AuthenticationService.ts index e646182dd805..99f4e970402e 100644 --- a/src/Components/WebAssembly/Authentication.Msal/src/Interop/AuthenticationService.ts +++ b/src/Components/WebAssembly/Authentication.Msal/src/Interop/AuthenticationService.ts @@ -154,7 +154,7 @@ class MsalAuthorizeService implements AuthorizeService { // when the app asks for it. const account = this.getAccount(); if (!account) { - return this.error("No account to get tokens with."); + return this.error("No account to get tokens for."); } const silentRequest = { redirectUri: request.redirectUri, @@ -357,15 +357,15 @@ class MsalAuthorizeService implements AuthorizeService { export class AuthenticationService { static _infrastructureKey = 'Microsoft.Authentication.WebAssembly.Msal'; - static _initialized = false; + static _initialized: Promise; static instance: MsalAuthorizeService; public static async init(settings: AuthorizeServiceConfiguration) { if (!AuthenticationService._initialized) { - AuthenticationService._initialized = true; AuthenticationService.instance = new MsalAuthorizeService(settings); - await AuthenticationService.instance.initializeMsalHandler(); + AuthenticationService._initialized = AuthenticationService.instance.initializeMsalHandler(); } + return AuthenticationService._initialized; } public static getUser() { From 995bf46f12db5ddb66f37dd0800bb58966276d75 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Mon, 17 Aug 2020 13:49:48 -0700 Subject: [PATCH 3/5] Set knownAuthority hostname from authority URL --- .../Authentication.Msal/src/Interop/AuthenticationService.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Components/WebAssembly/Authentication.Msal/src/Interop/AuthenticationService.ts b/src/Components/WebAssembly/Authentication.Msal/src/Interop/AuthenticationService.ts index 99f4e970402e..ef3d52d7d3cb 100644 --- a/src/Components/WebAssembly/Authentication.Msal/src/Interop/AuthenticationService.ts +++ b/src/Components/WebAssembly/Authentication.Msal/src/Interop/AuthenticationService.ts @@ -56,6 +56,9 @@ class MsalAuthorizeService implements AuthorizeService { constructor(private readonly _settings: AuthorizeServiceConfiguration) { const callbackUrl = location.href; + if (this._settings.auth) { + this._settings.auth.knownAuthorities = [new URL(this._settings.auth.authority!).hostname] + } this._msalApplication = new Msal.PublicClientApplication(this._settings); } From f66db6ce2e2e92b7eca22f22ddba519472a3d402 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Tue, 18 Aug 2020 18:33:42 -0700 Subject: [PATCH 4/5] Add KnownAuthorities config option and fix silent sign-in --- .../src/Interop/AuthenticationService.ts | 11 +++++++---- .../src/Models/MsalAuthenticationOptions.cs | 6 ++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/Components/WebAssembly/Authentication.Msal/src/Interop/AuthenticationService.ts b/src/Components/WebAssembly/Authentication.Msal/src/Interop/AuthenticationService.ts index ef3d52d7d3cb..7a0c31222d1d 100644 --- a/src/Components/WebAssembly/Authentication.Msal/src/Interop/AuthenticationService.ts +++ b/src/Components/WebAssembly/Authentication.Msal/src/Interop/AuthenticationService.ts @@ -53,10 +53,10 @@ interface AuthorizeServiceConfiguration extends Msal.Configuration { class MsalAuthorizeService implements AuthorizeService { private readonly _msalApplication: Msal.PublicClientApplication; private _account: Msal.AccountInfo | undefined; + private _redirectCallback: Promise | undefined; constructor(private readonly _settings: AuthorizeServiceConfiguration) { - const callbackUrl = location.href; - if (this._settings.auth) { + if (this._settings.auth && !this._settings.auth.knownAuthorities) { this._settings.auth.knownAuthorities = [new URL(this._settings.auth.authority!).hostname] } this._msalApplication = new Msal.PublicClientApplication(this._settings); @@ -187,7 +187,7 @@ class MsalAuthorizeService implements AuthorizeService { private async signInWithRedirect(request: Msal.RedirectRequest) { try { - this._msalApplication.loginRedirect(request); + return await this._msalApplication.loginRedirect(request); } catch (e) { return e; } @@ -207,6 +207,9 @@ class MsalAuthorizeService implements AuthorizeService { } async completeSignIn() { + // Make sure that the redirect handler has completed execution before + // completing sign in. + await this._redirectCallback; const account = this.getAccount(); if (account) { return this.success(account); @@ -306,7 +309,7 @@ class MsalAuthorizeService implements AuthorizeService { } async initializeMsalHandler() { - this._msalApplication.handleRedirectPromise().then( + this._redirectCallback = this._msalApplication.handleRedirectPromise().then( (result: Msal.AuthenticationResult | null) => this.handleResult(result) ).catch((error: any) => { if (this.isMsalError(error)) { diff --git a/src/Components/WebAssembly/Authentication.Msal/src/Models/MsalAuthenticationOptions.cs b/src/Components/WebAssembly/Authentication.Msal/src/Models/MsalAuthenticationOptions.cs index fb75968b6610..b42608a3009b 100644 --- a/src/Components/WebAssembly/Authentication.Msal/src/Models/MsalAuthenticationOptions.cs +++ b/src/Components/WebAssembly/Authentication.Msal/src/Models/MsalAuthenticationOptions.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; namespace Microsoft.Authentication.WebAssembly.Msal { @@ -48,5 +49,10 @@ public class MsalAuthenticationOptions /// Gets or sets whether or not to navigate to the login request url after a successful login. /// public bool NavigateToLoginRequestUrl { get; set; } = false; + + /// + /// Gets or sets the set of known authority host names for the application. + /// + public IList KnownAuthorities { get; set; } } } From bb97305c02567bc54b4a122e72a580fbf4413903 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Wed, 19 Aug 2020 16:32:03 -0700 Subject: [PATCH 5/5] Set knownAuthorities default to empty list --- .../Authentication.Msal/src/Interop/AuthenticationService.ts | 2 +- .../Authentication.Msal/src/Models/MsalAuthenticationOptions.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Components/WebAssembly/Authentication.Msal/src/Interop/AuthenticationService.ts b/src/Components/WebAssembly/Authentication.Msal/src/Interop/AuthenticationService.ts index 7a0c31222d1d..cb9ae90ab42c 100644 --- a/src/Components/WebAssembly/Authentication.Msal/src/Interop/AuthenticationService.ts +++ b/src/Components/WebAssembly/Authentication.Msal/src/Interop/AuthenticationService.ts @@ -56,7 +56,7 @@ class MsalAuthorizeService implements AuthorizeService { private _redirectCallback: Promise | undefined; constructor(private readonly _settings: AuthorizeServiceConfiguration) { - if (this._settings.auth && !this._settings.auth.knownAuthorities) { + if (this._settings.auth?.knownAuthorities?.length == 0) { this._settings.auth.knownAuthorities = [new URL(this._settings.auth.authority!).hostname] } this._msalApplication = new Msal.PublicClientApplication(this._settings); diff --git a/src/Components/WebAssembly/Authentication.Msal/src/Models/MsalAuthenticationOptions.cs b/src/Components/WebAssembly/Authentication.Msal/src/Models/MsalAuthenticationOptions.cs index b42608a3009b..53951a356f35 100644 --- a/src/Components/WebAssembly/Authentication.Msal/src/Models/MsalAuthenticationOptions.cs +++ b/src/Components/WebAssembly/Authentication.Msal/src/Models/MsalAuthenticationOptions.cs @@ -53,6 +53,6 @@ public class MsalAuthenticationOptions /// /// Gets or sets the set of known authority host names for the application. /// - public IList KnownAuthorities { get; set; } + public IList KnownAuthorities { get; set; } = new List(); } }