UNPKG

@azure/msal-browser

Version:
493 lines (461 loc) 16.2 kB
/* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ import { AccessTokenEntity, ICrypto, IdTokenEntity, Logger, ScopeSet, Authority, AuthorityOptions, ExternalTokenResponse, AccountEntity, AuthToken, RefreshTokenEntity, Constants, CacheRecord, TokenClaims, CacheHelpers, buildAccountToCache, } from "@azure/msal-common"; import { BrowserConfiguration } from "../config/Configuration"; import { SilentRequest } from "../request/SilentRequest"; import { BrowserCacheManager } from "./BrowserCacheManager"; import { ITokenCache } from "./ITokenCache"; import { createBrowserAuthError, BrowserAuthErrorCodes, } from "../error/BrowserAuthError"; import { AuthenticationResult } from "../response/AuthenticationResult"; import { base64Decode } from "../encode/Base64Decode"; import * as BrowserCrypto from "../crypto/BrowserCrypto"; export type LoadTokenOptions = { clientInfo?: string; expiresOn?: number; extendedExpiresOn?: number; }; /** * Token cache manager */ export class TokenCache implements ITokenCache { // Flag to indicate if in browser environment public isBrowserEnvironment: boolean; // Input configuration by developer/user protected config: BrowserConfiguration; // Browser cache storage private storage: BrowserCacheManager; // Logger private logger: Logger; // Crypto class private cryptoObj: ICrypto; constructor( configuration: BrowserConfiguration, storage: BrowserCacheManager, logger: Logger, cryptoObj: ICrypto ) { this.isBrowserEnvironment = typeof window !== "undefined"; this.config = configuration; this.storage = storage; this.logger = logger; this.cryptoObj = cryptoObj; } // Move getAllAccounts here and cache utility APIs /** * API to load tokens to msal-browser cache. * @param request * @param response * @param options * @returns `AuthenticationResult` for the response that was loaded. */ loadExternalTokens( request: SilentRequest, response: ExternalTokenResponse, options: LoadTokenOptions ): AuthenticationResult { this.logger.info("TokenCache - loadExternalTokens called"); if (!response.id_token) { throw createBrowserAuthError( BrowserAuthErrorCodes.unableToLoadToken ); } const idTokenClaims = AuthToken.extractTokenClaims( response.id_token, base64Decode ); let cacheRecord: CacheRecord; let authority: Authority | undefined; let cacheRecordAccount: AccountEntity; if (request.account) { cacheRecordAccount = AccountEntity.createFromAccountInfo( request.account ); cacheRecord = new CacheRecord( cacheRecordAccount, this.loadIdToken( response.id_token, cacheRecordAccount.homeAccountId, request.account.environment, request.account.tenantId ), this.loadAccessToken( request, response, cacheRecordAccount.homeAccountId, request.account.environment, request.account.tenantId, options ), this.loadRefreshToken( request, response, cacheRecordAccount.homeAccountId, request.account.environment ) ); } else if (request.authority) { const authorityUrl = Authority.generateAuthority( request.authority, request.azureCloudOptions ); const authorityOptions: AuthorityOptions = { protocolMode: this.config.auth.protocolMode, knownAuthorities: this.config.auth.knownAuthorities, cloudDiscoveryMetadata: this.config.auth.cloudDiscoveryMetadata, authorityMetadata: this.config.auth.authorityMetadata, skipAuthorityMetadataCache: this.config.auth.skipAuthorityMetadataCache, }; authority = new Authority( authorityUrl, this.config.system.networkClient, this.storage, authorityOptions, this.logger, request.correlationId || BrowserCrypto.createNewGuid() ); // "clientInfo" from options takes precedence over "clientInfo" in response if (options.clientInfo) { this.logger.trace("TokenCache - homeAccountId from options"); cacheRecordAccount = this.loadAccount( idTokenClaims, authority, options.clientInfo ); cacheRecord = new CacheRecord( cacheRecordAccount, this.loadIdToken( response.id_token, cacheRecordAccount.homeAccountId, authority.hostnameAndPort, authority.tenant ), this.loadAccessToken( request, response, cacheRecordAccount.homeAccountId, authority.hostnameAndPort, authority.tenant, options ), this.loadRefreshToken( request, response, cacheRecordAccount.homeAccountId, authority.hostnameAndPort ) ); } else if (response.client_info) { this.logger.trace("TokenCache - homeAccountId from response"); cacheRecordAccount = this.loadAccount( idTokenClaims, authority, response.client_info ); cacheRecord = new CacheRecord( cacheRecordAccount, this.loadIdToken( response.id_token, cacheRecordAccount.homeAccountId, authority.hostnameAndPort, authority.tenant ), this.loadAccessToken( request, response, cacheRecordAccount.homeAccountId, authority.hostnameAndPort, authority.tenant, options ), this.loadRefreshToken( request, response, cacheRecordAccount.homeAccountId, authority.hostnameAndPort ) ); } else { throw createBrowserAuthError( BrowserAuthErrorCodes.unableToLoadToken ); } } else { throw createBrowserAuthError( BrowserAuthErrorCodes.unableToLoadToken ); } return this.generateAuthenticationResult( request, idTokenClaims, cacheRecord, cacheRecordAccount, authority ); } /** * Helper function to load account to msal-browser cache * @param idToken * @param environment * @param clientInfo * @param authorityType * @param requestHomeAccountId * @returns `AccountEntity` */ private loadAccount( idTokenClaims: TokenClaims, authority: Authority, clientInfo?: string, requestHomeAccountId?: string ): AccountEntity { if (this.isBrowserEnvironment) { this.logger.verbose("TokenCache - loading account"); let homeAccountId; if (requestHomeAccountId) { homeAccountId = requestHomeAccountId; } else if (authority.authorityType !== undefined && clientInfo) { homeAccountId = AccountEntity.generateHomeAccountId( clientInfo, authority.authorityType, this.logger, this.cryptoObj, idTokenClaims ); } if (!homeAccountId) { throw createBrowserAuthError( BrowserAuthErrorCodes.unableToLoadToken ); } const claimsTenantId = idTokenClaims.tid; const cachedAccount = buildAccountToCache( this.storage, authority, homeAccountId, idTokenClaims, base64Decode, clientInfo, authority.hostnameAndPort, claimsTenantId, undefined, // authCodePayload undefined, // nativeAccountId this.logger ); this.storage.setAccount(cachedAccount); return cachedAccount; } else { throw createBrowserAuthError( BrowserAuthErrorCodes.unableToLoadToken ); } } /** * Helper function to load id tokens to msal-browser cache * @param idToken * @param homeAccountId * @param environment * @param tenantId * @returns `IdTokenEntity` */ private loadIdToken( idToken: string, homeAccountId: string, environment: string, tenantId: string ): IdTokenEntity { const idTokenEntity = CacheHelpers.createIdTokenEntity( homeAccountId, environment, idToken, this.config.auth.clientId, tenantId ); if (this.isBrowserEnvironment) { this.logger.verbose("TokenCache - loading id token"); this.storage.setIdTokenCredential(idTokenEntity); return idTokenEntity; } else { throw createBrowserAuthError( BrowserAuthErrorCodes.unableToLoadToken ); } } /** * Helper function to load access tokens to msal-browser cache * @param request * @param response * @param homeAccountId * @param environment * @param tenantId * @returns `AccessTokenEntity` */ private loadAccessToken( request: SilentRequest, response: ExternalTokenResponse, homeAccountId: string, environment: string, tenantId: string, options: LoadTokenOptions ): AccessTokenEntity | null { if (!response.access_token) { this.logger.verbose( "TokenCache - No access token provided for caching" ); return null; } if (!response.expires_in) { throw createBrowserAuthError( BrowserAuthErrorCodes.unableToLoadToken ); } if (!options.extendedExpiresOn) { throw createBrowserAuthError( BrowserAuthErrorCodes.unableToLoadToken ); } const scopes = new ScopeSet(request.scopes).printScopes(); const expiresOn = options.expiresOn || response.expires_in + new Date().getTime() / 1000; const extendedExpiresOn = options.extendedExpiresOn; const accessTokenEntity = CacheHelpers.createAccessTokenEntity( homeAccountId, environment, response.access_token, this.config.auth.clientId, tenantId, scopes, expiresOn, extendedExpiresOn, base64Decode ); if (this.isBrowserEnvironment) { this.logger.verbose("TokenCache - loading access token"); this.storage.setAccessTokenCredential(accessTokenEntity); return accessTokenEntity; } else { throw createBrowserAuthError( BrowserAuthErrorCodes.unableToLoadToken ); } } /** * Helper function to load refresh tokens to msal-browser cache * @param request * @param response * @param homeAccountId * @param environment * @returns `RefreshTokenEntity` */ private loadRefreshToken( request: SilentRequest, response: ExternalTokenResponse, homeAccountId: string, environment: string ): RefreshTokenEntity | null { if (!response.refresh_token) { this.logger.verbose( "TokenCache - No refresh token provided for caching" ); return null; } const refreshTokenEntity = CacheHelpers.createRefreshTokenEntity( homeAccountId, environment, response.refresh_token, this.config.auth.clientId ); if (this.isBrowserEnvironment) { this.logger.verbose("TokenCache - loading refresh token"); this.storage.setRefreshTokenCredential(refreshTokenEntity); return refreshTokenEntity; } else { throw createBrowserAuthError( BrowserAuthErrorCodes.unableToLoadToken ); } } /** * Helper function to generate an `AuthenticationResult` for the result. * @param request * @param idTokenObj * @param cacheRecord * @param authority * @returns `AuthenticationResult` */ private generateAuthenticationResult( request: SilentRequest, idTokenClaims: TokenClaims, cacheRecord: CacheRecord, accountEntity: AccountEntity, authority?: Authority ): AuthenticationResult { let accessToken: string = Constants.EMPTY_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(); expiresOn = new Date( Number(cacheRecord.accessToken.expiresOn) * 1000 ); extExpiresOn = new Date( Number(cacheRecord.accessToken.extendedExpiresOn) * 1000 ); } const uid = idTokenClaims.oid || idTokenClaims.sub || Constants.EMPTY_STRING; const tid = idTokenClaims.tid || Constants.EMPTY_STRING; return { authority: authority ? authority.canonicalAuthority : Constants.EMPTY_STRING, uniqueId: uid, tenantId: tid, scopes: responseScopes, account: accountEntity.getAccountInfo(), idToken: cacheRecord.idToken?.secret || "", idTokenClaims: idTokenClaims || {}, accessToken: accessToken, fromCache: true, expiresOn: expiresOn, correlationId: request.correlationId || Constants.EMPTY_STRING, requestId: Constants.EMPTY_STRING, extExpiresOn: extExpiresOn, familyId: Constants.EMPTY_STRING, tokenType: cacheRecord?.accessToken?.tokenType || Constants.EMPTY_STRING, state: Constants.EMPTY_STRING, cloudGraphHostName: accountEntity.cloudGraphHostName || Constants.EMPTY_STRING, msGraphHost: accountEntity.msGraphHost || Constants.EMPTY_STRING, code: undefined, fromNativeBroker: false, }; } }