Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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[];
Expand Down Expand Up @@ -52,34 +51,53 @@ interface AuthorizeServiceConfiguration extends Msal.Configuration {
}

class MsalAuthorizeService implements AuthorizeService {
readonly _msalApplication: Msal.UserAgentApplication;
readonly _callbackPromise: Promise<AuthenticationResult>;
private readonly _msalApplication: Msal.PublicClientApplication;
private _account: Msal.AccountInfo | undefined;
private _redirectCallback: Promise<AuthenticationResult | null> | undefined;

constructor(private readonly _settings: AuthorizeServiceConfiguration) {
if (this._settings.auth?.knownAuthorities?.length == 0) {
this._settings.auth.knownAuthorities = [new URL(this._settings.auth.authority!).hostname]
}
Copy link
Member

@javiercn javiercn Aug 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should make sure this doesn't give AAD any grief. Otherwise we'll have to do it conditionally.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. I've added this to make my dev loop faster but can remove later if this is not necessary.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we'll have to expose knownAuthorities in the MsalProviderOptions and maybe do this by default if no authorities are specified, it might be that this ends up clashing with AAD so it is something we must only do in B2C, similar to the ValidateAuthority parameter

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added it as an option to MsalProviderOptions and added fallback logic in the AuthenticationService.

it might be that this ends up clashing with AAD so it is something we must only do in B2C, similar to the ValidateAuthority parameter

Should we achieve this by setting it to default to an empty list? Not sure what kind of collisions we can expect here...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it might be that this ends up clashing with AAD so it is something we must only do in B2C, similar to the ValidateAuthority parameter

I can imagine AAD having an authority parameter and a knownAuthorities parameter that doesn't match. I'm just saying that we should validate that this doesn't break AAD, just that.

this._msalApplication = new Msal.PublicClientApplication(this._settings);
}

getAccount() {
if (this._account) {
return this._account;
}

// 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);
const accounts = this._msalApplication.getAllAccounts();
if (accounts && accounts.length) {
return accounts[0];
}

// This promise will only resolve in callback-paths, which is where we check it.
this._callbackPromise = this.createCallbackResult(callbackUrl);
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<AccessTokenResult> {
try {
const newToken = await this.getTokenCore(request?.scopes);

return {
status: AccessTokenResultStatus.Success,
token: newToken
};

} catch (e) {
return {
status: AccessTokenResultStatus.RequiresRedirect
Expand All @@ -88,12 +106,18 @@ class MsalAuthorizeService implements AuthorizeService {
}

async getTokenCore(scopes?: string[]): Promise<AccessToken | undefined> {
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,
Expand All @@ -106,9 +130,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) {
Expand All @@ -130,7 +155,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 for.");
}
const silentRequest = {
redirectUri: request.redirectUri,
account: account,
scopes: request.scopes,
};
await this._msalApplication.acquireTokenSilent(silentRequest);
}
} catch (e) {
return this.error(e.errorMessage);
Expand All @@ -142,33 +176,45 @@ class MsalAuthorizeService implements AuthorizeService {
}
}

async signInCore(request: Msal.AuthenticationParameters): Promise<Msal.AuthResponse | Msal.AuthError | undefined> {
if (this._settings.loginMode.toLowerCase() === "redirect") {
try {
this._msalApplication.loginRedirect(request);
} catch (e) {
return e;
}
async signInCore(request: Msal.AuthorizationUrlRequest): Promise<Msal.AuthenticationResult | Msal.AuthError | undefined> {
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 {
return await 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() {
// 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);
}
return this.operationCompleted();
}

async signOut(state: any) {
Expand Down Expand Up @@ -241,7 +287,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=<<guid>>|<<user_state>> and our format
// simple uses <<base64urlIdentifier>>.
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) {
Expand All @@ -262,37 +308,35 @@ class MsalAuthorizeService implements AuthorizeService {
}
}

private async createCallbackResult(callbackUrl: string): Promise<AuthenticationResult> {
// 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<Msal.AuthResponse>(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._redirectCallback = 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 {
Expand All @@ -319,14 +363,15 @@ class MsalAuthorizeService implements AuthorizeService {
export class AuthenticationService {

static _infrastructureKey = 'Microsoft.Authentication.WebAssembly.Msal';
static _initialized = false;
static _initialized: Promise<void>;
static instance: MsalAuthorizeService;

public static async init(settings: AuthorizeServiceConfiguration) {
if (!AuthenticationService._initialized) {
AuthenticationService._initialized = true;
AuthenticationService.instance = new MsalAuthorizeService(settings);
AuthenticationService._initialized = AuthenticationService.instance.initializeMsalHandler();
}
return AuthenticationService._initialized;
}

public static getUser() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@
"webpack-cli": "^3.3.10"
},
"dependencies": {
"msal": "^1.2.1"
"@azure/msal-browser": "^2.0.0"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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/[email protected]":
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/[email protected]":
version "1.8.5"
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.8.5.tgz#51b1c5fe6576a34953bf4b253df9f0d490d9e359"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -1667,12 +1688,10 @@ [email protected]:
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"
Expand Down Expand Up @@ -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==
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -48,5 +49,10 @@ public class MsalAuthenticationOptions
/// Gets or sets whether or not to navigate to the login request url after a successful login.
/// </summary>
public bool NavigateToLoginRequestUrl { get; set; } = false;

/// <summary>
/// Gets or sets the set of known authority host names for the application.
/// </summary>
public IList<string> KnownAuthorities { get; set; } = new List<string>();
}
}