UNPKG

@azure/msal-browser

Version:
524 lines (484 loc) 15.6 kB
/* * 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, }; }