489 lines
20 KiB
JavaScript
489 lines
20 KiB
JavaScript
|
/*! @azure/msal-browser v3.21.0 2024-08-13 */
|
||
|
'use strict';
|
||
|
import { DEFAULT_CRYPTO_IMPLEMENTATION, buildStaticAuthorityOptions, PerformanceEvents, TimeUtils, OIDC_DEFAULT_SCOPES, AccountEntity } from '@azure/msal-common';
|
||
|
import { InteractionType, DEFAULT_REQUEST } from '../utils/BrowserConstants.mjs';
|
||
|
import { CryptoOps } from '../crypto/CryptoOps.mjs';
|
||
|
import { NestedAppAuthAdapter } from '../naa/mapping/NestedAppAuthAdapter.mjs';
|
||
|
import { NestedAppAuthError } from '../error/NestedAppAuthError.mjs';
|
||
|
import { EventHandler } from '../event/EventHandler.mjs';
|
||
|
import { EventType } from '../event/EventType.mjs';
|
||
|
import { BrowserCacheManager, DEFAULT_BROWSER_CACHE_MANAGER } from '../cache/BrowserCacheManager.mjs';
|
||
|
import { getAccount, setActiveAccount, getAllAccounts, getAccountByUsername, getAccountByHomeId, getAccountByLocalId, getActiveAccount } from '../cache/AccountManager.mjs';
|
||
|
|
||
|
/*
|
||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||
|
* Licensed under the MIT License.
|
||
|
*/
|
||
|
class NestedAppAuthController {
|
||
|
constructor(operatingContext) {
|
||
|
this.operatingContext = operatingContext;
|
||
|
const proxy = this.operatingContext.getBridgeProxy();
|
||
|
if (proxy !== undefined) {
|
||
|
this.bridgeProxy = proxy;
|
||
|
}
|
||
|
else {
|
||
|
throw new Error("unexpected: bridgeProxy is undefined");
|
||
|
}
|
||
|
// Set the configuration.
|
||
|
this.config = operatingContext.getConfig();
|
||
|
// Initialize logger
|
||
|
this.logger = this.operatingContext.getLogger();
|
||
|
// Initialize performance client
|
||
|
this.performanceClient = this.config.telemetry.client;
|
||
|
// Initialize the crypto class.
|
||
|
this.browserCrypto = operatingContext.isBrowserEnvironment()
|
||
|
? new CryptoOps(this.logger, this.performanceClient)
|
||
|
: DEFAULT_CRYPTO_IMPLEMENTATION;
|
||
|
// Initialize the browser storage class.
|
||
|
this.browserStorage = this.operatingContext.isBrowserEnvironment()
|
||
|
? new BrowserCacheManager(this.config.auth.clientId, this.config.cache, this.browserCrypto, this.logger, buildStaticAuthorityOptions(this.config.auth))
|
||
|
: DEFAULT_BROWSER_CACHE_MANAGER(this.config.auth.clientId, this.logger);
|
||
|
this.eventHandler = new EventHandler(this.logger, this.browserCrypto);
|
||
|
this.nestedAppAuthAdapter = new NestedAppAuthAdapter(this.config.auth.clientId, this.config.auth.clientCapabilities, this.browserCrypto, this.logger);
|
||
|
// Set the active account if available
|
||
|
const accountContext = this.bridgeProxy.getAccountContext();
|
||
|
if (accountContext) {
|
||
|
const cachedAccount = getAccount(accountContext, this.logger, this.browserStorage);
|
||
|
setActiveAccount(cachedAccount, this.browserStorage);
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* Returns the event handler instance
|
||
|
* @returns EventHandler
|
||
|
*/
|
||
|
getEventHandler() {
|
||
|
return this.eventHandler;
|
||
|
}
|
||
|
/**
|
||
|
* Factory function to create a new instance of NestedAppAuthController
|
||
|
* @param operatingContext
|
||
|
* @returns Promise<IController>
|
||
|
*/
|
||
|
static async createController(operatingContext) {
|
||
|
const controller = new NestedAppAuthController(operatingContext);
|
||
|
return Promise.resolve(controller);
|
||
|
}
|
||
|
/**
|
||
|
* Specific implementation of initialize function for NestedAppAuthController
|
||
|
* @returns
|
||
|
*/
|
||
|
initialize() {
|
||
|
// do nothing not required by this controller
|
||
|
return Promise.resolve();
|
||
|
}
|
||
|
/**
|
||
|
* Validate the incoming request and add correlationId if not present
|
||
|
* @param request
|
||
|
* @returns
|
||
|
*/
|
||
|
ensureValidRequest(request) {
|
||
|
if (request?.correlationId) {
|
||
|
return request;
|
||
|
}
|
||
|
return {
|
||
|
...request,
|
||
|
correlationId: this.browserCrypto.createNewGuid(),
|
||
|
};
|
||
|
}
|
||
|
/**
|
||
|
* Internal implementation of acquireTokenInteractive flow
|
||
|
* @param request
|
||
|
* @returns
|
||
|
*/
|
||
|
async acquireTokenInteractive(request) {
|
||
|
const validRequest = this.ensureValidRequest(request);
|
||
|
this.eventHandler.emitEvent(EventType.ACQUIRE_TOKEN_START, InteractionType.Popup, validRequest);
|
||
|
const atPopupMeasurement = this.performanceClient.startMeasurement(PerformanceEvents.AcquireTokenPopup, validRequest.correlationId);
|
||
|
atPopupMeasurement?.add({ nestedAppAuthRequest: true });
|
||
|
try {
|
||
|
const naaRequest = this.nestedAppAuthAdapter.toNaaTokenRequest(validRequest);
|
||
|
const reqTimestamp = TimeUtils.nowSeconds();
|
||
|
const response = await this.bridgeProxy.getTokenInteractive(naaRequest);
|
||
|
const result = this.nestedAppAuthAdapter.fromNaaTokenResponse(naaRequest, response, reqTimestamp);
|
||
|
// cache the tokens in the response
|
||
|
await this.hydrateCache(result, request);
|
||
|
this.browserStorage.setActiveAccount(result.account);
|
||
|
this.eventHandler.emitEvent(EventType.ACQUIRE_TOKEN_SUCCESS, InteractionType.Popup, result);
|
||
|
atPopupMeasurement.add({
|
||
|
accessTokenSize: result.accessToken.length,
|
||
|
idTokenSize: result.idToken.length,
|
||
|
});
|
||
|
atPopupMeasurement.end({
|
||
|
success: true,
|
||
|
requestId: result.requestId,
|
||
|
});
|
||
|
return result;
|
||
|
}
|
||
|
catch (e) {
|
||
|
const error = this.nestedAppAuthAdapter.fromBridgeError(e);
|
||
|
this.eventHandler.emitEvent(EventType.ACQUIRE_TOKEN_FAILURE, InteractionType.Popup, null, e);
|
||
|
atPopupMeasurement.end({
|
||
|
success: false,
|
||
|
}, e);
|
||
|
throw error;
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* Internal implementation of acquireTokenSilent flow
|
||
|
* @param request
|
||
|
* @returns
|
||
|
*/
|
||
|
async acquireTokenSilentInternal(request) {
|
||
|
const validRequest = this.ensureValidRequest(request);
|
||
|
this.eventHandler.emitEvent(EventType.ACQUIRE_TOKEN_START, InteractionType.Silent, validRequest);
|
||
|
// Look for tokens in the cache first
|
||
|
const result = await this.acquireTokenFromCache(validRequest);
|
||
|
if (result) {
|
||
|
this.eventHandler.emitEvent(EventType.ACQUIRE_TOKEN_SUCCESS, InteractionType.Silent, result);
|
||
|
return result;
|
||
|
}
|
||
|
// proceed with acquiring tokens via the host
|
||
|
const ssoSilentMeasurement = this.performanceClient.startMeasurement(PerformanceEvents.SsoSilent, validRequest.correlationId);
|
||
|
ssoSilentMeasurement?.increment({
|
||
|
visibilityChangeCount: 0,
|
||
|
});
|
||
|
ssoSilentMeasurement?.add({
|
||
|
nestedAppAuthRequest: true,
|
||
|
});
|
||
|
try {
|
||
|
const naaRequest = this.nestedAppAuthAdapter.toNaaTokenRequest(validRequest);
|
||
|
const reqTimestamp = TimeUtils.nowSeconds();
|
||
|
const response = await this.bridgeProxy.getTokenSilent(naaRequest);
|
||
|
const result = this.nestedAppAuthAdapter.fromNaaTokenResponse(naaRequest, response, reqTimestamp);
|
||
|
// cache the tokens in the response
|
||
|
await this.hydrateCache(result, request);
|
||
|
this.browserStorage.setActiveAccount(result.account);
|
||
|
this.eventHandler.emitEvent(EventType.ACQUIRE_TOKEN_SUCCESS, InteractionType.Silent, result);
|
||
|
ssoSilentMeasurement?.add({
|
||
|
accessTokenSize: result.accessToken.length,
|
||
|
idTokenSize: result.idToken.length,
|
||
|
});
|
||
|
ssoSilentMeasurement?.end({
|
||
|
success: true,
|
||
|
requestId: result.requestId,
|
||
|
});
|
||
|
return result;
|
||
|
}
|
||
|
catch (e) {
|
||
|
const error = this.nestedAppAuthAdapter.fromBridgeError(e);
|
||
|
this.eventHandler.emitEvent(EventType.ACQUIRE_TOKEN_FAILURE, InteractionType.Silent, null, e);
|
||
|
ssoSilentMeasurement?.end({
|
||
|
success: false,
|
||
|
}, e);
|
||
|
throw error;
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* acquires tokens from cache
|
||
|
* @param request
|
||
|
* @returns
|
||
|
*/
|
||
|
async acquireTokenFromCache(request) {
|
||
|
const atsMeasurement = this.performanceClient.startMeasurement(PerformanceEvents.AcquireTokenSilent, request.correlationId);
|
||
|
atsMeasurement?.add({
|
||
|
nestedAppAuthRequest: true,
|
||
|
});
|
||
|
const result = await this.acquireTokenFromCacheInternal(request);
|
||
|
if (result) {
|
||
|
this.eventHandler.emitEvent(EventType.ACQUIRE_TOKEN_SUCCESS, InteractionType.Silent, result);
|
||
|
atsMeasurement?.add({
|
||
|
accessTokenSize: result?.accessToken.length,
|
||
|
idTokenSize: result?.idToken.length,
|
||
|
});
|
||
|
atsMeasurement?.end({
|
||
|
success: true,
|
||
|
});
|
||
|
return result;
|
||
|
}
|
||
|
this.logger.error("Cached tokens are not found for the account, proceeding with silent token request.");
|
||
|
this.eventHandler.emitEvent(EventType.ACQUIRE_TOKEN_FAILURE, InteractionType.Silent, null);
|
||
|
atsMeasurement?.end({
|
||
|
success: false,
|
||
|
});
|
||
|
return null;
|
||
|
}
|
||
|
/**
|
||
|
*
|
||
|
* @param request
|
||
|
* @returns
|
||
|
*/
|
||
|
async acquireTokenFromCacheInternal(request) {
|
||
|
const accountContext = this.bridgeProxy.getAccountContext();
|
||
|
let currentAccount = null;
|
||
|
if (accountContext) {
|
||
|
const hubAccount = getAccount(accountContext, this.logger, this.browserStorage);
|
||
|
// always prioritize for hub account context, the reqirement of `request.account` will be removed soon
|
||
|
currentAccount = hubAccount || request.account;
|
||
|
}
|
||
|
// fall back to brokering if no cached account is found
|
||
|
if (!currentAccount) {
|
||
|
this.logger.verbose("No active account found, falling back to the host");
|
||
|
return Promise.resolve(null);
|
||
|
}
|
||
|
this.logger.verbose("active account found, attempting to acquire token silently");
|
||
|
const authRequest = {
|
||
|
...request,
|
||
|
correlationId: request.correlationId || this.browserCrypto.createNewGuid(),
|
||
|
authority: request.authority || currentAccount.environment,
|
||
|
scopes: request.scopes?.length
|
||
|
? request.scopes
|
||
|
: [...OIDC_DEFAULT_SCOPES],
|
||
|
};
|
||
|
// fetch access token and check for expiry
|
||
|
const tokenKeys = this.browserStorage.getTokenKeys();
|
||
|
const cachedAccessToken = this.browserStorage.getAccessToken(currentAccount, authRequest, tokenKeys, currentAccount.tenantId, this.performanceClient, authRequest.correlationId);
|
||
|
// If there is no access token, log it and return null
|
||
|
if (!cachedAccessToken) {
|
||
|
this.logger.verbose("No cached access token found");
|
||
|
return Promise.resolve(null);
|
||
|
// If access token has expired, remove the token from cache and return null
|
||
|
}
|
||
|
else if (TimeUtils.wasClockTurnedBack(cachedAccessToken.cachedAt) ||
|
||
|
TimeUtils.isTokenExpired(cachedAccessToken.expiresOn, this.config.system.tokenRenewalOffsetSeconds)) {
|
||
|
this.logger.verbose("Cached access token has expired, deleting all related tokens from cache");
|
||
|
const accountEntity = AccountEntity.createFromAccountInfo(currentAccount);
|
||
|
await this.browserStorage.removeAccountContext(accountEntity);
|
||
|
return Promise.resolve(null);
|
||
|
}
|
||
|
const cachedIdToken = this.browserStorage.getIdToken(currentAccount, tokenKeys, currentAccount.tenantId, this.performanceClient, authRequest.correlationId);
|
||
|
if (!cachedIdToken) {
|
||
|
this.logger.verbose("No cached id token found");
|
||
|
return Promise.resolve(null);
|
||
|
}
|
||
|
return this.nestedAppAuthAdapter.toAuthenticationResultFromCache(currentAccount, cachedIdToken, cachedAccessToken, authRequest, authRequest.correlationId);
|
||
|
}
|
||
|
/**
|
||
|
* acquireTokenPopup flow implementation
|
||
|
* @param request
|
||
|
* @returns
|
||
|
*/
|
||
|
async acquireTokenPopup(request) {
|
||
|
return this.acquireTokenInteractive(request);
|
||
|
}
|
||
|
/**
|
||
|
* acquireTokenRedirect flow is not supported in nested app auth
|
||
|
* @param request
|
||
|
*/
|
||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||
|
acquireTokenRedirect(request) {
|
||
|
throw NestedAppAuthError.createUnsupportedError();
|
||
|
}
|
||
|
/**
|
||
|
* acquireTokenSilent flow implementation
|
||
|
* @param silentRequest
|
||
|
* @returns
|
||
|
*/
|
||
|
async acquireTokenSilent(silentRequest) {
|
||
|
return this.acquireTokenSilentInternal(silentRequest);
|
||
|
}
|
||
|
/**
|
||
|
* Hybrid flow is not currently supported in nested app auth
|
||
|
* @param request
|
||
|
*/
|
||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||
|
acquireTokenByCode(request // eslint-disable-line @typescript-eslint/no-unused-vars
|
||
|
) {
|
||
|
throw NestedAppAuthError.createUnsupportedError();
|
||
|
}
|
||
|
/**
|
||
|
* acquireTokenNative flow is not currently supported in nested app auth
|
||
|
* @param request
|
||
|
* @param apiId
|
||
|
* @param accountId
|
||
|
*/
|
||
|
acquireTokenNative(request, apiId, // eslint-disable-line @typescript-eslint/no-unused-vars
|
||
|
accountId // eslint-disable-line @typescript-eslint/no-unused-vars
|
||
|
) {
|
||
|
throw NestedAppAuthError.createUnsupportedError();
|
||
|
}
|
||
|
/**
|
||
|
* acquireTokenByRefreshToken flow is not currently supported in nested app auth
|
||
|
* @param commonRequest
|
||
|
* @param silentRequest
|
||
|
*/
|
||
|
acquireTokenByRefreshToken(commonRequest, // eslint-disable-line @typescript-eslint/no-unused-vars
|
||
|
silentRequest // eslint-disable-line @typescript-eslint/no-unused-vars
|
||
|
) {
|
||
|
throw NestedAppAuthError.createUnsupportedError();
|
||
|
}
|
||
|
/**
|
||
|
* Adds event callbacks to array
|
||
|
* @param callback
|
||
|
*/
|
||
|
addEventCallback(callback) {
|
||
|
return this.eventHandler.addEventCallback(callback);
|
||
|
}
|
||
|
/**
|
||
|
* Removes callback with provided id from callback array
|
||
|
* @param callbackId
|
||
|
*/
|
||
|
removeEventCallback(callbackId) {
|
||
|
this.eventHandler.removeEventCallback(callbackId);
|
||
|
}
|
||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||
|
addPerformanceCallback(callback) {
|
||
|
throw NestedAppAuthError.createUnsupportedError();
|
||
|
}
|
||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||
|
removePerformanceCallback(callbackId) {
|
||
|
throw NestedAppAuthError.createUnsupportedError();
|
||
|
}
|
||
|
enableAccountStorageEvents() {
|
||
|
throw NestedAppAuthError.createUnsupportedError();
|
||
|
}
|
||
|
disableAccountStorageEvents() {
|
||
|
throw NestedAppAuthError.createUnsupportedError();
|
||
|
}
|
||
|
// #region Account APIs
|
||
|
/**
|
||
|
* Returns all the accounts in the cache that match the optional filter. If no filter is provided, all accounts are returned.
|
||
|
* @param accountFilter - (Optional) filter to narrow down the accounts returned
|
||
|
* @returns Array of AccountInfo objects in cache
|
||
|
*/
|
||
|
getAllAccounts(accountFilter) {
|
||
|
return getAllAccounts(this.logger, this.browserStorage, this.isBrowserEnv(), accountFilter);
|
||
|
}
|
||
|
/**
|
||
|
* Returns the first account found in the cache that matches the account filter passed in.
|
||
|
* @param accountFilter
|
||
|
* @returns The first account found in the cache matching the provided filter or null if no account could be found.
|
||
|
*/
|
||
|
getAccount(accountFilter) {
|
||
|
return getAccount(accountFilter, this.logger, this.browserStorage);
|
||
|
}
|
||
|
/**
|
||
|
* Returns the signed in account matching username.
|
||
|
* (the account object is created at the time of successful login)
|
||
|
* or null when no matching account is found.
|
||
|
* This API is provided for convenience but getAccountById should be used for best reliability
|
||
|
* @param username
|
||
|
* @returns The account object stored in MSAL
|
||
|
*/
|
||
|
getAccountByUsername(username) {
|
||
|
return getAccountByUsername(username, this.logger, this.browserStorage);
|
||
|
}
|
||
|
/**
|
||
|
* Returns the signed in account matching homeAccountId.
|
||
|
* (the account object is created at the time of successful login)
|
||
|
* or null when no matching account is found
|
||
|
* @param homeAccountId
|
||
|
* @returns The account object stored in MSAL
|
||
|
*/
|
||
|
getAccountByHomeId(homeAccountId) {
|
||
|
return getAccountByHomeId(homeAccountId, this.logger, this.browserStorage);
|
||
|
}
|
||
|
/**
|
||
|
* Returns the signed in account matching localAccountId.
|
||
|
* (the account object is created at the time of successful login)
|
||
|
* or null when no matching account is found
|
||
|
* @param localAccountId
|
||
|
* @returns The account object stored in MSAL
|
||
|
*/
|
||
|
getAccountByLocalId(localAccountId) {
|
||
|
return getAccountByLocalId(localAccountId, this.logger, this.browserStorage);
|
||
|
}
|
||
|
/**
|
||
|
* Sets the account to use as the active account. If no account is passed to the acquireToken APIs, then MSAL will use this active account.
|
||
|
* @param account
|
||
|
*/
|
||
|
setActiveAccount(account) {
|
||
|
/*
|
||
|
* StandardController uses this to allow the developer to set the active account
|
||
|
* in the nested app auth scenario the active account is controlled by the app hosting the nested app
|
||
|
*/
|
||
|
return setActiveAccount(account, this.browserStorage);
|
||
|
}
|
||
|
/**
|
||
|
* Gets the currently active account
|
||
|
*/
|
||
|
getActiveAccount() {
|
||
|
return getActiveAccount(this.browserStorage);
|
||
|
}
|
||
|
// #endregion
|
||
|
handleRedirectPromise(hash // eslint-disable-line @typescript-eslint/no-unused-vars
|
||
|
) {
|
||
|
return Promise.resolve(null);
|
||
|
}
|
||
|
loginPopup(request // eslint-disable-line @typescript-eslint/no-unused-vars
|
||
|
) {
|
||
|
return this.acquireTokenInteractive(request || DEFAULT_REQUEST);
|
||
|
}
|
||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||
|
loginRedirect(request) {
|
||
|
throw NestedAppAuthError.createUnsupportedError();
|
||
|
}
|
||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||
|
logout(logoutRequest) {
|
||
|
throw NestedAppAuthError.createUnsupportedError();
|
||
|
}
|
||
|
logoutRedirect(logoutRequest // eslint-disable-line @typescript-eslint/no-unused-vars
|
||
|
) {
|
||
|
throw NestedAppAuthError.createUnsupportedError();
|
||
|
}
|
||
|
logoutPopup(logoutRequest // eslint-disable-line @typescript-eslint/no-unused-vars
|
||
|
) {
|
||
|
throw NestedAppAuthError.createUnsupportedError();
|
||
|
}
|
||
|
ssoSilent(
|
||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||
|
request) {
|
||
|
return this.acquireTokenSilentInternal(request);
|
||
|
}
|
||
|
getTokenCache() {
|
||
|
throw NestedAppAuthError.createUnsupportedError();
|
||
|
}
|
||
|
/**
|
||
|
* Returns the logger instance
|
||
|
*/
|
||
|
getLogger() {
|
||
|
return this.logger;
|
||
|
}
|
||
|
/**
|
||
|
* Replaces the default logger set in configurations with new Logger with new configurations
|
||
|
* @param logger Logger instance
|
||
|
*/
|
||
|
setLogger(logger) {
|
||
|
this.logger = logger;
|
||
|
}
|
||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||
|
initializeWrapperLibrary(sku, version) {
|
||
|
/*
|
||
|
* Standard controller uses this to set the sku and version of the wrapper library in the storage
|
||
|
* we do nothing here
|
||
|
*/
|
||
|
return;
|
||
|
}
|
||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||
|
setNavigationClient(navigationClient) {
|
||
|
this.logger.warning("setNavigationClient is not supported in nested app auth");
|
||
|
}
|
||
|
getConfiguration() {
|
||
|
return this.config;
|
||
|
}
|
||
|
isBrowserEnv() {
|
||
|
return this.operatingContext.isBrowserEnvironment();
|
||
|
}
|
||
|
getBrowserCrypto() {
|
||
|
return this.browserCrypto;
|
||
|
}
|
||
|
getPerformanceClient() {
|
||
|
throw NestedAppAuthError.createUnsupportedError();
|
||
|
}
|
||
|
getRedirectResponse() {
|
||
|
throw NestedAppAuthError.createUnsupportedError();
|
||
|
}
|
||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||
|
async clearCache(logoutRequest) {
|
||
|
throw NestedAppAuthError.createUnsupportedError();
|
||
|
}
|
||
|
async hydrateCache(result, request) {
|
||
|
this.logger.verbose("hydrateCache called");
|
||
|
const accountEntity = AccountEntity.createFromAccountInfo(result.account, result.cloudGraphHostName, result.msGraphHost);
|
||
|
this.browserStorage.setAccount(accountEntity);
|
||
|
return this.browserStorage.hydrateCache(result, request);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export { NestedAppAuthController };
|
||
|
//# sourceMappingURL=NestedAppAuthController.mjs.map
|