From 134d55a8712a7585e78dfca0412d7d926d4f8d2e Mon Sep 17 00:00:00 2001 From: Minh-Anh Phan Date: Wed, 4 Dec 2024 18:48:55 +0000 Subject: [PATCH 1/6] init remove abstract client --- .../src/msal/browserFlows/msalAuthCode.ts | 145 +++++++++++++- .../msal/browserFlows/msalBrowserCommon.ts | 185 +----------------- 2 files changed, 139 insertions(+), 191 deletions(-) diff --git a/sdk/identity/identity/src/msal/browserFlows/msalAuthCode.ts b/sdk/identity/identity/src/msal/browserFlows/msalAuthCode.ts index dadec04e8f2d..ef6633e0e4f9 100644 --- a/sdk/identity/identity/src/msal/browserFlows/msalAuthCode.ts +++ b/sdk/identity/identity/src/msal/browserFlows/msalAuthCode.ts @@ -3,21 +3,49 @@ import * as msalBrowser from "@azure/msal-browser"; -import type { MsalBrowserFlowOptions } from "./msalBrowserCommon.js"; -import { MsalBrowser } from "./msalBrowserCommon.js"; +import type { MsalBrowserFlow, MsalBrowserFlowOptions } from "./msalBrowserCommon.js"; import { defaultLoggerCallback, + ensureValidMsalToken, + getAuthority, + getKnownAuthorities, getMSALLogLevel, handleMsalError, msalToPublic, publicToMsal, } from "../utils.js"; -import type { AccessToken } from "@azure/core-auth"; -import type { AuthenticationRecord } from "../types.js"; -import { AuthenticationRequiredError } from "../../errors.js"; +import type { AccessToken, GetTokenOptions } from "@azure/core-auth"; +import type { AuthenticationRecord, MsalResult } from "../types.js"; +import { AuthenticationRequiredError, CredentialUnavailableError } from "../../errors.js"; import type { CredentialFlowGetTokenOptions } from "../credentials.js"; import { getLogLevel } from "@azure/logger"; +import { BrowserLoginStyle } from "../../credentials/interactiveBrowserCredentialOptions.js"; +import { CredentialLogger, formatSuccess } from "../../util/logging.js"; +import { processMultiTenantRequest, resolveAdditionallyAllowedTenantIds, resolveTenantId } from "../../util/tenantIdUtils.js"; +import { DefaultTenantId } from "../../constants.js"; + +/** + * Generates a MSAL configuration that generally works for browsers + * @internal + */ +function defaultBrowserMsalConfig( + options: MsalBrowserFlowOptions, +): msalBrowser.Configuration { + const tenantId = options.tenantId || DefaultTenantId; + const authority = getAuthority(tenantId, options.authorityHost); + return { + auth: { + clientId: options.clientId!, + authority, + knownAuthorities: getKnownAuthorities(tenantId, authority, options.disableInstanceDiscovery), + // If the users picked redirect as their login style, + // but they didn't provide a redirectUri, + // we can try to use the current page we're in as a default value. + redirectUri: options.redirectUri || self.location.origin, + }, + }; +} // We keep a copy of the redirect hash. const redirectHash = self.location.hash; @@ -27,8 +55,17 @@ const redirectHash = self.location.hash; * which uses the [Auth Code Flow](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow). * @internal */ -export class MSALAuthCode extends MsalBrowser { +export class MSALAuthCode implements MsalBrowserFlow { + protected loginStyle: BrowserLoginStyle; + protected clientId: string; + protected tenantId: string; + protected additionallyAllowedTenantIds: string[]; + protected authorityHost?: string; + protected account: AuthenticationRecord | undefined; + protected msalConfig: msalBrowser.Configuration; + protected disableAutomaticAuthentication?: boolean; protected app?: msalBrowser.IPublicClientApplication; + protected logger: CredentialLogger; private loginHint?: string; /** @@ -38,7 +75,26 @@ export class MSALAuthCode extends MsalBrowser { * @param options - Parameters necessary and otherwise used to create the MSAL object. */ constructor(options: MsalBrowserFlowOptions) { - super(options); + this.logger = options.logger; + this.loginStyle = options.loginStyle; + if (!options.clientId) { + throw new CredentialUnavailableError("A client ID is required in browsers"); + } + this.clientId = options.clientId; + this.additionallyAllowedTenantIds = resolveAdditionallyAllowedTenantIds( + options?.tokenCredentialOptions?.additionallyAllowedTenants, + ); + this.tenantId = resolveTenantId(this.logger, options.tenantId, options.clientId); + this.authorityHost = options.authorityHost; + this.msalConfig = defaultBrowserMsalConfig(options); + this.disableAutomaticAuthentication = options.disableAutomaticAuthentication; + + if (options.authenticationRecord) { + this.account = { + ...options.authenticationRecord, + tenantId: this.tenantId, + }; + } this.loginHint = options.loginHint; this.msalConfig.cache = { @@ -60,6 +116,81 @@ export class MSALAuthCode extends MsalBrowser { } } + /** + * In the browsers we don't need to init() + */ + async init(): Promise { + // Nothing to do here. + } + + /** + * Clears MSAL's cache. + */ + async logout(): Promise { + this.app?.logout(); + } + /** + * Attempts to retrieve an authenticated token from MSAL. + */ + public async getToken( + scopes: string[], + options: CredentialFlowGetTokenOptions = {}, + ): Promise { + const tenantId = + processMultiTenantRequest(this.tenantId, options, this.additionallyAllowedTenantIds) || + this.tenantId; + + if (!options.authority) { + options.authority = getAuthority(tenantId, this.authorityHost); + } + + // We ensure that redirection is handled at this point. + await this.handleRedirect(); + + if (!(await this.getActiveAccount()) && !this.disableAutomaticAuthentication) { + await this.login(scopes); + } + return this.getTokenSilent(scopes).catch((err) => { + if (err.name !== "AuthenticationRequiredError") { + throw err; + } + if (options?.disableAutomaticAuthentication) { + throw new AuthenticationRequiredError({ + scopes, + getTokenOptions: options, + message: + "Automatic authentication has been disabled. You may call the authentication() method.", + }); + } + this.logger.info( + `Silent authentication failed, falling back to interactive method ${this.loginStyle}`, + ); + return this.doGetToken(scopes); + }); + } + + /** + * Handles the MSAL authentication result. + * If the result has an account, we update the local account reference. + * If the token received is invalid, an error will be thrown depending on what's missing. + */ + protected handleResult( + scopes: string | string[], + result?: MsalResult, + getTokenOptions?: GetTokenOptions, + ): AccessToken { + if (result?.account) { + this.account = msalToPublic(this.clientId, result.account); + } + ensureValidMsalToken(scopes, result, getTokenOptions); + this.logger.getToken.info(formatSuccess(scopes)); + return { + token: result.accessToken, + expiresOnTimestamp: result.expiresOn.getTime(), + refreshAfterTimestamp: result.refreshOn?.getTime(), + tokenType: "Bearer", + } as AccessToken; + } private async getApp(): Promise { if (!this.app) { // Prepare the MSAL application diff --git a/sdk/identity/identity/src/msal/browserFlows/msalBrowserCommon.ts b/sdk/identity/identity/src/msal/browserFlows/msalBrowserCommon.ts index c7916b2a7787..a2ca8c645db6 100644 --- a/sdk/identity/identity/src/msal/browserFlows/msalBrowserCommon.ts +++ b/sdk/identity/identity/src/msal/browserFlows/msalBrowserCommon.ts @@ -1,24 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import type * as msalBrowser from "@azure/msal-browser"; - -import type { AccessToken, GetTokenOptions } from "@azure/core-auth"; -import type { AuthenticationRecord, MsalResult } from "../types.js"; -import { AuthenticationRequiredError, CredentialUnavailableError } from "../../errors.js"; -import type { CredentialLogger } from "../../util/logging.js"; -import { formatSuccess } from "../../util/logging.js"; +import type { AuthenticationRecord } from "../types.js"; import type { MsalFlow, MsalFlowOptions } from "./flows.js"; -import { ensureValidMsalToken, getAuthority, getKnownAuthorities, msalToPublic } from "../utils.js"; -import { - processMultiTenantRequest, - resolveAdditionallyAllowedTenantIds, - resolveTenantId, -} from "../../util/tenantIdUtils.js"; import type { BrowserLoginStyle } from "../../credentials/interactiveBrowserCredentialOptions.js"; -import type { CredentialFlowGetTokenOptions } from "../credentials.js"; -import { DefaultTenantId } from "../../constants.js"; import type { LogPolicyOptions } from "@azure/core-rest-pipeline"; import type { MultiTenantTokenCredentialOptions } from "../../credentials/multiTenantTokenCredentialOptions.js"; @@ -54,172 +40,3 @@ export interface MsalBrowserFlow extends MsalFlow { login(scopes?: string[]): Promise; handleRedirect(): Promise; } - -/** - * Generates a MSAL configuration that generally works for browsers - * @internal - */ -export function defaultBrowserMsalConfig( - options: MsalBrowserFlowOptions, -): msalBrowser.Configuration { - const tenantId = options.tenantId || DefaultTenantId; - const authority = getAuthority(tenantId, options.authorityHost); - return { - auth: { - clientId: options.clientId!, - authority, - knownAuthorities: getKnownAuthorities(tenantId, authority, options.disableInstanceDiscovery), - // If the users picked redirect as their login style, - // but they didn't provide a redirectUri, - // we can try to use the current page we're in as a default value. - redirectUri: options.redirectUri || self.location.origin, - }, - }; -} - -/** - * MSAL partial base client for the browsers. - * - * It completes the input configuration with some default values. - * It also provides with utility protected methods that can be used from any of the clients, - * which includes handlers for successful responses and errors. - * - * @internal - */ -export abstract class MsalBrowser implements MsalBrowserFlow { - protected loginStyle: BrowserLoginStyle; - protected clientId: string; - protected tenantId: string; - protected additionallyAllowedTenantIds: string[]; - protected authorityHost?: string; - protected account: AuthenticationRecord | undefined; - protected msalConfig: msalBrowser.Configuration; - protected disableAutomaticAuthentication?: boolean; - protected app?: msalBrowser.IPublicClientApplication; - protected logger: CredentialLogger; - - constructor(options: MsalBrowserFlowOptions) { - this.logger = options.logger; - this.loginStyle = options.loginStyle; - if (!options.clientId) { - throw new CredentialUnavailableError("A client ID is required in browsers"); - } - this.clientId = options.clientId; - this.additionallyAllowedTenantIds = resolveAdditionallyAllowedTenantIds( - options?.tokenCredentialOptions?.additionallyAllowedTenants, - ); - this.tenantId = resolveTenantId(this.logger, options.tenantId, options.clientId); - this.authorityHost = options.authorityHost; - this.msalConfig = defaultBrowserMsalConfig(options); - this.disableAutomaticAuthentication = options.disableAutomaticAuthentication; - - if (options.authenticationRecord) { - this.account = { - ...options.authenticationRecord, - tenantId: this.tenantId, - }; - } - } - - /** - * In the browsers we don't need to init() - */ - async init(): Promise { - // Nothing to do here. - } - - /** - * Attempts to handle a redirection request the least amount of times possible. - */ - public abstract handleRedirect(): Promise; - - /** - * Clears MSAL's cache. - */ - async logout(): Promise { - this.app?.logout(); - } - - /** - * Uses MSAL to retrieve the active account. - */ - public abstract getActiveAccount(): Promise; - - /** - * Uses MSAL to trigger a redirect or a popup login. - */ - public abstract login(scopes?: string | string[]): Promise; - - /** - * Attempts to retrieve a token from cache. - */ - public abstract getTokenSilent(scopes: string[]): Promise; - - /** - * Attempts to retrieve the token in the browser. - */ - protected abstract doGetToken(scopes: string[]): Promise; - - /** - * Attempts to retrieve an authenticated token from MSAL. - */ - public async getToken( - scopes: string[], - options: CredentialFlowGetTokenOptions = {}, - ): Promise { - const tenantId = - processMultiTenantRequest(this.tenantId, options, this.additionallyAllowedTenantIds) || - this.tenantId; - - if (!options.authority) { - options.authority = getAuthority(tenantId, this.authorityHost); - } - - // We ensure that redirection is handled at this point. - await this.handleRedirect(); - - if (!(await this.getActiveAccount()) && !this.disableAutomaticAuthentication) { - await this.login(scopes); - } - return this.getTokenSilent(scopes).catch((err) => { - if (err.name !== "AuthenticationRequiredError") { - throw err; - } - if (options?.disableAutomaticAuthentication) { - throw new AuthenticationRequiredError({ - scopes, - getTokenOptions: options, - message: - "Automatic authentication has been disabled. You may call the authentication() method.", - }); - } - this.logger.info( - `Silent authentication failed, falling back to interactive method ${this.loginStyle}`, - ); - return this.doGetToken(scopes); - }); - } - - /** - * Handles the MSAL authentication result. - * If the result has an account, we update the local account reference. - * If the token received is invalid, an error will be thrown depending on what's missing. - */ - protected handleResult( - scopes: string | string[], - result?: MsalResult, - getTokenOptions?: GetTokenOptions, - ): AccessToken { - if (result?.account) { - this.account = msalToPublic(this.clientId, result.account); - } - ensureValidMsalToken(scopes, result, getTokenOptions); - this.logger.getToken.info(formatSuccess(scopes)); - return { - token: result.accessToken, - expiresOnTimestamp: result.expiresOn.getTime(), - refreshAfterTimestamp: result.refreshOn?.getTime(), - tokenType: "Bearer", - } as AccessToken; - } -} From 2b8dbf48f182f8191942a55a0802066d92819889 Mon Sep 17 00:00:00 2001 From: Minh-Anh Phan Date: Fri, 6 Dec 2024 17:33:45 +0000 Subject: [PATCH 2/6] add client interface --- .../src/msal/browserFlows/msalBrowserCommon.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/sdk/identity/identity/src/msal/browserFlows/msalBrowserCommon.ts b/sdk/identity/identity/src/msal/browserFlows/msalBrowserCommon.ts index a2ca8c645db6..c03b322ace69 100644 --- a/sdk/identity/identity/src/msal/browserFlows/msalBrowserCommon.ts +++ b/sdk/identity/identity/src/msal/browserFlows/msalBrowserCommon.ts @@ -7,6 +7,8 @@ import type { MsalFlow, MsalFlowOptions } from "./flows.js"; import type { BrowserLoginStyle } from "../../credentials/interactiveBrowserCredentialOptions.js"; import type { LogPolicyOptions } from "@azure/core-rest-pipeline"; import type { MultiTenantTokenCredentialOptions } from "../../credentials/multiTenantTokenCredentialOptions.js"; +import { AccessToken } from "@azure/core-auth"; +import type { CredentialFlowGetTokenOptions } from "../credentials.js"; /** * Union of the constructor parameters that all MSAL flow types take. @@ -40,3 +42,15 @@ export interface MsalBrowserFlow extends MsalFlow { login(scopes?: string[]): Promise; handleRedirect(): Promise; } + +/** + * The common methods we use to work with the MSAL browser flows. + * @internal + */ +export interface MsalBrowserClient extends MsalFlow { + getActiveAccount(): Promise; + getToken( + scopes: string[], + options: CredentialFlowGetTokenOptions, + ): Promise; +} From b731ba77b74585c87f7b8120364d6d3be0077192 Mon Sep 17 00:00:00 2001 From: Minh-Anh Phan Date: Fri, 6 Dec 2024 19:19:04 +0000 Subject: [PATCH 3/6] update new client structure --- .../src/msal/browserFlows/msalAuthCode.ts | 312 +++++++++++++++++- .../msal/browserFlows/msalBrowserCommon.ts | 7 +- 2 files changed, 307 insertions(+), 12 deletions(-) diff --git a/sdk/identity/identity/src/msal/browserFlows/msalAuthCode.ts b/sdk/identity/identity/src/msal/browserFlows/msalAuthCode.ts index ef6633e0e4f9..66a05a191c43 100644 --- a/sdk/identity/identity/src/msal/browserFlows/msalAuthCode.ts +++ b/sdk/identity/identity/src/msal/browserFlows/msalAuthCode.ts @@ -3,7 +3,11 @@ import * as msalBrowser from "@azure/msal-browser"; -import type { MsalBrowserFlow, MsalBrowserFlowOptions } from "./msalBrowserCommon.js"; +import type { + MsalBrowserClient, + MsalBrowserFlow, + MsalBrowserFlowOptions, +} from "./msalBrowserCommon.js"; import { defaultLoggerCallback, ensureValidMsalToken, @@ -22,14 +26,18 @@ import type { CredentialFlowGetTokenOptions } from "../credentials.js"; import { getLogLevel } from "@azure/logger"; import { BrowserLoginStyle } from "../../credentials/interactiveBrowserCredentialOptions.js"; import { CredentialLogger, formatSuccess } from "../../util/logging.js"; -import { processMultiTenantRequest, resolveAdditionallyAllowedTenantIds, resolveTenantId } from "../../util/tenantIdUtils.js"; +import { + processMultiTenantRequest, + resolveAdditionallyAllowedTenantIds, + resolveTenantId, +} from "../../util/tenantIdUtils.js"; import { DefaultTenantId } from "../../constants.js"; /** * Generates a MSAL configuration that generally works for browsers * @internal */ -function defaultBrowserMsalConfig( +function generateMsalBrowserConfiguration( options: MsalBrowserFlowOptions, ): msalBrowser.Configuration { const tenantId = options.tenantId || DefaultTenantId; @@ -44,6 +52,17 @@ function defaultBrowserMsalConfig( // we can try to use the current page we're in as a default value. redirectUri: options.redirectUri || self.location.origin, }, + cache: { + cacheLocation: "sessionStorage", + storeAuthStateInCookie: true, // Set to true to improve the experience on IE11 and Edge. + }, + system: { + loggerOptions: { + loggerCallback: defaultLoggerCallback(options.logger, "Browser"), + logLevel: getMSALLogLevel(getLogLevel()), + piiLoggingEnabled: options.loggingOptions?.enableUnsafeSupportLogging, + }, + }, }; } @@ -86,7 +105,7 @@ export class MSALAuthCode implements MsalBrowserFlow { ); this.tenantId = resolveTenantId(this.logger, options.tenantId, options.clientId); this.authorityHost = options.authorityHost; - this.msalConfig = defaultBrowserMsalConfig(options); + this.msalConfig = generateMsalBrowserConfiguration(options); this.disableAutomaticAuthentication = options.disableAutomaticAuthentication; if (options.authenticationRecord) { @@ -126,9 +145,9 @@ export class MSALAuthCode implements MsalBrowserFlow { /** * Clears MSAL's cache. */ - async logout(): Promise { - this.app?.logout(); - } + async logout(): Promise { + this.app?.logout(); + } /** * Attempts to retrieve an authenticated token from MSAL. */ @@ -381,3 +400,282 @@ To work with multiple accounts for the same Client ID and Tenant ID, please prov } } } + +export function createMsalBrowserClient(options: MsalBrowserFlowOptions): MsalBrowserClient { + let loginStyle = options.loginStyle; + if (!options.clientId) { + throw new CredentialUnavailableError("A client ID is required in browsers"); + } + let clientId = options.clientId; + let logger = options.logger; + let tenantId = resolveTenantId(logger, options.tenantId, options.clientId); + let additionallyAllowedTenantIds: string[] = resolveAdditionallyAllowedTenantIds( + options?.tokenCredentialOptions?.additionallyAllowedTenants, + ); + let authorityHost = options.authorityHost; + let msalConfig = generateMsalBrowserConfiguration(options); + let disableAutomaticAuthentication = options.disableAutomaticAuthentication; + let app: msalBrowser.IPublicClientApplication; + let loginHint = options.loginHint; + + let account: AuthenticationRecord | undefined; + if (options.authenticationRecord) { + account = { + ...options.authenticationRecord, + tenantId, + }; + } + + /** + * Return the MSAL account if not set yet + * @returns MSAL application + */ + async function getApp(): Promise { + if (!app) { + // Prepare the MSAL application + app = await msalBrowser.PublicClientApplication.createPublicClientApplication( + msalConfig as msalBrowser.Configuration, + ); + + // setting the account right after the app is created. + if (account) { + app.setActiveAccount(publicToMsal(account)); + } + } + + return app; + } + + /** + * Loads the account based on the result of the authentication. + * If no result was received, tries to load the account from the cache. + * @param result - Result object received from MSAL. + */ + async function handleBrowserResult( + result?: msalBrowser.AuthenticationResult, + ): Promise { + try { + const msalApp = await getApp(); + if (result && result.account) { + logger.info(`MSAL Browser V2 authentication successful.`); + msalApp.setActiveAccount(result.account); + return msalToPublic(clientId, result.account); + } + + // If by this point we happen to have an active account, we should stop trying to parse this. + const activeAccount = msalApp!.getActiveAccount(); + if (activeAccount) { + return msalToPublic(clientId, activeAccount); + } + + // If we don't have an active account, we try to activate it from all the already loaded accounts. + const accounts = app.getAllAccounts(); + if (accounts.length > 1) { + // If there's more than one account in memory, we force the user to authenticate again. + // At this point we can't identify which account should this credential work with, + // since at this point the user won't have provided enough information. + // We log a message in case that helps. + logger.info( + `More than one account was found authenticated for this Client ID and Tenant ID. + However, no "authenticationRecord" has been provided for this credential, + therefore we're unable to pick between these accounts. + A new login attempt will be requested, to ensure the correct account is picked. + To work with multiple accounts for the same Client ID and Tenant ID, please provide an "authenticationRecord" when initializing "InteractiveBrowserCredential".`, + ); + // To safely trigger a new login, we're also ensuring the local cache is cleared up for this MSAL object. + // However, we want to avoid kicking the user out of their authentication on the Azure side. + // We do this by calling to logout while specifying a `onRedirectNavigate` that returns false. + await msalApp.logout({ + onRedirectNavigate: () => false, + }); + return; + } + + // If there's only one account for this MSAL object, we can safely activate it. + if (accounts.length === 1) { + const account = accounts[0]; + msalApp.setActiveAccount(account); + return msalToPublic(clientId, account); + } + + logger.info(`No accounts were found through MSAL.`); + } catch (e: any) { + logger.info(`Failed to acquire token through MSAL. ${e.message}`); + } + return; + } + + /** + * Handles the MSAL authentication result. + * If the result has an account, we update the local account reference. + * If the token received is invalid, an error will be thrown depending on what's missing. + */ + function handleResult( + scopes: string | string[], + result?: MsalResult, + getTokenOptions?: GetTokenOptions, + ): AccessToken { + if (result?.account) { + account = msalToPublic(clientId, result.account); + } + ensureValidMsalToken(scopes, result, getTokenOptions); + logger.getToken.info(formatSuccess(scopes)); + return { + token: result.accessToken, + expiresOnTimestamp: result.expiresOn.getTime(), + refreshAfterTimestamp: result.refreshOn?.getTime(), + tokenType: "Bearer", + } as AccessToken; + } + + /** + * Uses MSAL to handle the redirect. + */ + async function handleRedirect(): Promise { + const msalApp = await getApp(); + return handleBrowserResult((await msalApp.handleRedirectPromise(redirectHash)) || undefined); + } + + /** + * Uses MSAL to retrieve the active account. + */ + async function getActiveAccount(): Promise { + const app = await getApp(); + const account = app.getActiveAccount(); + if (!account) { + return; + } + return msalToPublic(clientId, account); + } + + /** + * Uses MSAL to trigger a redirect or a popup login. + */ + async function login(scopes: string | string[] = []): Promise { + const arrayScopes = Array.isArray(scopes) ? scopes : [scopes]; + const loginRequest: msalBrowser.RedirectRequest = { + scopes: arrayScopes, + loginHint: loginHint, + }; + const msalApp = await getApp(); + switch (loginStyle) { + case "redirect": { + await app.loginRedirect(loginRequest); + return; + } + case "popup": + return handleBrowserResult(await msalApp.loginPopup(loginRequest)); + } + } + + /** + * Attempts to retrieve a token from cache. + */ + async function getTokenSilent( + scopes: string[], + options?: CredentialFlowGetTokenOptions, + ): Promise { + const account = await getActiveAccount(); + if (!account) { + throw new AuthenticationRequiredError({ + scopes, + getTokenOptions: options, + message: + "Silent authentication failed. We couldn't retrieve an active account from the cache.", + }); + } + + const parameters: msalBrowser.SilentRequest = { + authority: options?.authority || msalConfig.auth.authority!, + correlationId: options?.correlationId, + claims: options?.claims, + account: publicToMsal(account), + forceRefresh: false, + scopes, + }; + + try { + logger.info("Attempting to acquire token silently"); + const app = await getApp(); + const response = await app.acquireTokenSilent(parameters); + return handleResult(scopes, response); + } catch (err: any) { + throw handleMsalError(scopes, err, options); + } + } + /** + * Attempts to retrieve the token in the browser. + */ + async function doGetToken( + scopes: string[], + options?: CredentialFlowGetTokenOptions, + ): Promise { + const account = await getActiveAccount(); + if (!account) { + throw new AuthenticationRequiredError({ + scopes, + getTokenOptions: options, + message: + "Silent authentication failed. We couldn't retrieve an active account from the cache.", + }); + } + + const parameters: msalBrowser.RedirectRequest = { + authority: options?.authority || msalConfig.auth.authority!, + correlationId: options?.correlationId, + claims: options?.claims, + account: publicToMsal(account), + loginHint: loginHint, + scopes, + }; + const app = await getApp(); + switch (loginStyle) { + case "redirect": + // This will go out of the page. + // Once the InteractiveBrowserCredential is initialized again, + // we'll load the MSAL account in the constructor. + + await app.acquireTokenRedirect(parameters); + return { token: "", expiresOnTimestamp: 0, tokenType: "Bearer" }; + case "popup": + return handleResult(scopes, await app.acquireTokenPopup(parameters)); + } + } + async function getToken( + scopes: string[], + options: CredentialFlowGetTokenOptions = {}, + ): Promise { + const getTokenTenantId = + processMultiTenantRequest(tenantId, options, additionallyAllowedTenantIds) || tenantId; + + if (!options.authority) { + options.authority = getAuthority(getTokenTenantId, authorityHost); + } + + // We ensure that redirection is handled at this point. + await handleRedirect(); + + if (!(await getActiveAccount()) && !disableAutomaticAuthentication) { + await login(scopes); + } + return getTokenSilent(scopes).catch((err) => { + if (err.name !== "AuthenticationRequiredError") { + throw err; + } + if (options?.disableAutomaticAuthentication) { + throw new AuthenticationRequiredError({ + scopes, + getTokenOptions: options, + message: + "Automatic authentication has been disabled. You may call the authentication() method.", + }); + } + logger.info(`Silent authentication failed, falling back to interactive method ${loginStyle}`); + return doGetToken(scopes); + }); + } + return { + getActiveAccount, + getToken, + }; +} diff --git a/sdk/identity/identity/src/msal/browserFlows/msalBrowserCommon.ts b/sdk/identity/identity/src/msal/browserFlows/msalBrowserCommon.ts index c03b322ace69..f231190c0478 100644 --- a/sdk/identity/identity/src/msal/browserFlows/msalBrowserCommon.ts +++ b/sdk/identity/identity/src/msal/browserFlows/msalBrowserCommon.ts @@ -47,10 +47,7 @@ export interface MsalBrowserFlow extends MsalFlow { * The common methods we use to work with the MSAL browser flows. * @internal */ -export interface MsalBrowserClient extends MsalFlow { +export interface MsalBrowserClient { getActiveAccount(): Promise; - getToken( - scopes: string[], - options: CredentialFlowGetTokenOptions, - ): Promise; + getToken(scopes: string[], options: CredentialFlowGetTokenOptions): Promise; } From 96837901334320730db2d5159e8939b471637d03 Mon Sep 17 00:00:00 2001 From: Minh-Anh Phan Date: Fri, 6 Dec 2024 19:19:23 +0000 Subject: [PATCH 4/6] update comment --- .../src/msal/browserFlows/msalAuthCode.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/sdk/identity/identity/src/msal/browserFlows/msalAuthCode.ts b/sdk/identity/identity/src/msal/browserFlows/msalAuthCode.ts index 66a05a191c43..f4487edfc3ed 100644 --- a/sdk/identity/identity/src/msal/browserFlows/msalAuthCode.ts +++ b/sdk/identity/identity/src/msal/browserFlows/msalAuthCode.ts @@ -447,10 +447,10 @@ export function createMsalBrowserClient(options: MsalBrowserFlowOptions): MsalBr } /** - * Loads the account based on the result of the authentication. - * If no result was received, tries to load the account from the cache. - * @param result - Result object received from MSAL. - */ + * Loads the account based on the result of the authentication. + * If no result was received, tries to load the account from the cache. + * @param result - Result object received from MSAL. + */ async function handleBrowserResult( result?: msalBrowser.AuthenticationResult, ): Promise { @@ -529,8 +529,8 @@ export function createMsalBrowserClient(options: MsalBrowserFlowOptions): MsalBr } /** - * Uses MSAL to handle the redirect. - */ + * Uses MSAL to handle the redirect. + */ async function handleRedirect(): Promise { const msalApp = await getApp(); return handleBrowserResult((await msalApp.handleRedirectPromise(redirectHash)) || undefined); @@ -549,8 +549,8 @@ export function createMsalBrowserClient(options: MsalBrowserFlowOptions): MsalBr } /** - * Uses MSAL to trigger a redirect or a popup login. - */ + * Uses MSAL to trigger a redirect or a popup login. + */ async function login(scopes: string | string[] = []): Promise { const arrayScopes = Array.isArray(scopes) ? scopes : [scopes]; const loginRequest: msalBrowser.RedirectRequest = { From 5461d43b870a55a6fb6f6056d1a2af379cf89ce9 Mon Sep 17 00:00:00 2001 From: Minh-Anh Phan Date: Fri, 6 Dec 2024 21:06:45 +0000 Subject: [PATCH 5/6] remove auth code and merge flow into browser --- .../interactiveBrowserCredential-browser.mts | 9 +- .../identity/src/msal/browserFlows/flows.ts | 46 --- .../src/msal/browserFlows/msalAuthCode.ts | 342 +----------------- .../msal/browserFlows/msalBrowserCommon.ts | 89 ++++- 4 files changed, 85 insertions(+), 401 deletions(-) delete mode 100644 sdk/identity/identity/src/msal/browserFlows/flows.ts diff --git a/sdk/identity/identity/src/credentials/interactiveBrowserCredential-browser.mts b/sdk/identity/identity/src/credentials/interactiveBrowserCredential-browser.mts index 8f0abcaf7822..5310a2f6637a 100644 --- a/sdk/identity/identity/src/credentials/interactiveBrowserCredential-browser.mts +++ b/sdk/identity/identity/src/credentials/interactiveBrowserCredential-browser.mts @@ -13,11 +13,10 @@ import { } from "../util/tenantIdUtils.js"; import type { AuthenticationRecord } from "../msal/types.js"; -import { MSALAuthCode } from "../msal/browserFlows/msalAuthCode.js"; -import type { MsalBrowserFlowOptions } from "../msal/browserFlows/msalBrowserCommon.js"; -import type { MsalFlow } from "../msal/browserFlows/flows.js"; +import type { MsalBrowserClient, MsalBrowserFlowOptions } from "../msal/browserFlows/msalBrowserCommon.js"; import { ensureScopes } from "../util/scopeUtils.js"; import { tracingClient } from "../util/tracing.js"; +import { createMsalBrowserClient } from "../msal/browserFlows/msalAuthCode.js"; const logger = credentialLogger("InteractiveBrowserCredential"); @@ -28,7 +27,7 @@ const logger = credentialLogger("InteractiveBrowserCredential"); export class InteractiveBrowserCredential implements TokenCredential { private tenantId?: string; private additionallyAllowedTenantIds: string[]; - private msalFlow: MsalFlow; + private msalFlow: MsalBrowserClient; private disableAutomaticAuthentication?: boolean; /** @@ -84,7 +83,7 @@ export class InteractiveBrowserCredential implements TokenCredential { typeof options.redirectUri === "function" ? options.redirectUri() : options.redirectUri, }; - this.msalFlow = new MSALAuthCode(msalOptions); + this.msalFlow = createMsalBrowserClient(msalOptions); this.disableAutomaticAuthentication = options?.disableAutomaticAuthentication; } diff --git a/sdk/identity/identity/src/msal/browserFlows/flows.ts b/sdk/identity/identity/src/msal/browserFlows/flows.ts deleted file mode 100644 index a4126711595a..000000000000 --- a/sdk/identity/identity/src/msal/browserFlows/flows.ts +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import type { AccessToken } from "@azure/core-auth"; -import type { AuthenticationRecord } from "../types.js"; -import type { CredentialFlowGetTokenOptions } from "../credentials.js"; -import type { CredentialLogger } from "../../util/logging.js"; - -/** - * Union of the constructor parameters that all MSAL flow types take. - * @internal - */ -export interface MsalFlowOptions { - logger: CredentialLogger; - clientId?: string; - tenantId?: string; - authorityHost?: string; - authenticationRecord?: AuthenticationRecord; - disableAutomaticAuthentication?: boolean; - disableInstanceDiscovery?: boolean; - getAssertion?: () => Promise; - enableMsaPassthrough?: boolean; -} - -/** - * The common methods we use to work with the MSAL flows. - * @internal - */ -export interface MsalFlow { - /** - * Allows for any setup before any request is processed. - */ - init(options?: CredentialFlowGetTokenOptions): Promise; - /** - * Tries to load the active account, either from memory or from MSAL. - */ - getActiveAccount(): Promise; - /** - * Tries to retrieve the token silently using MSAL. - */ - getTokenSilent(scopes?: string[], options?: CredentialFlowGetTokenOptions): Promise; - /** - * Calls to the implementation's doGetToken method. - */ - getToken(scopes?: string[], options?: CredentialFlowGetTokenOptions): Promise; -} diff --git a/sdk/identity/identity/src/msal/browserFlows/msalAuthCode.ts b/sdk/identity/identity/src/msal/browserFlows/msalAuthCode.ts index f4487edfc3ed..c3b9cf7db134 100644 --- a/sdk/identity/identity/src/msal/browserFlows/msalAuthCode.ts +++ b/sdk/identity/identity/src/msal/browserFlows/msalAuthCode.ts @@ -3,11 +3,7 @@ import * as msalBrowser from "@azure/msal-browser"; -import type { - MsalBrowserClient, - MsalBrowserFlow, - MsalBrowserFlowOptions, -} from "./msalBrowserCommon.js"; +import type { MsalBrowserClient, MsalBrowserFlowOptions } from "./msalBrowserCommon.js"; import { defaultLoggerCallback, ensureValidMsalToken, @@ -24,8 +20,7 @@ import type { AuthenticationRecord, MsalResult } from "../types.js"; import { AuthenticationRequiredError, CredentialUnavailableError } from "../../errors.js"; import type { CredentialFlowGetTokenOptions } from "../credentials.js"; import { getLogLevel } from "@azure/logger"; -import { BrowserLoginStyle } from "../../credentials/interactiveBrowserCredentialOptions.js"; -import { CredentialLogger, formatSuccess } from "../../util/logging.js"; +import { formatSuccess } from "../../util/logging.js"; import { processMultiTenantRequest, resolveAdditionallyAllowedTenantIds, @@ -74,333 +69,6 @@ const redirectHash = self.location.hash; * which uses the [Auth Code Flow](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow). * @internal */ -export class MSALAuthCode implements MsalBrowserFlow { - protected loginStyle: BrowserLoginStyle; - protected clientId: string; - protected tenantId: string; - protected additionallyAllowedTenantIds: string[]; - protected authorityHost?: string; - protected account: AuthenticationRecord | undefined; - protected msalConfig: msalBrowser.Configuration; - protected disableAutomaticAuthentication?: boolean; - protected app?: msalBrowser.IPublicClientApplication; - protected logger: CredentialLogger; - private loginHint?: string; - - /** - * Sets up an MSAL object based on the given parameters. - * MSAL with Auth Code allows sending a previously obtained `authenticationRecord` through the optional parameters, - * which is set to be the active account. - * @param options - Parameters necessary and otherwise used to create the MSAL object. - */ - constructor(options: MsalBrowserFlowOptions) { - this.logger = options.logger; - this.loginStyle = options.loginStyle; - if (!options.clientId) { - throw new CredentialUnavailableError("A client ID is required in browsers"); - } - this.clientId = options.clientId; - this.additionallyAllowedTenantIds = resolveAdditionallyAllowedTenantIds( - options?.tokenCredentialOptions?.additionallyAllowedTenants, - ); - this.tenantId = resolveTenantId(this.logger, options.tenantId, options.clientId); - this.authorityHost = options.authorityHost; - this.msalConfig = generateMsalBrowserConfiguration(options); - this.disableAutomaticAuthentication = options.disableAutomaticAuthentication; - - if (options.authenticationRecord) { - this.account = { - ...options.authenticationRecord, - tenantId: this.tenantId, - }; - } - this.loginHint = options.loginHint; - - this.msalConfig.cache = { - cacheLocation: "sessionStorage", - storeAuthStateInCookie: true, // Set to true to improve the experience on IE11 and Edge. - }; - this.msalConfig.system = { - loggerOptions: { - loggerCallback: defaultLoggerCallback(this.logger, "Browser"), - logLevel: getMSALLogLevel(getLogLevel()), - piiLoggingEnabled: options.loggingOptions?.enableUnsafeSupportLogging, - }, - }; - if (options.authenticationRecord) { - this.account = { - ...options.authenticationRecord, - tenantId: this.tenantId, - }; - } - } - - /** - * In the browsers we don't need to init() - */ - async init(): Promise { - // Nothing to do here. - } - - /** - * Clears MSAL's cache. - */ - async logout(): Promise { - this.app?.logout(); - } - /** - * Attempts to retrieve an authenticated token from MSAL. - */ - public async getToken( - scopes: string[], - options: CredentialFlowGetTokenOptions = {}, - ): Promise { - const tenantId = - processMultiTenantRequest(this.tenantId, options, this.additionallyAllowedTenantIds) || - this.tenantId; - - if (!options.authority) { - options.authority = getAuthority(tenantId, this.authorityHost); - } - - // We ensure that redirection is handled at this point. - await this.handleRedirect(); - - if (!(await this.getActiveAccount()) && !this.disableAutomaticAuthentication) { - await this.login(scopes); - } - return this.getTokenSilent(scopes).catch((err) => { - if (err.name !== "AuthenticationRequiredError") { - throw err; - } - if (options?.disableAutomaticAuthentication) { - throw new AuthenticationRequiredError({ - scopes, - getTokenOptions: options, - message: - "Automatic authentication has been disabled. You may call the authentication() method.", - }); - } - this.logger.info( - `Silent authentication failed, falling back to interactive method ${this.loginStyle}`, - ); - return this.doGetToken(scopes); - }); - } - - /** - * Handles the MSAL authentication result. - * If the result has an account, we update the local account reference. - * If the token received is invalid, an error will be thrown depending on what's missing. - */ - protected handleResult( - scopes: string | string[], - result?: MsalResult, - getTokenOptions?: GetTokenOptions, - ): AccessToken { - if (result?.account) { - this.account = msalToPublic(this.clientId, result.account); - } - ensureValidMsalToken(scopes, result, getTokenOptions); - this.logger.getToken.info(formatSuccess(scopes)); - return { - token: result.accessToken, - expiresOnTimestamp: result.expiresOn.getTime(), - refreshAfterTimestamp: result.refreshOn?.getTime(), - tokenType: "Bearer", - } as AccessToken; - } - private async getApp(): Promise { - if (!this.app) { - // Prepare the MSAL application - this.app = await msalBrowser.PublicClientApplication.createPublicClientApplication( - this.msalConfig as msalBrowser.Configuration, - ); - - // setting the account right after the app is created. - if (this.account) { - this.app.setActiveAccount(publicToMsal(this.account)); - } - } - - return this.app; - } - - /** - * Loads the account based on the result of the authentication. - * If no result was received, tries to load the account from the cache. - * @param result - Result object received from MSAL. - */ - private async handleBrowserResult( - result?: msalBrowser.AuthenticationResult, - ): Promise { - try { - const app = await this.getApp(); - if (result && result.account) { - this.logger.info(`MSAL Browser V2 authentication successful.`); - app.setActiveAccount(result.account); - return msalToPublic(this.clientId, result.account); - } - - // If by this point we happen to have an active account, we should stop trying to parse this. - const activeAccount = await this.app!.getActiveAccount(); - if (activeAccount) { - return msalToPublic(this.clientId, activeAccount); - } - - // If we don't have an active account, we try to activate it from all the already loaded accounts. - const accounts = app.getAllAccounts(); - if (accounts.length > 1) { - // If there's more than one account in memory, we force the user to authenticate again. - // At this point we can't identify which account should this credential work with, - // since at this point the user won't have provided enough information. - // We log a message in case that helps. - this.logger.info( - `More than one account was found authenticated for this Client ID and Tenant ID. -However, no "authenticationRecord" has been provided for this credential, -therefore we're unable to pick between these accounts. -A new login attempt will be requested, to ensure the correct account is picked. -To work with multiple accounts for the same Client ID and Tenant ID, please provide an "authenticationRecord" when initializing "InteractiveBrowserCredential".`, - ); - // To safely trigger a new login, we're also ensuring the local cache is cleared up for this MSAL object. - // However, we want to avoid kicking the user out of their authentication on the Azure side. - // We do this by calling to logout while specifying a `onRedirectNavigate` that returns false. - await app.logout({ - onRedirectNavigate: () => false, - }); - return; - } - - // If there's only one account for this MSAL object, we can safely activate it. - if (accounts.length === 1) { - const account = accounts[0]; - app.setActiveAccount(account); - return msalToPublic(this.clientId, account); - } - - this.logger.info(`No accounts were found through MSAL.`); - } catch (e: any) { - this.logger.info(`Failed to acquire token through MSAL. ${e.message}`); - } - return; - } - - /** - * Uses MSAL to handle the redirect. - */ - public async handleRedirect(): Promise { - const app = await this.getApp(); - return this.handleBrowserResult((await app.handleRedirectPromise(redirectHash)) || undefined); - } - - /** - * Uses MSAL to trigger a redirect or a popup login. - */ - public async login(scopes: string | string[] = []): Promise { - const arrayScopes = Array.isArray(scopes) ? scopes : [scopes]; - const loginRequest: msalBrowser.RedirectRequest = { - scopes: arrayScopes, - loginHint: this.loginHint, - }; - const app = await this.getApp(); - switch (this.loginStyle) { - case "redirect": { - await app.loginRedirect(loginRequest); - return; - } - case "popup": - return this.handleBrowserResult(await app.loginPopup(loginRequest)); - } - } - - /** - * Uses MSAL to retrieve the active account. - */ - public async getActiveAccount(): Promise { - const app = await this.getApp(); - const account = app.getActiveAccount(); - if (!account) { - return; - } - return msalToPublic(this.clientId, account); - } - - /** - * Attempts to retrieve a token from cache. - */ - public async getTokenSilent( - scopes: string[], - options?: CredentialFlowGetTokenOptions, - ): Promise { - const account = await this.getActiveAccount(); - if (!account) { - throw new AuthenticationRequiredError({ - scopes, - getTokenOptions: options, - message: - "Silent authentication failed. We couldn't retrieve an active account from the cache.", - }); - } - - const parameters: msalBrowser.SilentRequest = { - authority: options?.authority || this.msalConfig.auth.authority!, - correlationId: options?.correlationId, - claims: options?.claims, - account: publicToMsal(account), - forceRefresh: false, - scopes, - }; - - try { - this.logger.info("Attempting to acquire token silently"); - const app = await this.getApp(); - const response = await app.acquireTokenSilent(parameters); - return this.handleResult(scopes, response); - } catch (err: any) { - throw handleMsalError(scopes, err, options); - } - } - - /** - * Attempts to retrieve the token in the browser. - */ - protected async doGetToken( - scopes: string[], - options?: CredentialFlowGetTokenOptions, - ): Promise { - const account = await this.getActiveAccount(); - if (!account) { - throw new AuthenticationRequiredError({ - scopes, - getTokenOptions: options, - message: - "Silent authentication failed. We couldn't retrieve an active account from the cache.", - }); - } - - const parameters: msalBrowser.RedirectRequest = { - authority: options?.authority || this.msalConfig.auth.authority!, - correlationId: options?.correlationId, - claims: options?.claims, - account: publicToMsal(account), - loginHint: this.loginHint, - scopes, - }; - const app = await this.getApp(); - switch (this.loginStyle) { - case "redirect": - // This will go out of the page. - // Once the InteractiveBrowserCredential is initialized again, - // we'll load the MSAL account in the constructor. - - await app.acquireTokenRedirect(parameters); - return { token: "", expiresOnTimestamp: 0, tokenType: "Bearer" }; - case "popup": - return this.handleResult(scopes, await app.acquireTokenPopup(parameters)); - } - } -} - export function createMsalBrowserClient(options: MsalBrowserFlowOptions): MsalBrowserClient { let loginStyle = options.loginStyle; if (!options.clientId) { @@ -569,7 +237,7 @@ export function createMsalBrowserClient(options: MsalBrowserFlowOptions): MsalBr } /** - * Attempts to retrieve a token from cache. + * Tries to retrieve the token silently using MSAL. */ async function getTokenSilent( scopes: string[], @@ -641,6 +309,10 @@ export function createMsalBrowserClient(options: MsalBrowserFlowOptions): MsalBr return handleResult(scopes, await app.acquireTokenPopup(parameters)); } } + + /** + * Calls to the implementation's doGetToken method. + */ async function getToken( scopes: string[], options: CredentialFlowGetTokenOptions = {}, diff --git a/sdk/identity/identity/src/msal/browserFlows/msalBrowserCommon.ts b/sdk/identity/identity/src/msal/browserFlows/msalBrowserCommon.ts index f231190c0478..55a569e11cce 100644 --- a/sdk/identity/identity/src/msal/browserFlows/msalBrowserCommon.ts +++ b/sdk/identity/identity/src/msal/browserFlows/msalBrowserCommon.ts @@ -2,23 +2,91 @@ // Licensed under the MIT License. import type { AuthenticationRecord } from "../types.js"; -import type { MsalFlow, MsalFlowOptions } from "./flows.js"; - import type { BrowserLoginStyle } from "../../credentials/interactiveBrowserCredentialOptions.js"; import type { LogPolicyOptions } from "@azure/core-rest-pipeline"; import type { MultiTenantTokenCredentialOptions } from "../../credentials/multiTenantTokenCredentialOptions.js"; import { AccessToken } from "@azure/core-auth"; import type { CredentialFlowGetTokenOptions } from "../credentials.js"; +import { CredentialLogger } from "../../util/logging.js"; /** - * Union of the constructor parameters that all MSAL flow types take. - * Some properties might not be used by some flow types. + * Options for the MSAL browser flows. + * @internal */ -export interface MsalBrowserFlowOptions extends MsalFlowOptions { +export interface MsalBrowserFlowOptions { + logger: CredentialLogger; + + /** + * The Client ID of the Microsoft Entra application that users will sign into. + * This parameter is required on the browser. + */ + clientId?: string; + + /** + * The Microsoft Entra tenant (directory) ID. + */ + tenantId?: string; + + /** + * The authority host to use for authentication requests. + * Possible values are available through {@link AzureAuthorityHosts}. + * The default is "https://login.microsoftonline.com". + */ + authorityHost?: string; + + /** + * Result of a previous authentication that can be used to retrieve the cached credentials of each individual account. + * This is necessary to provide in case the application wants to work with more than one account per + * Client ID and Tenant ID pair. + * + * This record can be retrieved by calling to the credential's `authenticate()` method, as follows: + * + * const authenticationRecord = await credential.authenticate(); + * + */ + authenticationRecord?: AuthenticationRecord; + + /** + * Makes getToken throw if a manual authentication is necessary. + * Developers will need to call to `authenticate()` to control when to manually authenticate. + */ + disableAutomaticAuthentication?: boolean; + + /** + * The field determines whether instance discovery is performed when attempting to authenticate. + * Setting this to `true` will completely disable both instance discovery and authority validation. + * As a result, it's crucial to ensure that the configured authority host is valid and trustworthy. + * This functionality is intended for use in scenarios where the metadata endpoint cannot be reached, such as in private clouds or Azure Stack. + * The process of instance discovery entails retrieving authority metadata from https://login.microsoft.com/ to validate the authority. + */ + disableInstanceDiscovery?: boolean; + + /** + * Options for multi-tenant applications which allows for additionally allowed tenants. + */ tokenCredentialOptions: MultiTenantTokenCredentialOptions; + + /** + * Gets the redirect URI of the application. This should be same as the value + * in the application registration portal. Defaults to `window.location.href`. + * This field is no longer required for Node.js. + */ redirectUri?: string; + + /** + * Specifies whether a redirect or a popup window should be used to + * initiate the user authentication flow. Possible values are "redirect" + * or "popup" (default) for browser and "popup" (default) for node. + * + */ loginStyle: BrowserLoginStyle; + + /** + * loginHint allows a user name to be pre-selected for interactive logins. + * Setting this option skips the account selection prompt and immediately attempts to login with the specified account. + */ loginHint?: string; + /** * Allows users to configure settings for logging policy options, allow logging account information and personally identifiable information for customer support. */ @@ -35,16 +103,7 @@ export interface MsalBrowserFlowOptions extends MsalFlowOptions { } /** - * The common methods we use to work with the MSAL browser flows. - * @internal - */ -export interface MsalBrowserFlow extends MsalFlow { - login(scopes?: string[]): Promise; - handleRedirect(): Promise; -} - -/** - * The common methods we use to work with the MSAL browser flows. + * Methods that are used by InteractiveBrowserCredential * @internal */ export interface MsalBrowserClient { From 49ba880a4a92bfb5d849e83e2bf299872d5b86dc Mon Sep 17 00:00:00 2001 From: Minh-Anh Phan Date: Thu, 19 Dec 2024 14:21:45 +0000 Subject: [PATCH 6/6] update format & lint --- .../src/msal/browserFlows/msalAuthCode.ts | 95 ++++++++++--------- 1 file changed, 48 insertions(+), 47 deletions(-) diff --git a/sdk/identity/identity/src/msal/browserFlows/msalAuthCode.ts b/sdk/identity/identity/src/msal/browserFlows/msalAuthCode.ts index c3b9cf7db134..b78a73c896ee 100644 --- a/sdk/identity/identity/src/msal/browserFlows/msalAuthCode.ts +++ b/sdk/identity/identity/src/msal/browserFlows/msalAuthCode.ts @@ -70,21 +70,21 @@ const redirectHash = self.location.hash; * @internal */ export function createMsalBrowserClient(options: MsalBrowserFlowOptions): MsalBrowserClient { - let loginStyle = options.loginStyle; + const loginStyle = options.loginStyle; if (!options.clientId) { throw new CredentialUnavailableError("A client ID is required in browsers"); } - let clientId = options.clientId; - let logger = options.logger; - let tenantId = resolveTenantId(logger, options.tenantId, options.clientId); - let additionallyAllowedTenantIds: string[] = resolveAdditionallyAllowedTenantIds( + const clientId = options.clientId; + const logger = options.logger; + const tenantId = resolveTenantId(logger, options.tenantId, options.clientId); + const additionallyAllowedTenantIds: string[] = resolveAdditionallyAllowedTenantIds( options?.tokenCredentialOptions?.additionallyAllowedTenants, ); - let authorityHost = options.authorityHost; - let msalConfig = generateMsalBrowserConfiguration(options); - let disableAutomaticAuthentication = options.disableAutomaticAuthentication; + const authorityHost = options.authorityHost; + const msalConfig = generateMsalBrowserConfiguration(options); + const disableAutomaticAuthentication = options.disableAutomaticAuthentication; let app: msalBrowser.IPublicClientApplication; - let loginHint = options.loginHint; + const loginHint = options.loginHint; let account: AuthenticationRecord | undefined; if (options.authenticationRecord) { @@ -137,8 +137,8 @@ export function createMsalBrowserClient(options: MsalBrowserFlowOptions): MsalBr } // If we don't have an active account, we try to activate it from all the already loaded accounts. - const accounts = app.getAllAccounts(); - if (accounts.length > 1) { + const allAccounts = app.getAllAccounts(); + if (allAccounts.length > 1) { // If there's more than one account in memory, we force the user to authenticate again. // At this point we can't identify which account should this credential work with, // since at this point the user won't have provided enough information. @@ -160,10 +160,10 @@ export function createMsalBrowserClient(options: MsalBrowserFlowOptions): MsalBr } // If there's only one account for this MSAL object, we can safely activate it. - if (accounts.length === 1) { - const account = accounts[0]; - msalApp.setActiveAccount(account); - return msalToPublic(clientId, account); + if (allAccounts.length === 1) { + const msalAccount = allAccounts[0]; + msalApp.setActiveAccount(msalAccount); + return msalToPublic(clientId, msalAccount); } logger.info(`No accounts were found through MSAL.`); @@ -208,12 +208,12 @@ export function createMsalBrowserClient(options: MsalBrowserFlowOptions): MsalBr * Uses MSAL to retrieve the active account. */ async function getActiveAccount(): Promise { - const app = await getApp(); - const account = app.getActiveAccount(); - if (!account) { + const msalApp = await getApp(); + const activeAccount = msalApp.getActiveAccount(); + if (!activeAccount) { return; } - return msalToPublic(clientId, account); + return msalToPublic(clientId, activeAccount); } /** @@ -241,31 +241,31 @@ export function createMsalBrowserClient(options: MsalBrowserFlowOptions): MsalBr */ async function getTokenSilent( scopes: string[], - options?: CredentialFlowGetTokenOptions, + getTokenOptions?: CredentialFlowGetTokenOptions, ): Promise { - const account = await getActiveAccount(); - if (!account) { + const activeAccount = await getActiveAccount(); + if (!activeAccount) { throw new AuthenticationRequiredError({ scopes, - getTokenOptions: options, + getTokenOptions, message: "Silent authentication failed. We couldn't retrieve an active account from the cache.", }); } const parameters: msalBrowser.SilentRequest = { - authority: options?.authority || msalConfig.auth.authority!, - correlationId: options?.correlationId, - claims: options?.claims, - account: publicToMsal(account), + authority: getTokenOptions?.authority || msalConfig.auth.authority!, + correlationId: getTokenOptions?.correlationId, + claims: getTokenOptions?.claims, + account: publicToMsal(activeAccount), forceRefresh: false, scopes, }; try { logger.info("Attempting to acquire token silently"); - const app = await getApp(); - const response = await app.acquireTokenSilent(parameters); + const msalApp = await getApp(); + const response = await msalApp.acquireTokenSilent(parameters); return handleResult(scopes, response); } catch (err: any) { throw handleMsalError(scopes, err, options); @@ -276,34 +276,34 @@ export function createMsalBrowserClient(options: MsalBrowserFlowOptions): MsalBr */ async function doGetToken( scopes: string[], - options?: CredentialFlowGetTokenOptions, + getTokenOptions?: CredentialFlowGetTokenOptions, ): Promise { - const account = await getActiveAccount(); - if (!account) { + const activeAccount = await getActiveAccount(); + if (!activeAccount) { throw new AuthenticationRequiredError({ scopes, - getTokenOptions: options, + getTokenOptions, message: "Silent authentication failed. We couldn't retrieve an active account from the cache.", }); } const parameters: msalBrowser.RedirectRequest = { - authority: options?.authority || msalConfig.auth.authority!, - correlationId: options?.correlationId, - claims: options?.claims, - account: publicToMsal(account), + authority: getTokenOptions?.authority || msalConfig.auth.authority!, + correlationId: getTokenOptions?.correlationId, + claims: getTokenOptions?.claims, + account: publicToMsal(activeAccount), loginHint: loginHint, scopes, }; - const app = await getApp(); + const msalApp = await getApp(); switch (loginStyle) { case "redirect": // This will go out of the page. // Once the InteractiveBrowserCredential is initialized again, // we'll load the MSAL account in the constructor. - await app.acquireTokenRedirect(parameters); + await msalApp.acquireTokenRedirect(parameters); return { token: "", expiresOnTimestamp: 0, tokenType: "Bearer" }; case "popup": return handleResult(scopes, await app.acquireTokenPopup(parameters)); @@ -315,13 +315,14 @@ export function createMsalBrowserClient(options: MsalBrowserFlowOptions): MsalBr */ async function getToken( scopes: string[], - options: CredentialFlowGetTokenOptions = {}, + getTokenOptions: CredentialFlowGetTokenOptions = {}, ): Promise { const getTokenTenantId = - processMultiTenantRequest(tenantId, options, additionallyAllowedTenantIds) || tenantId; + processMultiTenantRequest(tenantId, getTokenOptions, additionallyAllowedTenantIds) || + tenantId; - if (!options.authority) { - options.authority = getAuthority(getTokenTenantId, authorityHost); + if (!getTokenOptions.authority) { + getTokenOptions.authority = getAuthority(getTokenTenantId, authorityHost); } // We ensure that redirection is handled at this point. @@ -330,20 +331,20 @@ export function createMsalBrowserClient(options: MsalBrowserFlowOptions): MsalBr if (!(await getActiveAccount()) && !disableAutomaticAuthentication) { await login(scopes); } - return getTokenSilent(scopes).catch((err) => { + return getTokenSilent(scopes, getTokenOptions).catch((err) => { if (err.name !== "AuthenticationRequiredError") { throw err; } - if (options?.disableAutomaticAuthentication) { + if (getTokenOptions?.disableAutomaticAuthentication) { throw new AuthenticationRequiredError({ scopes, - getTokenOptions: options, + getTokenOptions, message: "Automatic authentication has been disabled. You may call the authentication() method.", }); } logger.info(`Silent authentication failed, falling back to interactive method ${loginStyle}`); - return doGetToken(scopes); + return doGetToken(scopes, getTokenOptions); }); } return {