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 32976ca6c306..b78a73c896ee 100644 --- a/sdk/identity/identity/src/msal/browserFlows/msalAuthCode.ts +++ b/sdk/identity/identity/src/msal/browserFlows/msalAuthCode.ts @@ -3,76 +3,115 @@ import * as msalBrowser from "@azure/msal-browser"; -import type { MsalBrowserFlowOptions } from "./msalBrowserCommon.js"; -import { MsalBrowser } from "./msalBrowserCommon.js"; +import type { MsalBrowserClient, 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"; - -// We keep a copy of the redirect hash. -const redirectHash = self.location.hash; +import { formatSuccess } from "../../util/logging.js"; +import { + processMultiTenantRequest, + resolveAdditionallyAllowedTenantIds, + resolveTenantId, +} from "../../util/tenantIdUtils.js"; +import { DefaultTenantId } from "../../constants.js"; /** - * Uses MSAL Browser 2.X for browser authentication, - * which uses the [Auth Code Flow](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow). + * Generates a MSAL configuration that generally works for browsers * @internal */ -export class MSALAuthCode extends MsalBrowser { - 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) { - super(options); - this.loginHint = options.loginHint; - - this.msalConfig.cache = { +function generateMsalBrowserConfiguration( + 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, + }, + cache: { cacheLocation: "sessionStorage", storeAuthStateInCookie: true, // Set to true to improve the experience on IE11 and Edge. - }; - this.msalConfig.system = { + }, + system: { loggerOptions: { - loggerCallback: defaultLoggerCallback(this.logger, "Browser"), + loggerCallback: defaultLoggerCallback(options.logger, "Browser"), logLevel: getMSALLogLevel(getLogLevel()), piiLoggingEnabled: options.loggingOptions?.enableUnsafeSupportLogging, }, + }, + }; +} + +// We keep a copy of the redirect hash. +const redirectHash = self.location.hash; + +/** + * Uses MSAL Browser 2.X for browser authentication, + * which uses the [Auth Code Flow](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow). + * @internal + */ +export function createMsalBrowserClient(options: MsalBrowserFlowOptions): MsalBrowserClient { + const loginStyle = options.loginStyle; + if (!options.clientId) { + throw new CredentialUnavailableError("A client ID is required in browsers"); + } + const clientId = options.clientId; + const logger = options.logger; + const tenantId = resolveTenantId(logger, options.tenantId, options.clientId); + const additionallyAllowedTenantIds: string[] = resolveAdditionallyAllowedTenantIds( + options?.tokenCredentialOptions?.additionallyAllowedTenants, + ); + const authorityHost = options.authorityHost; + const msalConfig = generateMsalBrowserConfiguration(options); + const disableAutomaticAuthentication = options.disableAutomaticAuthentication; + let app: msalBrowser.IPublicClientApplication; + const loginHint = options.loginHint; + + let account: AuthenticationRecord | undefined; + if (options.authenticationRecord) { + account = { + ...options.authenticationRecord, + tenantId, }; - if (options.authenticationRecord) { - this.account = { - ...options.authenticationRecord, - tenantId: this.tenantId, - }; - } } - private async getApp(): Promise { - if (!this.app) { + /** + * Return the MSAL account if not set yet + * @returns MSAL application + */ + async function getApp(): Promise { + if (!app) { // Prepare the MSAL application - this.app = await msalBrowser.PublicClientApplication.createPublicClientApplication( - this.msalConfig as msalBrowser.Configuration, + app = await msalBrowser.PublicClientApplication.createPublicClientApplication( + msalConfig as msalBrowser.Configuration, ); // setting the account right after the app is created. - if (this.account) { - this.app.setActiveAccount(publicToMsal(this.account)); + if (account) { + app.setActiveAccount(publicToMsal(account)); } } - return this.app; + return app; } /** @@ -80,172 +119,236 @@ export class MSALAuthCode extends MsalBrowser { * If no result was received, tries to load the account from the cache. * @param result - Result object received from MSAL. */ - private async handleBrowserResult( + async function handleBrowserResult( result?: msalBrowser.AuthenticationResult, ): Promise { try { - const app = await this.getApp(); + const msalApp = await getApp(); if (result && result.account) { - this.logger.info(`MSAL Browser V2 authentication successful.`); - app.setActiveAccount(result.account); - return msalToPublic(this.clientId, 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 = await this.app!.getActiveAccount(); + const activeAccount = msalApp!.getActiveAccount(); if (activeAccount) { - return msalToPublic(this.clientId, 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) { + 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. // We log a message in case that helps. - this.logger.info( + 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".`, + 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({ + 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]; - app.setActiveAccount(account); - return msalToPublic(this.clientId, account); + if (allAccounts.length === 1) { + const msalAccount = allAccounts[0]; + msalApp.setActiveAccount(msalAccount); + return msalToPublic(clientId, msalAccount); } - this.logger.info(`No accounts were found through MSAL.`); + logger.info(`No accounts were found through MSAL.`); } catch (e: any) { - this.logger.info(`Failed to acquire token through MSAL. ${e.message}`); + 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. */ - public async handleRedirect(): Promise { - const app = await this.getApp(); - return this.handleBrowserResult((await app.handleRedirectPromise(redirectHash)) || undefined); + 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 msalApp = await getApp(); + const activeAccount = msalApp.getActiveAccount(); + if (!activeAccount) { + return; + } + return msalToPublic(clientId, activeAccount); } /** * Uses MSAL to trigger a redirect or a popup login. */ - public async login(scopes: string | string[] = []): Promise { + async function login(scopes: string | string[] = []): Promise { const arrayScopes = Array.isArray(scopes) ? scopes : [scopes]; const loginRequest: msalBrowser.RedirectRequest = { scopes: arrayScopes, - loginHint: this.loginHint, + loginHint: loginHint, }; - const app = await this.getApp(); - switch (this.loginStyle) { + const msalApp = await getApp(); + switch (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 handleBrowserResult(await msalApp.loginPopup(loginRequest)); } - return msalToPublic(this.clientId, account); } /** - * Attempts to retrieve a token from cache. + * Tries to retrieve the token silently using MSAL. */ - public async getTokenSilent( + async function getTokenSilent( scopes: string[], - options?: CredentialFlowGetTokenOptions, + getTokenOptions?: CredentialFlowGetTokenOptions, ): Promise { - const account = await this.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 || this.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 { - this.logger.info("Attempting to acquire token silently"); - const app = await this.getApp(); - const response = await app.acquireTokenSilent(parameters); - return this.handleResult(scopes, response); + logger.info("Attempting to acquire token silently"); + const msalApp = await getApp(); + const response = await msalApp.acquireTokenSilent(parameters); + return handleResult(scopes, response); } catch (err: any) { throw handleMsalError(scopes, err, options); } } - /** * Attempts to retrieve the token in the browser. */ - protected async doGetToken( + async function doGetToken( scopes: string[], - options?: CredentialFlowGetTokenOptions, + getTokenOptions?: CredentialFlowGetTokenOptions, ): Promise { - const account = await this.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 || this.msalConfig.auth.authority!, - correlationId: options?.correlationId, - claims: options?.claims, - account: publicToMsal(account), - loginHint: this.loginHint, + authority: getTokenOptions?.authority || msalConfig.auth.authority!, + correlationId: getTokenOptions?.correlationId, + claims: getTokenOptions?.claims, + account: publicToMsal(activeAccount), + loginHint: loginHint, scopes, }; - const app = await this.getApp(); - switch (this.loginStyle) { + 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 this.handleResult(scopes, await app.acquireTokenPopup(parameters)); + return handleResult(scopes, await app.acquireTokenPopup(parameters)); } } + + /** + * Calls to the implementation's doGetToken method. + */ + async function getToken( + scopes: string[], + getTokenOptions: CredentialFlowGetTokenOptions = {}, + ): Promise { + const getTokenTenantId = + processMultiTenantRequest(tenantId, getTokenOptions, additionallyAllowedTenantIds) || + tenantId; + + if (!getTokenOptions.authority) { + getTokenOptions.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, getTokenOptions).catch((err) => { + if (err.name !== "AuthenticationRequiredError") { + throw err; + } + if (getTokenOptions?.disableAutomaticAuthentication) { + throw new AuthenticationRequiredError({ + scopes, + 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, getTokenOptions); + }); + } + return { + getActiveAccount, + getToken, + }; } diff --git a/sdk/identity/identity/src/msal/browserFlows/msalBrowserCommon.ts b/sdk/identity/identity/src/msal/browserFlows/msalBrowserCommon.ts index c7916b2a7787..55a569e11cce 100644 --- a/sdk/identity/identity/src/msal/browserFlows/msalBrowserCommon.ts +++ b/sdk/identity/identity/src/msal/browserFlows/msalBrowserCommon.ts @@ -1,225 +1,112 @@ // 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 { MsalFlow, MsalFlowOptions } from "./flows.js"; -import { ensureValidMsalToken, getAuthority, getKnownAuthorities, msalToPublic } from "../utils.js"; -import { - processMultiTenantRequest, - resolveAdditionallyAllowedTenantIds, - resolveTenantId, -} from "../../util/tenantIdUtils.js"; - +import type { AuthenticationRecord } from "../types.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"; +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. - */ -export interface MsalBrowserFlowOptions extends MsalFlowOptions { - tokenCredentialOptions: MultiTenantTokenCredentialOptions; - redirectUri?: string; - loginStyle: BrowserLoginStyle; - loginHint?: string; - /** - * Allows users to configure settings for logging policy options, allow logging account information and personally identifiable information for customer support. - */ - loggingOptions?: LogPolicyOptions & { - /** - * Allows logging account information once the authentication flow succeeds. - */ - allowLoggingAccountIdentifiers?: boolean; - /** - * Allows logging personally identifiable information for customer support. - */ - enableUnsafeSupportLogging?: boolean; - }; -} - -/** - * The common methods we use to work with the MSAL browser flows. - * @internal - */ -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. - * + * Options for the MSAL browser flows. * @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, - }; - } - } +export interface MsalBrowserFlowOptions { + logger: CredentialLogger; /** - * In the browsers we don't need to init() + * The Client ID of the Microsoft Entra application that users will sign into. + * This parameter is required on the browser. */ - async init(): Promise { - // Nothing to do here. - } + clientId?: string; /** - * Attempts to handle a redirection request the least amount of times possible. + * The Microsoft Entra tenant (directory) ID. */ - public abstract handleRedirect(): Promise; + tenantId?: string; /** - * Clears MSAL's cache. + * The authority host to use for authentication requests. + * Possible values are available through {@link AzureAuthorityHosts}. + * The default is "https://login.microsoftonline.com". */ - async logout(): Promise { - this.app?.logout(); - } + authorityHost?: string; /** - * Uses MSAL to retrieve the active account. + * 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(); + * */ - public abstract getActiveAccount(): Promise; + authenticationRecord?: AuthenticationRecord; /** - * Uses MSAL to trigger a redirect or a popup login. + * Makes getToken throw if a manual authentication is necessary. + * Developers will need to call to `authenticate()` to control when to manually authenticate. */ - public abstract login(scopes?: string | string[]): Promise; + disableAutomaticAuthentication?: boolean; /** - * Attempts to retrieve a token from cache. + * 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. */ - public abstract getTokenSilent(scopes: string[]): Promise; + disableInstanceDiscovery?: boolean; /** - * Attempts to retrieve the token in the browser. + * Options for multi-tenant applications which allows for additionally allowed tenants. */ - protected abstract doGetToken(scopes: string[]): Promise; + tokenCredentialOptions: MultiTenantTokenCredentialOptions; /** - * Attempts to retrieve an authenticated token from MSAL. + * 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. */ - 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); - } + redirectUri?: string; - // We ensure that redirection is handled at this point. - await this.handleRedirect(); + /** + * 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; - 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); - }); - } + /** + * 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; /** - * 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. + * Allows users to configure settings for logging policy options, allow logging account information and personally identifiable information for customer support. */ - 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; - } + loggingOptions?: LogPolicyOptions & { + /** + * Allows logging account information once the authentication flow succeeds. + */ + allowLoggingAccountIdentifiers?: boolean; + /** + * Allows logging personally identifiable information for customer support. + */ + enableUnsafeSupportLogging?: boolean; + }; +} + +/** + * Methods that are used by InteractiveBrowserCredential + * @internal + */ +export interface MsalBrowserClient { + getActiveAccount(): Promise; + getToken(scopes: string[], options: CredentialFlowGetTokenOptions): Promise; }