UNPKG

@azure/msal-common

Version:
1,564 lines (1,389 loc) 60.9 kB
/* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ import { AccountFilter, CredentialFilter, ValidCredentialType, AppMetadataFilter, AppMetadataCache, TokenKeys, TenantProfileFilter, } from "./utils/CacheTypes.js"; import { CacheRecord } from "./entities/CacheRecord.js"; import { CredentialType, APP_METADATA, THE_FAMILY_ID, AUTHORITY_METADATA_CONSTANTS, AuthenticationScheme, Separators, } from "../utils/Constants.js"; import { CredentialEntity } from "./entities/CredentialEntity.js"; import { generateCredentialKey } from "./utils/CacheHelpers.js"; import { ScopeSet } from "../request/ScopeSet.js"; import { AccountEntity } from "./entities/AccountEntity.js"; import { AccessTokenEntity } from "./entities/AccessTokenEntity.js"; import { IdTokenEntity } from "./entities/IdTokenEntity.js"; import { RefreshTokenEntity } from "./entities/RefreshTokenEntity.js"; import { ICacheManager } from "./interface/ICacheManager.js"; import { createClientAuthError, ClientAuthErrorCodes, } from "../error/ClientAuthError.js"; import { AccountInfo, TenantProfile, updateAccountTenantProfileData, } from "../account/AccountInfo.js"; import { AppMetadataEntity } from "./entities/AppMetadataEntity.js"; import { ServerTelemetryEntity } from "./entities/ServerTelemetryEntity.js"; import { ThrottlingEntity } from "./entities/ThrottlingEntity.js"; import { extractTokenClaims } from "../account/AuthToken.js"; import { ICrypto } from "../crypto/ICrypto.js"; import { AuthorityMetadataEntity } from "./entities/AuthorityMetadataEntity.js"; import { BaseAuthRequest } from "../request/BaseAuthRequest.js"; import { Logger } from "../logger/Logger.js"; import { name, version } from "../packageMetadata.js"; import { StoreInCache } from "../request/StoreInCache.js"; import { getAliasesFromStaticSources } from "../authority/AuthorityMetadata.js"; import { StaticAuthorityOptions } from "../authority/AuthorityOptions.js"; import { TokenClaims } from "../account/TokenClaims.js"; import { IPerformanceClient } from "../telemetry/performance/IPerformanceClient.js"; import { CacheError, CacheErrorCodes } from "../error/CacheError.js"; /** * Interface class which implement cache storage functions used by MSAL to perform validity checks, and store tokens. * @internal */ export abstract class CacheManager implements ICacheManager { protected clientId: string; protected cryptoImpl: ICrypto; // Instance of logger for functions defined in the msal-common layer private commonLogger: Logger; private staticAuthorityOptions?: StaticAuthorityOptions; constructor( clientId: string, cryptoImpl: ICrypto, logger: Logger, staticAuthorityOptions?: StaticAuthorityOptions ) { this.clientId = clientId; this.cryptoImpl = cryptoImpl; this.commonLogger = logger.clone(name, version); this.staticAuthorityOptions = staticAuthorityOptions; } /** * fetch the account entity from the platform cache * @param accountKey */ abstract getAccount( accountKey: string, logger?: Logger ): AccountEntity | null; /** * set account entity in the platform cache * @param account * @param correlationId */ abstract setAccount( account: AccountEntity, correlationId: string ): Promise<void>; /** * fetch the idToken entity from the platform cache * @param idTokenKey */ abstract getIdTokenCredential(idTokenKey: string): IdTokenEntity | null; /** * set idToken entity to the platform cache * @param idToken * @param correlationId */ abstract setIdTokenCredential( idToken: IdTokenEntity, correlationId: string ): Promise<void>; /** * fetch the idToken entity from the platform cache * @param accessTokenKey */ abstract getAccessTokenCredential( accessTokenKey: string ): AccessTokenEntity | null; /** * set accessToken entity to the platform cache * @param accessToken * @param correlationId */ abstract setAccessTokenCredential( accessToken: AccessTokenEntity, correlationId: string ): Promise<void>; /** * fetch the idToken entity from the platform cache * @param refreshTokenKey */ abstract getRefreshTokenCredential( refreshTokenKey: string ): RefreshTokenEntity | null; /** * set refreshToken entity to the platform cache * @param refreshToken * @param correlationId */ abstract setRefreshTokenCredential( refreshToken: RefreshTokenEntity, correlationId: string ): Promise<void>; /** * fetch appMetadata entity from the platform cache * @param appMetadataKey */ abstract getAppMetadata(appMetadataKey: string): AppMetadataEntity | null; /** * set appMetadata entity to the platform cache * @param appMetadata */ abstract setAppMetadata(appMetadata: AppMetadataEntity): void; /** * fetch server telemetry entity from the platform cache * @param serverTelemetryKey */ abstract getServerTelemetry( serverTelemetryKey: string ): ServerTelemetryEntity | null; /** * set server telemetry entity to the platform cache * @param serverTelemetryKey * @param serverTelemetry */ abstract setServerTelemetry( serverTelemetryKey: string, serverTelemetry: ServerTelemetryEntity ): void; /** * fetch cloud discovery metadata entity from the platform cache * @param key */ abstract getAuthorityMetadata(key: string): AuthorityMetadataEntity | null; /** * */ abstract getAuthorityMetadataKeys(): Array<string>; /** * set cloud discovery metadata entity to the platform cache * @param key * @param value */ abstract setAuthorityMetadata( key: string, value: AuthorityMetadataEntity ): void; /** * fetch throttling entity from the platform cache * @param throttlingCacheKey */ abstract getThrottlingCache( throttlingCacheKey: string ): ThrottlingEntity | null; /** * set throttling entity to the platform cache * @param throttlingCacheKey * @param throttlingCache */ abstract setThrottlingCache( throttlingCacheKey: string, throttlingCache: ThrottlingEntity ): void; /** * Function to remove an item from cache given its key. * @param key */ abstract removeItem(key: string): void; /** * Function which retrieves all current keys from the cache. */ abstract getKeys(): string[]; /** * Function which retrieves all account keys from the cache */ abstract getAccountKeys(): string[]; /** * Function which retrieves all token keys from the cache */ abstract getTokenKeys(): TokenKeys; /** * 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?: AccountFilter): AccountInfo[] { return this.buildTenantProfiles( this.getAccountsFilteredBy(accountFilter || {}), accountFilter ); } /** * Gets first tenanted AccountInfo object found based on provided filters */ getAccountInfoFilteredBy(accountFilter: AccountFilter): AccountInfo | null { const allAccounts = this.getAllAccounts(accountFilter); if (allAccounts.length > 1) { // If one or more accounts are found, prioritize accounts that have an ID token const sortedAccounts = allAccounts.sort((account) => { return account.idTokenClaims ? -1 : 1; }); return sortedAccounts[0]; } else if (allAccounts.length === 1) { // If only one account is found, return it regardless of whether a matching ID token was found return allAccounts[0]; } else { return null; } } /** * Returns a single matching * @param accountFilter * @returns */ getBaseAccountInfo(accountFilter: AccountFilter): AccountInfo | null { const accountEntities = this.getAccountsFilteredBy(accountFilter); if (accountEntities.length > 0) { return accountEntities[0].getAccountInfo(); } else { return null; } } /** * Matches filtered account entities with cached ID tokens that match the tenant profile-specific account filters * and builds the account info objects from the matching ID token's claims * @param cachedAccounts * @param accountFilter * @returns Array of AccountInfo objects that match account and tenant profile filters */ private buildTenantProfiles( cachedAccounts: AccountEntity[], accountFilter?: AccountFilter ): AccountInfo[] { return cachedAccounts.flatMap((accountEntity) => { return this.getTenantProfilesFromAccountEntity( accountEntity, accountFilter?.tenantId, accountFilter ); }); } private getTenantedAccountInfoByFilter( accountInfo: AccountInfo, tokenKeys: TokenKeys, tenantProfile: TenantProfile, tenantProfileFilter?: TenantProfileFilter ): AccountInfo | null { let tenantedAccountInfo: AccountInfo | null = null; let idTokenClaims: TokenClaims | undefined; if (tenantProfileFilter) { if ( !this.tenantProfileMatchesFilter( tenantProfile, tenantProfileFilter ) ) { return null; } } const idToken = this.getIdToken( accountInfo, tokenKeys, tenantProfile.tenantId ); if (idToken) { idTokenClaims = extractTokenClaims( idToken.secret, this.cryptoImpl.base64Decode ); if ( !this.idTokenClaimsMatchTenantProfileFilter( idTokenClaims, tenantProfileFilter ) ) { // ID token sourced claims don't match so this tenant profile is not a match return null; } } // Expand tenant profile into account info based on matching tenant profile and if available matching ID token claims tenantedAccountInfo = updateAccountTenantProfileData( accountInfo, tenantProfile, idTokenClaims, idToken?.secret ); return tenantedAccountInfo; } private getTenantProfilesFromAccountEntity( accountEntity: AccountEntity, targetTenantId?: string, tenantProfileFilter?: TenantProfileFilter ): AccountInfo[] { const accountInfo = accountEntity.getAccountInfo(); let searchTenantProfiles: Map<string, TenantProfile> = accountInfo.tenantProfiles || new Map<string, TenantProfile>(); const tokenKeys = this.getTokenKeys(); // If a tenant ID was provided, only return the tenant profile for that tenant ID if it exists if (targetTenantId) { const tenantProfile = searchTenantProfiles.get(targetTenantId); if (tenantProfile) { // Reduce search field to just this tenant profile searchTenantProfiles = new Map<string, TenantProfile>([ [targetTenantId, tenantProfile], ]); } else { // No tenant profile for search tenant ID, return empty array return []; } } const matchingTenantProfiles: AccountInfo[] = []; searchTenantProfiles.forEach((tenantProfile: TenantProfile) => { const tenantedAccountInfo = this.getTenantedAccountInfoByFilter( accountInfo, tokenKeys, tenantProfile, tenantProfileFilter ); if (tenantedAccountInfo) { matchingTenantProfiles.push(tenantedAccountInfo); } }); return matchingTenantProfiles; } private tenantProfileMatchesFilter( tenantProfile: TenantProfile, tenantProfileFilter: TenantProfileFilter ): boolean { if ( !!tenantProfileFilter.localAccountId && !this.matchLocalAccountIdFromTenantProfile( tenantProfile, tenantProfileFilter.localAccountId ) ) { return false; } if ( !!tenantProfileFilter.name && !(tenantProfile.name === tenantProfileFilter.name) ) { return false; } if ( tenantProfileFilter.isHomeTenant !== undefined && !(tenantProfile.isHomeTenant === tenantProfileFilter.isHomeTenant) ) { return false; } return true; } private idTokenClaimsMatchTenantProfileFilter( idTokenClaims: TokenClaims, tenantProfileFilter?: TenantProfileFilter ): boolean { // Tenant Profile filtering if (tenantProfileFilter) { if ( !!tenantProfileFilter.localAccountId && !this.matchLocalAccountIdFromTokenClaims( idTokenClaims, tenantProfileFilter.localAccountId ) ) { return false; } if ( !!tenantProfileFilter.loginHint && !this.matchLoginHintFromTokenClaims( idTokenClaims, tenantProfileFilter.loginHint ) ) { return false; } if ( !!tenantProfileFilter.username && !this.matchUsername( idTokenClaims.preferred_username, tenantProfileFilter.username ) ) { return false; } if ( !!tenantProfileFilter.name && !this.matchName(idTokenClaims, tenantProfileFilter.name) ) { return false; } if ( !!tenantProfileFilter.sid && !this.matchSid(idTokenClaims, tenantProfileFilter.sid) ) { return false; } } return true; } /** * saves a cache record * @param cacheRecord {CacheRecord} * @param storeInCache {?StoreInCache} * @param correlationId {?string} correlation id */ async saveCacheRecord( cacheRecord: CacheRecord, correlationId: string, storeInCache?: StoreInCache ): Promise<void> { if (!cacheRecord) { throw createClientAuthError( ClientAuthErrorCodes.invalidCacheRecord ); } try { if (!!cacheRecord.account) { await this.setAccount(cacheRecord.account, correlationId); } if (!!cacheRecord.idToken && storeInCache?.idToken !== false) { await this.setIdTokenCredential( cacheRecord.idToken, correlationId ); } if ( !!cacheRecord.accessToken && storeInCache?.accessToken !== false ) { await this.saveAccessToken( cacheRecord.accessToken, correlationId ); } if ( !!cacheRecord.refreshToken && storeInCache?.refreshToken !== false ) { await this.setRefreshTokenCredential( cacheRecord.refreshToken, correlationId ); } if (!!cacheRecord.appMetadata) { this.setAppMetadata(cacheRecord.appMetadata); } } catch (e: unknown) { this.commonLogger?.error(`CacheManager.saveCacheRecord: failed`); if (e instanceof Error) { this.commonLogger?.errorPii( `CacheManager.saveCacheRecord: ${e.message}`, correlationId ); if ( e.name === "QuotaExceededError" || e.name === "NS_ERROR_DOM_QUOTA_REACHED" || e.message.includes("exceeded the quota") ) { this.commonLogger?.error( `CacheManager.saveCacheRecord: exceeded storage quota`, correlationId ); throw new CacheError( CacheErrorCodes.cacheQuotaExceededErrorCode ); } else { throw new CacheError(e.name, e.message); } } else { this.commonLogger?.errorPii( `CacheManager.saveCacheRecord: ${e}`, correlationId ); throw new CacheError(CacheErrorCodes.cacheUnknownErrorCode); } } } /** * saves access token credential * @param credential */ private async saveAccessToken( credential: AccessTokenEntity, correlationId: string ): Promise<void> { const accessTokenFilter: CredentialFilter = { clientId: credential.clientId, credentialType: credential.credentialType, environment: credential.environment, homeAccountId: credential.homeAccountId, realm: credential.realm, tokenType: credential.tokenType, requestedClaimsHash: credential.requestedClaimsHash, }; const tokenKeys = this.getTokenKeys(); const currentScopes = ScopeSet.fromString(credential.target); const removedAccessTokens: Array<Promise<void>> = []; tokenKeys.accessToken.forEach((key) => { if ( !this.accessTokenKeyMatchesFilter(key, accessTokenFilter, false) ) { return; } const tokenEntity = this.getAccessTokenCredential(key); if ( tokenEntity && this.credentialMatchesFilter(tokenEntity, accessTokenFilter) ) { const tokenScopeSet = ScopeSet.fromString(tokenEntity.target); if (tokenScopeSet.intersectingScopeSets(currentScopes)) { removedAccessTokens.push(this.removeAccessToken(key)); } } }); await Promise.all(removedAccessTokens); await this.setAccessTokenCredential(credential, correlationId); } /** * Retrieve account entities matching all provided tenant-agnostic filters; if no filter is set, get all account entities in the cache * Not checking for casing as keys are all generated in lower case, remember to convert to lower case if object properties are compared * @param accountFilter - An object containing Account properties to filter by */ getAccountsFilteredBy(accountFilter: AccountFilter): AccountEntity[] { const allAccountKeys = this.getAccountKeys(); const matchingAccounts: AccountEntity[] = []; allAccountKeys.forEach((cacheKey) => { if (!this.isAccountKey(cacheKey, accountFilter.homeAccountId)) { // Don't parse value if the key doesn't match the account filters return; } const entity: AccountEntity | null = this.getAccount( cacheKey, this.commonLogger ); // Match base account fields if (!entity) { return; } if ( !!accountFilter.homeAccountId && !this.matchHomeAccountId(entity, accountFilter.homeAccountId) ) { return; } if ( !!accountFilter.username && !this.matchUsername(entity.username, accountFilter.username) ) { return; } if ( !!accountFilter.environment && !this.matchEnvironment(entity, accountFilter.environment) ) { return; } if ( !!accountFilter.realm && !this.matchRealm(entity, accountFilter.realm) ) { return; } if ( !!accountFilter.nativeAccountId && !this.matchNativeAccountId( entity, accountFilter.nativeAccountId ) ) { return; } if ( !!accountFilter.authorityType && !this.matchAuthorityType(entity, accountFilter.authorityType) ) { return; } // If at least one tenant profile matches the tenant profile filter, add the account to the list of matching accounts const tenantProfileFilter: TenantProfileFilter = { localAccountId: accountFilter?.localAccountId, name: accountFilter?.name, }; const matchingTenantProfiles = entity.tenantProfiles?.filter( (tenantProfile: TenantProfile) => { return this.tenantProfileMatchesFilter( tenantProfile, tenantProfileFilter ); } ); if (matchingTenantProfiles && matchingTenantProfiles.length === 0) { // No tenant profile for this account matches filter, don't add to list of matching accounts return; } matchingAccounts.push(entity); }); return matchingAccounts; } /** * Returns true if the given key matches our account key schema. Also matches homeAccountId and/or tenantId if provided * @param key * @param homeAccountId * @param tenantId * @returns */ isAccountKey( key: string, homeAccountId?: string, tenantId?: string ): boolean { if (key.split(Separators.CACHE_KEY_SEPARATOR).length < 3) { // Account cache keys contain 3 items separated by '-' (each item may also contain '-') return false; } if ( homeAccountId && !key.toLowerCase().includes(homeAccountId.toLowerCase()) ) { return false; } if (tenantId && !key.toLowerCase().includes(tenantId.toLowerCase())) { return false; } // Do not check environment as aliasing can cause false negatives return true; } /** * Returns true if the given key matches our credential key schema. * @param key */ isCredentialKey(key: string): boolean { if (key.split(Separators.CACHE_KEY_SEPARATOR).length < 6) { // Credential cache keys contain 6 items separated by '-' (each item may also contain '-') return false; } const lowerCaseKey = key.toLowerCase(); // Credential keys must indicate what credential type they represent if ( lowerCaseKey.indexOf(CredentialType.ID_TOKEN.toLowerCase()) === -1 && lowerCaseKey.indexOf(CredentialType.ACCESS_TOKEN.toLowerCase()) === -1 && lowerCaseKey.indexOf( CredentialType.ACCESS_TOKEN_WITH_AUTH_SCHEME.toLowerCase() ) === -1 && lowerCaseKey.indexOf(CredentialType.REFRESH_TOKEN.toLowerCase()) === -1 ) { return false; } if ( lowerCaseKey.indexOf(CredentialType.REFRESH_TOKEN.toLowerCase()) > -1 ) { // Refresh tokens must contain the client id or family id const clientIdValidation = `${CredentialType.REFRESH_TOKEN}${Separators.CACHE_KEY_SEPARATOR}${this.clientId}${Separators.CACHE_KEY_SEPARATOR}`; const familyIdValidation = `${CredentialType.REFRESH_TOKEN}${Separators.CACHE_KEY_SEPARATOR}${THE_FAMILY_ID}${Separators.CACHE_KEY_SEPARATOR}`; if ( lowerCaseKey.indexOf(clientIdValidation.toLowerCase()) === -1 && lowerCaseKey.indexOf(familyIdValidation.toLowerCase()) === -1 ) { return false; } } else if (lowerCaseKey.indexOf(this.clientId.toLowerCase()) === -1) { // Tokens must contain the clientId return false; } return true; } /** * Returns whether or not the given credential entity matches the filter * @param entity * @param filter * @returns */ credentialMatchesFilter( entity: ValidCredentialType, filter: CredentialFilter ): boolean { if (!!filter.clientId && !this.matchClientId(entity, filter.clientId)) { return false; } if ( !!filter.userAssertionHash && !this.matchUserAssertionHash(entity, filter.userAssertionHash) ) { return false; } /* * homeAccountId can be undefined, and we want to filter out cached items that have a homeAccountId of "" * because we don't want a client_credential request to return a cached token that has a homeAccountId */ if ( typeof filter.homeAccountId === "string" && !this.matchHomeAccountId(entity, filter.homeAccountId) ) { return false; } if ( !!filter.environment && !this.matchEnvironment(entity, filter.environment) ) { return false; } if (!!filter.realm && !this.matchRealm(entity, filter.realm)) { return false; } if ( !!filter.credentialType && !this.matchCredentialType(entity, filter.credentialType) ) { return false; } if (!!filter.familyId && !this.matchFamilyId(entity, filter.familyId)) { return false; } /* * idTokens do not have "target", target specific refreshTokens do exist for some types of authentication * Resource specific refresh tokens case will be added when the support is deemed necessary */ if (!!filter.target && !this.matchTarget(entity, filter.target)) { return false; } // If request OR cached entity has requested Claims Hash, check if they match if (filter.requestedClaimsHash || entity.requestedClaimsHash) { // Don't match if either is undefined or they are different if (entity.requestedClaimsHash !== filter.requestedClaimsHash) { return false; } } // Access Token with Auth Scheme specific matching if ( entity.credentialType === CredentialType.ACCESS_TOKEN_WITH_AUTH_SCHEME ) { if ( !!filter.tokenType && !this.matchTokenType(entity, filter.tokenType) ) { return false; } // KeyId (sshKid) in request must match cached SSH certificate keyId because SSH cert is bound to a specific key if (filter.tokenType === AuthenticationScheme.SSH) { if (filter.keyId && !this.matchKeyId(entity, filter.keyId)) { return false; } } } return true; } /** * retrieve appMetadata matching all provided filters; if no filter is set, get all appMetadata * @param filter */ getAppMetadataFilteredBy(filter: AppMetadataFilter): AppMetadataCache { const allCacheKeys = this.getKeys(); const matchingAppMetadata: AppMetadataCache = {}; allCacheKeys.forEach((cacheKey) => { // don't parse any non-appMetadata type cache entities if (!this.isAppMetadata(cacheKey)) { return; } // Attempt retrieval const entity = this.getAppMetadata(cacheKey); if (!entity) { return; } if ( !!filter.environment && !this.matchEnvironment(entity, filter.environment) ) { return; } if ( !!filter.clientId && !this.matchClientId(entity, filter.clientId) ) { return; } matchingAppMetadata[cacheKey] = entity; }); return matchingAppMetadata; } /** * retrieve authorityMetadata that contains a matching alias * @param filter */ getAuthorityMetadataByAlias(host: string): AuthorityMetadataEntity | null { const allCacheKeys = this.getAuthorityMetadataKeys(); let matchedEntity = null; allCacheKeys.forEach((cacheKey) => { // don't parse any non-authorityMetadata type cache entities if ( !this.isAuthorityMetadata(cacheKey) || cacheKey.indexOf(this.clientId) === -1 ) { return; } // Attempt retrieval const entity = this.getAuthorityMetadata(cacheKey); if (!entity) { return; } if (entity.aliases.indexOf(host) === -1) { return; } matchedEntity = entity; }); return matchedEntity; } /** * Removes all accounts and related tokens from cache. */ async removeAllAccounts(): Promise<void> { const allAccountKeys = this.getAccountKeys(); const removedAccounts: Array<Promise<void>> = []; allAccountKeys.forEach((cacheKey) => { removedAccounts.push(this.removeAccount(cacheKey)); }); await Promise.all(removedAccounts); } /** * Removes the account and related tokens for a given account key * @param account */ async removeAccount(accountKey: string): Promise<void> { const account = this.getAccount(accountKey, this.commonLogger); if (!account) { return; } await this.removeAccountContext(account); this.removeItem(accountKey); } /** * Removes credentials associated with the provided account * @param account */ async removeAccountContext(account: AccountEntity): Promise<void> { const allTokenKeys = this.getTokenKeys(); const accountId = account.generateAccountId(); const removedCredentials: Array<Promise<void>> = []; allTokenKeys.idToken.forEach((key) => { if (key.indexOf(accountId) === 0) { this.removeIdToken(key); } }); allTokenKeys.accessToken.forEach((key) => { if (key.indexOf(accountId) === 0) { removedCredentials.push(this.removeAccessToken(key)); } }); allTokenKeys.refreshToken.forEach((key) => { if (key.indexOf(accountId) === 0) { this.removeRefreshToken(key); } }); await Promise.all(removedCredentials); } /** * returns a boolean if the given credential is removed * @param credential */ async removeAccessToken(key: string): Promise<void> { const credential = this.getAccessTokenCredential(key); if (!credential) { return; } // Remove Token Binding Key from key store for PoP Tokens Credentials if ( credential.credentialType.toLowerCase() === CredentialType.ACCESS_TOKEN_WITH_AUTH_SCHEME.toLowerCase() ) { if (credential.tokenType === AuthenticationScheme.POP) { const accessTokenWithAuthSchemeEntity = credential as AccessTokenEntity; const kid = accessTokenWithAuthSchemeEntity.keyId; if (kid) { try { await this.cryptoImpl.removeTokenBindingKey(kid); } catch (error) { throw createClientAuthError( ClientAuthErrorCodes.bindingKeyNotRemoved ); } } } } return this.removeItem(key); } /** * Removes all app metadata objects from cache. */ removeAppMetadata(): boolean { const allCacheKeys = this.getKeys(); allCacheKeys.forEach((cacheKey) => { if (this.isAppMetadata(cacheKey)) { this.removeItem(cacheKey); } }); return true; } /** * Retrieve AccountEntity from cache * @param account */ readAccountFromCache(account: AccountInfo): AccountEntity | null { const accountKey: string = AccountEntity.generateAccountCacheKey(account); return this.getAccount(accountKey, this.commonLogger); } /** * Retrieve IdTokenEntity from cache * @param account {AccountInfo} * @param tokenKeys {?TokenKeys} * @param targetRealm {?string} * @param performanceClient {?IPerformanceClient} * @param correlationId {?string} */ getIdToken( account: AccountInfo, tokenKeys?: TokenKeys, targetRealm?: string, performanceClient?: IPerformanceClient, correlationId?: string ): IdTokenEntity | null { this.commonLogger.trace("CacheManager - getIdToken called"); const idTokenFilter: CredentialFilter = { homeAccountId: account.homeAccountId, environment: account.environment, credentialType: CredentialType.ID_TOKEN, clientId: this.clientId, realm: targetRealm, }; const idTokenMap: Map<string, IdTokenEntity> = this.getIdTokensByFilter( idTokenFilter, tokenKeys ); const numIdTokens = idTokenMap.size; if (numIdTokens < 1) { this.commonLogger.info("CacheManager:getIdToken - No token found"); return null; } else if (numIdTokens > 1) { let tokensToBeRemoved: Map<string, IdTokenEntity> = idTokenMap; // Multiple tenant profiles and no tenant specified, pick home account if (!targetRealm) { const homeIdTokenMap: Map<string, IdTokenEntity> = new Map< string, IdTokenEntity >(); idTokenMap.forEach((idToken, key) => { if (idToken.realm === account.tenantId) { homeIdTokenMap.set(key, idToken); } }); const numHomeIdTokens = homeIdTokenMap.size; if (numHomeIdTokens < 1) { this.commonLogger.info( "CacheManager:getIdToken - Multiple ID tokens found for account but none match account entity tenant id, returning first result" ); return idTokenMap.values().next().value; } else if (numHomeIdTokens === 1) { this.commonLogger.info( "CacheManager:getIdToken - Multiple ID tokens found for account, defaulting to home tenant profile" ); return homeIdTokenMap.values().next().value; } else { // Multiple ID tokens for home tenant profile, remove all and return null tokensToBeRemoved = homeIdTokenMap; } } // Multiple tokens for a single tenant profile, remove all and return null this.commonLogger.info( "CacheManager:getIdToken - Multiple matching ID tokens found, clearing them" ); tokensToBeRemoved.forEach((idToken, key) => { this.removeIdToken(key); }); if (performanceClient && correlationId) { performanceClient.addFields( { multiMatchedID: idTokenMap.size }, correlationId ); } return null; } this.commonLogger.info("CacheManager:getIdToken - Returning ID token"); return idTokenMap.values().next().value; } /** * Gets all idTokens matching the given filter * @param filter * @returns */ getIdTokensByFilter( filter: CredentialFilter, tokenKeys?: TokenKeys ): Map<string, IdTokenEntity> { const idTokenKeys = (tokenKeys && tokenKeys.idToken) || this.getTokenKeys().idToken; const idTokens: Map<string, IdTokenEntity> = new Map< string, IdTokenEntity >(); idTokenKeys.forEach((key) => { if ( !this.idTokenKeyMatchesFilter(key, { clientId: this.clientId, ...filter, }) ) { return; } const idToken = this.getIdTokenCredential(key); if (idToken && this.credentialMatchesFilter(idToken, filter)) { idTokens.set(key, idToken); } }); return idTokens; } /** * Validate the cache key against filter before retrieving and parsing cache value * @param key * @param filter * @returns */ idTokenKeyMatchesFilter( inputKey: string, filter: CredentialFilter ): boolean { const key = inputKey.toLowerCase(); if ( filter.clientId && key.indexOf(filter.clientId.toLowerCase()) === -1 ) { return false; } if ( filter.homeAccountId && key.indexOf(filter.homeAccountId.toLowerCase()) === -1 ) { return false; } return true; } /** * Removes idToken from the cache * @param key */ removeIdToken(key: string): void { this.removeItem(key); } /** * Removes refresh token from the cache * @param key */ removeRefreshToken(key: string): void { this.removeItem(key); } /** * Retrieve AccessTokenEntity from cache * @param account {AccountInfo} * @param request {BaseAuthRequest} * @param tokenKeys {?TokenKeys} * @param performanceClient {?IPerformanceClient} * @param correlationId {?string} */ getAccessToken( account: AccountInfo, request: BaseAuthRequest, tokenKeys?: TokenKeys, targetRealm?: string, performanceClient?: IPerformanceClient, correlationId?: string ): AccessTokenEntity | null { this.commonLogger.trace("CacheManager - getAccessToken called"); const scopes = ScopeSet.createSearchScopes(request.scopes); const authScheme = request.authenticationScheme || AuthenticationScheme.BEARER; /* * Distinguish between Bearer and PoP/SSH token cache types * Cast to lowercase to handle "bearer" from ADFS */ const credentialType = authScheme && authScheme.toLowerCase() !== AuthenticationScheme.BEARER.toLowerCase() ? CredentialType.ACCESS_TOKEN_WITH_AUTH_SCHEME : CredentialType.ACCESS_TOKEN; const accessTokenFilter: CredentialFilter = { homeAccountId: account.homeAccountId, environment: account.environment, credentialType: credentialType, clientId: this.clientId, realm: targetRealm || account.tenantId, target: scopes, tokenType: authScheme, keyId: request.sshKid, requestedClaimsHash: request.requestedClaimsHash, }; const accessTokenKeys = (tokenKeys && tokenKeys.accessToken) || this.getTokenKeys().accessToken; const accessTokens: AccessTokenEntity[] = []; accessTokenKeys.forEach((key) => { // Validate key if ( this.accessTokenKeyMatchesFilter(key, accessTokenFilter, true) ) { const accessToken = this.getAccessTokenCredential(key); // Validate value if ( accessToken && this.credentialMatchesFilter(accessToken, accessTokenFilter) ) { accessTokens.push(accessToken); } } }); const numAccessTokens = accessTokens.length; if (numAccessTokens < 1) { this.commonLogger.info( "CacheManager:getAccessToken - No token found" ); return null; } else if (numAccessTokens > 1) { this.commonLogger.info( "CacheManager:getAccessToken - Multiple access tokens found, clearing them" ); accessTokens.forEach((accessToken) => { void this.removeAccessToken(generateCredentialKey(accessToken)); }); if (performanceClient && correlationId) { performanceClient.addFields( { multiMatchedAT: accessTokens.length }, correlationId ); } return null; } this.commonLogger.info( "CacheManager:getAccessToken - Returning access token" ); return accessTokens[0]; } /** * Validate the cache key against filter before retrieving and parsing cache value * @param key * @param filter * @param keyMustContainAllScopes * @returns */ accessTokenKeyMatchesFilter( inputKey: string, filter: CredentialFilter, keyMustContainAllScopes: boolean ): boolean { const key = inputKey.toLowerCase(); if ( filter.clientId && key.indexOf(filter.clientId.toLowerCase()) === -1 ) { return false; } if ( filter.homeAccountId && key.indexOf(filter.homeAccountId.toLowerCase()) === -1 ) { return false; } if (filter.realm && key.indexOf(filter.realm.toLowerCase()) === -1) { return false; } if ( filter.requestedClaimsHash && key.indexOf(filter.requestedClaimsHash.toLowerCase()) === -1 ) { return false; } if (filter.target) { const scopes = filter.target.asArray(); for (let i = 0; i < scopes.length; i++) { if ( keyMustContainAllScopes && !key.includes(scopes[i].toLowerCase()) ) { // When performing a cache lookup a missing scope would be a cache miss return false; } else if ( !keyMustContainAllScopes && key.includes(scopes[i].toLowerCase()) ) { // When performing a cache write, any token with a subset of requested scopes should be replaced return true; } } } return true; } /** * Gets all access tokens matching the filter * @param filter * @returns */ getAccessTokensByFilter(filter: CredentialFilter): AccessTokenEntity[] { const tokenKeys = this.getTokenKeys(); const accessTokens: AccessTokenEntity[] = []; tokenKeys.accessToken.forEach((key) => { if (!this.accessTokenKeyMatchesFilter(key, filter, true)) { return; } const accessToken = this.getAccessTokenCredential(key); if ( accessToken && this.credentialMatchesFilter(accessToken, filter) ) { accessTokens.push(accessToken); } }); return accessTokens; } /** * Helper to retrieve the appropriate refresh token from cache * @param account {AccountInfo} * @param familyRT {boolean} * @param tokenKeys {?TokenKeys} * @param performanceClient {?IPerformanceClient} * @param correlationId {?string} */ getRefreshToken( account: AccountInfo, familyRT: boolean, tokenKeys?: TokenKeys, performanceClient?: IPerformanceClient, correlationId?: string ): RefreshTokenEntity | null { this.commonLogger.trace("CacheManager - getRefreshToken called"); const id = familyRT ? THE_FAMILY_ID : undefined; const refreshTokenFilter: CredentialFilter = { homeAccountId: account.homeAccountId, environment: account.environment, credentialType: CredentialType.REFRESH_TOKEN, clientId: this.clientId, familyId: id, }; const refreshTokenKeys = (tokenKeys && tokenKeys.refreshToken) || this.getTokenKeys().refreshToken; const refreshTokens: RefreshTokenEntity[] = []; refreshTokenKeys.forEach((key) => { // Validate key if (this.refreshTokenKeyMatchesFilter(key, refreshTokenFilter)) { const refreshToken = this.getRefreshTokenCredential(key); // Validate value if ( refreshToken && this.credentialMatchesFilter( refreshToken, refreshTokenFilter ) ) { refreshTokens.push(refreshToken); } } }); const numRefreshTokens = refreshTokens.length; if (numRefreshTokens < 1) { this.commonLogger.info( "CacheManager:getRefreshToken - No refresh token found." ); return null; } // address the else case after remove functions address environment aliases if (numRefreshTokens > 1 && performanceClient && correlationId) { performanceClient.addFields( { multiMatchedRT: numRefreshTokens }, correlationId ); } this.commonLogger.info( "CacheManager:getRefreshToken - returning refresh token" ); return refreshTokens[0] as RefreshTokenEntity; } /** * Validate the cache key against filter before retrieving and parsing cache value * @param key * @param filter */ refreshTokenKeyMatchesFilter( inputKey: string, filter: CredentialFilter ): boolean { const key = inputKey.toLowerCase(); if ( filter.familyId && key.indexOf(filter.familyId.toLowerCase()) === -1 ) { return false; } // If familyId is used, clientId is not in the key if ( !filter.familyId && filter.clientId && key.indexOf(filter.clientId.toLowerCase()) === -1 ) { return false; } if ( filter.homeAccountId && key.indexOf(filter.homeAccountId.toLowerCase()) === -1 ) { return false; } return true; } /** * Retrieve AppMetadataEntity from cache */ readAppMetadataFromCache(environment: string): AppMetadataEntity | null { const appMetadataFilter: AppMetadataFilter = { environment, clientId: this.clientId, }; const appMetadata: AppMetadataCache = this.getAppMetadataFilteredBy(appMetadataFilter); const appMetadataEntries: AppMetadataEntity[] = Object.keys( appMetadata ).map((key) => appMetadata[key]); const numAppMetadata = appMetadataEntries.length; if (numAppMetadata < 1) { return null; } else if (numAppMetadata > 1) { throw createClientAuthError( ClientAuthErrorCodes.multipleMatchingAppMetadata ); } return appMetadataEntries[0] as AppMetadataEntity; } /** * Return the family_id value associated with FOCI * @param environment * @param clientId */ isAppMetadataFOCI(environment: string): boolean { const appMetadata = this.readAppMetadataFromCache(environment); return !!(appMetadata && appMetadata.familyId === THE_FAMILY_ID); } /** * helper to match account ids * @param value * @param homeAccountId */ private matchHomeAccountId( entity: AccountEntity | CredentialEntity,