@azure/msal-browser
Version:
Microsoft Authentication Library for js
524 lines (484 loc) • 15.6 kB
text/typescript
/*
* 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,
};
}