/*
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
* Licensed under the MIT License.
|
*/
|
|
import {
|
AccessTokenEntity,
|
AccountEntity,
|
AccountEntityUtils,
|
Authority,
|
AuthorityFactory,
|
AuthorityOptions,
|
AuthToken,
|
buildAccountToCache,
|
buildStaticAuthorityOptions,
|
CacheHelpers,
|
CacheRecord,
|
ExternalTokenResponse,
|
ICrypto,
|
IdTokenEntity,
|
invokeAsync,
|
IPerformanceClient,
|
Logger,
|
RefreshTokenEntity,
|
ScopeSet,
|
StubPerformanceClient,
|
TimeUtils,
|
TokenClaims,
|
} from "@azure/msal-common/browser";
|
import { buildConfiguration, Configuration } from "../config/Configuration.js";
|
import * as BrowserCrypto from "../crypto/BrowserCrypto.js";
|
import { CryptoOps } from "../crypto/CryptoOps.js";
|
import { base64Decode } from "../encode/Base64Decode.js";
|
import {
|
BrowserAuthErrorCodes,
|
createBrowserAuthError,
|
} from "../error/BrowserAuthError.js";
|
import { EventHandler } from "../event/EventHandler.js";
|
import type { SilentRequest } from "../request/SilentRequest.js";
|
import type { AuthenticationResult } from "../response/AuthenticationResult.js";
|
import * as BrowserPerformanceEvents from "../telemetry/BrowserPerformanceEvents.js";
|
import * as BrowserRootPerformanceEvents from "../telemetry/BrowserRootPerformanceEvents.js";
|
import { ApiId } from "../utils/BrowserConstants.js";
|
import * as BrowserUtils from "../utils/BrowserUtils.js";
|
import { BrowserCacheManager } from "./BrowserCacheManager.js";
|
|
export type LoadTokenOptions = {
|
clientInfo?: string;
|
expiresOn?: number;
|
extendedExpiresOn?: number;
|
};
|
|
/**
|
* API to load tokens to msal-browser cache.
|
* @param config - Object to configure the MSAL app.
|
* @param request - Silent request containing authority, scopes, and account.
|
* @param response - External token response to load into the cache.
|
* @param options - Options controlling how tokens are loaded into the cache.
|
* @param performanceClient - Optional performance client used for telemetry measurements.
|
* @returns `AuthenticationResult` for the response that was loaded.
|
*/
|
export async function loadExternalTokens(
|
config: Configuration,
|
request: SilentRequest,
|
response: ExternalTokenResponse,
|
options: LoadTokenOptions,
|
performanceClient: IPerformanceClient = new StubPerformanceClient()
|
): Promise<AuthenticationResult> {
|
BrowserUtils.blockNonBrowserEnvironment();
|
|
const browserConfig = buildConfiguration(config, true);
|
|
const correlationId =
|
request.correlationId || BrowserCrypto.createNewGuid();
|
|
const rootMeasurement = performanceClient.startMeasurement(
|
BrowserRootPerformanceEvents.LoadExternalTokens,
|
correlationId
|
);
|
|
try {
|
const idTokenClaims = response.id_token
|
? AuthToken.extractTokenClaims(response.id_token, base64Decode)
|
: undefined;
|
const kmsi = AuthToken.isKmsi(idTokenClaims || {});
|
|
const authorityOptions: AuthorityOptions = {
|
protocolMode: browserConfig.system.protocolMode,
|
knownAuthorities: browserConfig.auth.knownAuthorities,
|
cloudDiscoveryMetadata: browserConfig.auth.cloudDiscoveryMetadata,
|
authorityMetadata: browserConfig.auth.authorityMetadata,
|
};
|
|
const logger = new Logger(browserConfig.system.loggerOptions || {});
|
const cryptoOps = new CryptoOps(logger, browserConfig.telemetry.client);
|
const storage = new BrowserCacheManager(
|
browserConfig.auth.clientId,
|
browserConfig.cache,
|
cryptoOps,
|
logger,
|
browserConfig.telemetry.client,
|
new EventHandler(logger),
|
buildStaticAuthorityOptions(browserConfig.auth)
|
);
|
|
const authorityString =
|
request.authority || browserConfig.auth.authority;
|
const authority = await AuthorityFactory.createDiscoveredInstance(
|
Authority.generateAuthority(
|
authorityString,
|
request.azureCloudOptions
|
),
|
browserConfig.system.networkClient,
|
storage,
|
authorityOptions,
|
logger,
|
correlationId,
|
performanceClient
|
);
|
|
const cacheRecordAccount: AccountEntity = await invokeAsync(
|
loadAccount,
|
BrowserPerformanceEvents.LoadAccount,
|
logger,
|
performanceClient,
|
correlationId
|
)(
|
request,
|
options.clientInfo || response.client_info || "",
|
correlationId,
|
storage,
|
logger,
|
cryptoOps,
|
authority,
|
idTokenClaims,
|
performanceClient
|
);
|
|
const idToken = await invokeAsync(
|
loadIdToken,
|
BrowserPerformanceEvents.LoadIdToken,
|
logger,
|
performanceClient,
|
correlationId
|
)(
|
response,
|
cacheRecordAccount.homeAccountId,
|
cacheRecordAccount.environment,
|
cacheRecordAccount.realm,
|
kmsi,
|
correlationId,
|
storage,
|
logger,
|
config.auth.clientId
|
);
|
|
const accessToken = await invokeAsync(
|
loadAccessToken,
|
BrowserPerformanceEvents.LoadAccessToken,
|
logger,
|
performanceClient,
|
correlationId
|
)(
|
request,
|
response,
|
cacheRecordAccount.homeAccountId,
|
cacheRecordAccount.environment,
|
cacheRecordAccount.realm,
|
kmsi,
|
options,
|
correlationId,
|
storage,
|
logger,
|
config.auth.clientId
|
);
|
|
const refreshToken = await invokeAsync(
|
loadRefreshToken,
|
BrowserPerformanceEvents.LoadRefreshToken,
|
logger,
|
performanceClient,
|
correlationId
|
)(
|
response,
|
cacheRecordAccount.homeAccountId,
|
cacheRecordAccount.environment,
|
kmsi,
|
correlationId,
|
storage,
|
logger,
|
config.auth.clientId,
|
performanceClient
|
);
|
|
rootMeasurement.end(
|
{ success: true },
|
undefined,
|
AccountEntityUtils.getAccountInfo(cacheRecordAccount)
|
);
|
|
return generateAuthenticationResult(
|
request,
|
{
|
account: cacheRecordAccount,
|
idToken,
|
accessToken,
|
refreshToken,
|
},
|
authority,
|
idTokenClaims
|
);
|
} catch (error) {
|
rootMeasurement.end({ success: false }, error);
|
throw error;
|
}
|
}
|
|
/**
|
* Helper function to load account to msal-browser cache
|
* @param idToken
|
* @param environment
|
* @param clientInfo
|
* @param authorityType
|
* @param requestHomeAccountId
|
* @returns `AccountEntity`
|
*/
|
async function loadAccount(
|
request: SilentRequest,
|
clientInfo: string,
|
correlationId: string,
|
storage: BrowserCacheManager,
|
logger: Logger,
|
cryptoObj: ICrypto,
|
authority: Authority,
|
idTokenClaims?: TokenClaims,
|
performanceClient?: IPerformanceClient
|
): Promise<AccountEntity> {
|
logger.verbose("TokenCache - loading account", correlationId);
|
|
if (request.account) {
|
const accountEntity =
|
AccountEntityUtils.createAccountEntityFromAccountInfo(
|
request.account
|
);
|
await storage.setAccount(
|
accountEntity,
|
correlationId,
|
AuthToken.isKmsi(idTokenClaims || {}),
|
ApiId.loadExternalTokens
|
);
|
return accountEntity;
|
} else if (!clientInfo && !idTokenClaims) {
|
logger.error(
|
"TokenCache - if an account is not provided on the request, clientInfo or idToken must be provided instead.",
|
correlationId
|
);
|
throw createBrowserAuthError(BrowserAuthErrorCodes.unableToLoadToken);
|
}
|
|
const homeAccountId = AccountEntityUtils.generateHomeAccountId(
|
clientInfo,
|
authority.authorityType,
|
logger,
|
cryptoObj,
|
correlationId,
|
idTokenClaims
|
);
|
|
const claimsTenantId = idTokenClaims?.tid;
|
|
const cachedAccount = buildAccountToCache(
|
storage,
|
authority,
|
homeAccountId,
|
base64Decode,
|
correlationId,
|
idTokenClaims,
|
clientInfo,
|
authority.getPreferredCache(),
|
claimsTenantId,
|
undefined, // authCodePayload
|
undefined, // nativeAccountId
|
logger,
|
performanceClient
|
);
|
|
await storage.setAccount(
|
cachedAccount,
|
correlationId,
|
AuthToken.isKmsi(idTokenClaims || {}),
|
ApiId.loadExternalTokens
|
);
|
return cachedAccount;
|
}
|
|
/**
|
* Helper function to load id tokens to msal-browser cache
|
* @param idToken
|
* @param homeAccountId
|
* @param environment
|
* @param tenantId
|
* @returns `IdTokenEntity`
|
*/
|
async function loadIdToken(
|
response: ExternalTokenResponse,
|
homeAccountId: string,
|
environment: string,
|
tenantId: string,
|
kmsi: boolean,
|
correlationId: string,
|
storage: BrowserCacheManager,
|
logger: Logger,
|
clientId: string
|
): Promise<IdTokenEntity | null> {
|
if (!response.id_token) {
|
logger.verbose(
|
"TokenCache - no id token found in response",
|
correlationId
|
);
|
return null;
|
}
|
|
logger.verbose("TokenCache - loading id token", correlationId);
|
const idTokenEntity = CacheHelpers.createIdTokenEntity(
|
homeAccountId,
|
environment,
|
response.id_token,
|
clientId,
|
tenantId
|
);
|
|
await storage.setIdTokenCredential(idTokenEntity, correlationId, kmsi);
|
return idTokenEntity;
|
}
|
|
/**
|
* Helper function to load access tokens to msal-browser cache
|
* @param request
|
* @param response
|
* @param homeAccountId
|
* @param environment
|
* @param tenantId
|
* @returns `AccessTokenEntity`
|
*/
|
async function loadAccessToken(
|
request: SilentRequest,
|
response: ExternalTokenResponse,
|
homeAccountId: string,
|
environment: string,
|
tenantId: string,
|
kmsi: boolean,
|
options: LoadTokenOptions,
|
correlationId: string,
|
storage: BrowserCacheManager,
|
logger: Logger,
|
clientId: string
|
): Promise<AccessTokenEntity | null> {
|
if (!response.access_token) {
|
logger.verbose(
|
"TokenCache - no access token found in response",
|
correlationId
|
);
|
return null;
|
} else if (!response.expires_in) {
|
logger.error(
|
"TokenCache - no expiration set on the access token. Cannot add it to the cache.",
|
correlationId
|
);
|
return null;
|
} else if (!response.scope && (!request.scopes || !request.scopes.length)) {
|
logger.error(
|
"TokenCache - scopes not specified in the request or response. Cannot add token to the cache.",
|
correlationId
|
);
|
return null;
|
}
|
|
logger.verbose("TokenCache - loading access token", correlationId);
|
|
const scopes = response.scope
|
? ScopeSet.fromString(response.scope)
|
: new ScopeSet(request.scopes);
|
const expiresOn =
|
options.expiresOn || response.expires_in + TimeUtils.nowSeconds();
|
|
const extendedExpiresOn =
|
options.extendedExpiresOn ||
|
(response.ext_expires_in || response.expires_in) +
|
TimeUtils.nowSeconds();
|
|
const accessTokenEntity = CacheHelpers.createAccessTokenEntity(
|
homeAccountId,
|
environment,
|
response.access_token,
|
clientId,
|
tenantId,
|
scopes.printScopes(),
|
expiresOn,
|
extendedExpiresOn,
|
base64Decode
|
);
|
|
await storage.setAccessTokenCredential(
|
accessTokenEntity,
|
correlationId,
|
kmsi
|
);
|
return accessTokenEntity;
|
}
|
|
/**
|
* Helper function to load refresh tokens to msal-browser cache
|
* @param request
|
* @param response
|
* @param homeAccountId
|
* @param environment
|
* @returns `RefreshTokenEntity`
|
*/
|
async function loadRefreshToken(
|
response: ExternalTokenResponse,
|
homeAccountId: string,
|
environment: string,
|
kmsi: boolean,
|
correlationId: string,
|
storage: BrowserCacheManager,
|
logger: Logger,
|
clientId: string,
|
performanceClient: IPerformanceClient
|
): Promise<RefreshTokenEntity | null> {
|
if (!response.refresh_token) {
|
logger.verbose(
|
"TokenCache - no refresh token found in response",
|
correlationId
|
);
|
return null;
|
}
|
|
const expiresOn = response.refresh_token_expires_in
|
? response.refresh_token_expires_in + TimeUtils.nowSeconds()
|
: undefined;
|
performanceClient.addFields(
|
{
|
extRtExpiresOnSeconds: expiresOn,
|
},
|
correlationId
|
);
|
|
logger.verbose("TokenCache - loading refresh token", correlationId);
|
const refreshTokenEntity = CacheHelpers.createRefreshTokenEntity(
|
homeAccountId,
|
environment,
|
response.refresh_token,
|
clientId,
|
response.foci,
|
undefined, // userAssertionHash
|
expiresOn
|
);
|
|
await storage.setRefreshTokenCredential(
|
refreshTokenEntity,
|
correlationId,
|
kmsi
|
);
|
return refreshTokenEntity;
|
}
|
|
/**
|
* Helper function to generate an `AuthenticationResult` for the result.
|
* @param request
|
* @param idTokenObj
|
* @param cacheRecord
|
* @param authority
|
* @returns `AuthenticationResult`
|
*/
|
function generateAuthenticationResult(
|
request: SilentRequest,
|
cacheRecord: CacheRecord & { account: AccountEntity },
|
authority: Authority,
|
idTokenClaims?: TokenClaims
|
): AuthenticationResult {
|
let accessToken: string = "";
|
let responseScopes: Array<string> = [];
|
let expiresOn: Date | null = null;
|
let extExpiresOn: Date | undefined;
|
|
if (cacheRecord?.accessToken) {
|
accessToken = cacheRecord.accessToken.secret;
|
responseScopes = ScopeSet.fromString(
|
cacheRecord.accessToken.target
|
).asArray();
|
// Access token expiresOn stored in seconds, converting to Date for AuthenticationResult
|
expiresOn = TimeUtils.toDateFromSeconds(
|
cacheRecord.accessToken.expiresOn
|
);
|
extExpiresOn = TimeUtils.toDateFromSeconds(
|
cacheRecord.accessToken.extendedExpiresOn
|
);
|
}
|
|
const accountEntity = cacheRecord.account;
|
|
return {
|
authority: authority.canonicalAuthority,
|
uniqueId: cacheRecord.account.localAccountId,
|
tenantId: cacheRecord.account.realm,
|
scopes: responseScopes,
|
account: AccountEntityUtils.getAccountInfo(accountEntity),
|
idToken: cacheRecord.idToken?.secret || "",
|
idTokenClaims: idTokenClaims || {},
|
accessToken: accessToken,
|
fromCache: true,
|
expiresOn: expiresOn,
|
correlationId: request.correlationId || "",
|
requestId: "",
|
extExpiresOn: extExpiresOn,
|
familyId: cacheRecord.refreshToken?.familyId || "",
|
tokenType: cacheRecord?.accessToken?.tokenType || "",
|
state: request.state || "",
|
cloudGraphHostName: accountEntity.cloudGraphHostName || "",
|
msGraphHost: accountEntity.msGraphHost || "",
|
fromPlatformBroker: false,
|
};
|
}
|