1118 lines
41 KiB
JavaScript
1118 lines
41 KiB
JavaScript
/*! @azure/msal-common v14.4.0 2023-11-07 */
|
|
'use strict';
|
|
import { Separators, CredentialType, AuthenticationScheme, THE_FAMILY_ID, APP_METADATA, AUTHORITY_METADATA_CONSTANTS } from '../utils/Constants.mjs';
|
|
import { generateCredentialKey } from './utils/CacheHelpers.mjs';
|
|
import { ScopeSet } from '../request/ScopeSet.mjs';
|
|
import { AccountEntity } from './entities/AccountEntity.mjs';
|
|
import { createClientAuthError } from '../error/ClientAuthError.mjs';
|
|
import { extractTokenClaims } from '../account/AuthToken.mjs';
|
|
import { name, version } from '../packageMetadata.mjs';
|
|
import { getAliasesFromStaticSources } from '../authority/AuthorityMetadata.mjs';
|
|
import { invalidCacheRecord, bindingKeyNotRemoved, multipleMatchingAppMetadata, methodNotImplemented } from '../error/ClientAuthErrorCodes.mjs';
|
|
|
|
/*
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License.
|
|
*/
|
|
/**
|
|
* Interface class which implement cache storage functions used by MSAL to perform validity checks, and store tokens.
|
|
* @internal
|
|
*/
|
|
class CacheManager {
|
|
constructor(clientId, cryptoImpl, logger, staticAuthorityOptions) {
|
|
this.clientId = clientId;
|
|
this.cryptoImpl = cryptoImpl;
|
|
this.commonLogger = logger.clone(name, version);
|
|
this.staticAuthorityOptions = staticAuthorityOptions;
|
|
}
|
|
/**
|
|
* 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) {
|
|
const validAccounts = [];
|
|
this.getAccountsFilteredBy(accountFilter || {}).forEach((accountEntity) => {
|
|
const accountInfo = this.getAccountInfoFromEntity(accountEntity, accountFilter);
|
|
if (accountInfo) {
|
|
validAccounts.push(accountInfo);
|
|
}
|
|
});
|
|
return validAccounts;
|
|
}
|
|
/**
|
|
* Gets accountInfo object based on provided filters
|
|
*/
|
|
getAccountInfoFilteredBy(accountFilter) {
|
|
const allAccounts = this.getAllAccounts(accountFilter);
|
|
if (allAccounts.length > 1) {
|
|
// If one or more accounts are found, further filter to the first account that has an ID token
|
|
return allAccounts.filter((account) => {
|
|
return !!account.idTokenClaims;
|
|
})[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) {
|
|
const accountEntities = this.getAccountsFilteredBy(accountFilter);
|
|
if (accountEntities.length > 0) {
|
|
return accountEntities[0].getAccountInfo();
|
|
}
|
|
else {
|
|
return null;
|
|
}
|
|
}
|
|
getAccountInfoFromEntity(accountEntity, accountFilter) {
|
|
const accountInfo = accountEntity.getAccountInfo();
|
|
const idToken = this.getIdToken(accountInfo);
|
|
if (idToken) {
|
|
const idTokenClaims = extractTokenClaims(idToken.secret, this.cryptoImpl.base64Decode);
|
|
if (this.idTokenClaimsMatchAccountFilter(idTokenClaims, accountFilter)) {
|
|
accountInfo.idToken = idToken.secret;
|
|
accountInfo.idTokenClaims = idTokenClaims;
|
|
return accountInfo;
|
|
}
|
|
}
|
|
return accountInfo;
|
|
}
|
|
idTokenClaimsMatchAccountFilter(idTokenClaims, accountFilter) {
|
|
if (accountFilter) {
|
|
if (!!accountFilter.loginHint &&
|
|
!this.matchLoginHint(idTokenClaims, accountFilter.loginHint)) {
|
|
return false;
|
|
}
|
|
if (!!accountFilter.sid &&
|
|
!this.matchSid(idTokenClaims, accountFilter.sid)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
/**
|
|
* saves a cache record
|
|
* @param cacheRecord
|
|
*/
|
|
async saveCacheRecord(cacheRecord, storeInCache) {
|
|
if (!cacheRecord) {
|
|
throw createClientAuthError(invalidCacheRecord);
|
|
}
|
|
if (!!cacheRecord.account) {
|
|
this.setAccount(cacheRecord.account);
|
|
}
|
|
if (!!cacheRecord.idToken && storeInCache?.idToken !== false) {
|
|
this.setIdTokenCredential(cacheRecord.idToken);
|
|
}
|
|
if (!!cacheRecord.accessToken && storeInCache?.accessToken !== false) {
|
|
await this.saveAccessToken(cacheRecord.accessToken);
|
|
}
|
|
if (!!cacheRecord.refreshToken &&
|
|
storeInCache?.refreshToken !== false) {
|
|
this.setRefreshTokenCredential(cacheRecord.refreshToken);
|
|
}
|
|
if (!!cacheRecord.appMetadata) {
|
|
this.setAppMetadata(cacheRecord.appMetadata);
|
|
}
|
|
}
|
|
/**
|
|
* saves access token credential
|
|
* @param credential
|
|
*/
|
|
async saveAccessToken(credential) {
|
|
const accessTokenFilter = {
|
|
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 = [];
|
|
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);
|
|
this.setAccessTokenCredential(credential);
|
|
}
|
|
/**
|
|
* Retrieve accounts matching all provided filters; if no filter is set, get all accounts
|
|
* 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) {
|
|
const allAccountKeys = this.getAccountKeys();
|
|
const matchingAccounts = [];
|
|
allAccountKeys.forEach((cacheKey) => {
|
|
if (!this.isAccountKey(cacheKey, accountFilter.homeAccountId, accountFilter.tenantId)) {
|
|
// Don't parse value if the key doesn't match the account filters
|
|
return;
|
|
}
|
|
const entity = this.getAccount(cacheKey);
|
|
if (!entity) {
|
|
return;
|
|
}
|
|
if (!!accountFilter.homeAccountId &&
|
|
!this.matchHomeAccountId(entity, accountFilter.homeAccountId)) {
|
|
return;
|
|
}
|
|
if (!!accountFilter.localAccountId &&
|
|
!this.matchLocalAccountId(entity, accountFilter.localAccountId)) {
|
|
return;
|
|
}
|
|
if (!!accountFilter.username &&
|
|
!this.matchUsername(entity, accountFilter.username)) {
|
|
return;
|
|
}
|
|
if (!!accountFilter.environment &&
|
|
!this.matchEnvironment(entity, accountFilter.environment)) {
|
|
return;
|
|
}
|
|
if (!!accountFilter.realm &&
|
|
!this.matchRealm(entity, accountFilter.realm)) {
|
|
return;
|
|
}
|
|
// tenantId is another name for realm
|
|
if (!!accountFilter.tenantId &&
|
|
!this.matchRealm(entity, accountFilter.tenantId)) {
|
|
return;
|
|
}
|
|
if (!!accountFilter.nativeAccountId &&
|
|
!this.matchNativeAccountId(entity, accountFilter.nativeAccountId)) {
|
|
return;
|
|
}
|
|
if (!!accountFilter.authorityType &&
|
|
!this.matchAuthorityType(entity, accountFilter.authorityType)) {
|
|
return;
|
|
}
|
|
if (!!accountFilter.name &&
|
|
!this.matchName(entity, accountFilter.name)) {
|
|
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, homeAccountId, tenantId) {
|
|
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) {
|
|
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, filter) {
|
|
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) {
|
|
return this.getAppMetadataFilteredByInternal(filter.environment, filter.clientId);
|
|
}
|
|
/**
|
|
* Support function to help match appMetadata
|
|
* @param environment
|
|
* @param clientId
|
|
*/
|
|
getAppMetadataFilteredByInternal(environment, clientId) {
|
|
const allCacheKeys = this.getKeys();
|
|
const matchingAppMetadata = {};
|
|
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 (!!environment && !this.matchEnvironment(entity, environment)) {
|
|
return;
|
|
}
|
|
if (!!clientId && !this.matchClientId(entity, clientId)) {
|
|
return;
|
|
}
|
|
matchingAppMetadata[cacheKey] = entity;
|
|
});
|
|
return matchingAppMetadata;
|
|
}
|
|
/**
|
|
* retrieve authorityMetadata that contains a matching alias
|
|
* @param filter
|
|
*/
|
|
getAuthorityMetadataByAlias(host) {
|
|
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() {
|
|
const allAccountKeys = this.getAccountKeys();
|
|
const removedAccounts = [];
|
|
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) {
|
|
const account = this.getAccount(accountKey);
|
|
if (!account) {
|
|
return;
|
|
}
|
|
await this.removeAccountContext(account);
|
|
this.removeItem(accountKey);
|
|
}
|
|
/**
|
|
* Removes credentials associated with the provided account
|
|
* @param account
|
|
*/
|
|
async removeAccountContext(account) {
|
|
const allTokenKeys = this.getTokenKeys();
|
|
const accountId = account.generateAccountId();
|
|
const removedCredentials = [];
|
|
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) {
|
|
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;
|
|
const kid = accessTokenWithAuthSchemeEntity.keyId;
|
|
if (kid) {
|
|
try {
|
|
await this.cryptoImpl.removeTokenBindingKey(kid);
|
|
}
|
|
catch (error) {
|
|
throw createClientAuthError(bindingKeyNotRemoved);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return this.removeItem(key);
|
|
}
|
|
/**
|
|
* Removes all app metadata objects from cache.
|
|
*/
|
|
removeAppMetadata() {
|
|
const allCacheKeys = this.getKeys();
|
|
allCacheKeys.forEach((cacheKey) => {
|
|
if (this.isAppMetadata(cacheKey)) {
|
|
this.removeItem(cacheKey);
|
|
}
|
|
});
|
|
return true;
|
|
}
|
|
/**
|
|
* Retrieve the cached credentials into a cacherecord
|
|
* @param account {AccountInfo}
|
|
* @param request {BaseAuthRequest}
|
|
* @param environment {string}
|
|
* @param performanceClient {?IPerformanceClient}
|
|
* @param correlationId {?string}
|
|
*/
|
|
readCacheRecord(account, request, environment, performanceClient, correlationId) {
|
|
const tokenKeys = this.getTokenKeys();
|
|
const cachedAccount = this.readAccountFromCache(account);
|
|
const cachedIdToken = this.getIdToken(account, tokenKeys, performanceClient, correlationId);
|
|
const cachedAccessToken = this.getAccessToken(account, request, tokenKeys, performanceClient, correlationId);
|
|
const cachedRefreshToken = this.getRefreshToken(account, false, tokenKeys, performanceClient, correlationId);
|
|
const cachedAppMetadata = this.readAppMetadataFromCache(environment);
|
|
if (cachedAccount && cachedIdToken) {
|
|
cachedAccount.idTokenClaims = extractTokenClaims(cachedIdToken.secret, this.cryptoImpl.base64Decode);
|
|
}
|
|
return {
|
|
account: cachedAccount,
|
|
idToken: cachedIdToken,
|
|
accessToken: cachedAccessToken,
|
|
refreshToken: cachedRefreshToken,
|
|
appMetadata: cachedAppMetadata,
|
|
};
|
|
}
|
|
/**
|
|
* Retrieve AccountEntity from cache
|
|
* @param account
|
|
*/
|
|
readAccountFromCache(account) {
|
|
const accountKey = AccountEntity.generateAccountCacheKey(account);
|
|
return this.getAccount(accountKey);
|
|
}
|
|
/**
|
|
* Retrieve IdTokenEntity from cache
|
|
* @param account {AccountInfo}
|
|
* @param tokenKeys {?TokenKeys}
|
|
* @param performanceClient {?IPerformanceClient}
|
|
* @param correlationId {?string}
|
|
*/
|
|
getIdToken(account, tokenKeys, performanceClient, correlationId) {
|
|
this.commonLogger.trace("CacheManager - getIdToken called");
|
|
const idTokenFilter = {
|
|
homeAccountId: account.homeAccountId,
|
|
environment: account.environment,
|
|
credentialType: CredentialType.ID_TOKEN,
|
|
clientId: this.clientId,
|
|
realm: account.tenantId,
|
|
};
|
|
const idTokens = this.getIdTokensByFilter(idTokenFilter, tokenKeys);
|
|
const numIdTokens = idTokens.length;
|
|
if (numIdTokens < 1) {
|
|
this.commonLogger.info("CacheManager:getIdToken - No token found");
|
|
return null;
|
|
}
|
|
else if (numIdTokens > 1) {
|
|
this.commonLogger.info("CacheManager:getIdToken - Multiple id tokens found, clearing them");
|
|
idTokens.forEach((idToken) => {
|
|
this.removeIdToken(generateCredentialKey(idToken));
|
|
});
|
|
if (performanceClient && correlationId) {
|
|
performanceClient.addFields({ multiMatchedID: idTokens.length }, correlationId);
|
|
}
|
|
return null;
|
|
}
|
|
this.commonLogger.info("CacheManager:getIdToken - Returning id token");
|
|
return idTokens[0];
|
|
}
|
|
/**
|
|
* Gets all idTokens matching the given filter
|
|
* @param filter
|
|
* @returns
|
|
*/
|
|
getIdTokensByFilter(filter, tokenKeys) {
|
|
const idTokenKeys = (tokenKeys && tokenKeys.idToken) || this.getTokenKeys().idToken;
|
|
const idTokens = [];
|
|
idTokenKeys.forEach((key) => {
|
|
if (!this.idTokenKeyMatchesFilter(key, {
|
|
clientId: this.clientId,
|
|
...filter,
|
|
})) {
|
|
return;
|
|
}
|
|
const idToken = this.getIdTokenCredential(key);
|
|
if (idToken && this.credentialMatchesFilter(idToken, filter)) {
|
|
idTokens.push(idToken);
|
|
}
|
|
});
|
|
return idTokens;
|
|
}
|
|
/**
|
|
* Validate the cache key against filter before retrieving and parsing cache value
|
|
* @param key
|
|
* @param filter
|
|
* @returns
|
|
*/
|
|
idTokenKeyMatchesFilter(inputKey, filter) {
|
|
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) {
|
|
this.removeItem(key);
|
|
}
|
|
/**
|
|
* Removes refresh token from the cache
|
|
* @param key
|
|
*/
|
|
removeRefreshToken(key) {
|
|
this.removeItem(key);
|
|
}
|
|
/**
|
|
* Retrieve AccessTokenEntity from cache
|
|
* @param account {AccountInfo}
|
|
* @param request {BaseAuthRequest}
|
|
* @param tokenKeys {?TokenKeys}
|
|
* @param performanceClient {?IPerformanceClient}
|
|
* @param correlationId {?string}
|
|
*/
|
|
getAccessToken(account, request, tokenKeys, performanceClient, correlationId) {
|
|
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 = {
|
|
homeAccountId: account.homeAccountId,
|
|
environment: account.environment,
|
|
credentialType: credentialType,
|
|
clientId: this.clientId,
|
|
realm: account.tenantId,
|
|
target: scopes,
|
|
tokenType: authScheme,
|
|
keyId: request.sshKid,
|
|
requestedClaimsHash: request.requestedClaimsHash,
|
|
};
|
|
const accessTokenKeys = (tokenKeys && tokenKeys.accessToken) ||
|
|
this.getTokenKeys().accessToken;
|
|
const accessTokens = [];
|
|
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, filter, keyMustContainAllScopes) {
|
|
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) {
|
|
const tokenKeys = this.getTokenKeys();
|
|
const accessTokens = [];
|
|
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, familyRT, tokenKeys, performanceClient, correlationId) {
|
|
this.commonLogger.trace("CacheManager - getRefreshToken called");
|
|
const id = familyRT ? THE_FAMILY_ID : undefined;
|
|
const refreshTokenFilter = {
|
|
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 = [];
|
|
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];
|
|
}
|
|
/**
|
|
* Validate the cache key against filter before retrieving and parsing cache value
|
|
* @param key
|
|
* @param filter
|
|
*/
|
|
refreshTokenKeyMatchesFilter(inputKey, filter) {
|
|
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) {
|
|
const appMetadataFilter = {
|
|
environment,
|
|
clientId: this.clientId,
|
|
};
|
|
const appMetadata = this.getAppMetadataFilteredBy(appMetadataFilter);
|
|
const appMetadataEntries = Object.keys(appMetadata).map((key) => appMetadata[key]);
|
|
const numAppMetadata = appMetadataEntries.length;
|
|
if (numAppMetadata < 1) {
|
|
return null;
|
|
}
|
|
else if (numAppMetadata > 1) {
|
|
throw createClientAuthError(multipleMatchingAppMetadata);
|
|
}
|
|
return appMetadataEntries[0];
|
|
}
|
|
/**
|
|
* Return the family_id value associated with FOCI
|
|
* @param environment
|
|
* @param clientId
|
|
*/
|
|
isAppMetadataFOCI(environment) {
|
|
const appMetadata = this.readAppMetadataFromCache(environment);
|
|
return !!(appMetadata && appMetadata.familyId === THE_FAMILY_ID);
|
|
}
|
|
/**
|
|
* helper to match account ids
|
|
* @param value
|
|
* @param homeAccountId
|
|
*/
|
|
matchHomeAccountId(entity, homeAccountId) {
|
|
return !!(typeof entity.homeAccountId === "string" &&
|
|
homeAccountId === entity.homeAccountId);
|
|
}
|
|
/**
|
|
* helper to match account ids
|
|
* @param entity
|
|
* @param localAccountId
|
|
* @returns
|
|
*/
|
|
matchLocalAccountId(entity, localAccountId) {
|
|
return !!(typeof entity.localAccountId === "string" &&
|
|
localAccountId === entity.localAccountId);
|
|
}
|
|
/**
|
|
* helper to match usernames
|
|
* @param entity
|
|
* @param username
|
|
* @returns
|
|
*/
|
|
matchUsername(entity, username) {
|
|
return !!(typeof entity.username === "string" &&
|
|
username.toLowerCase() === entity.username.toLowerCase());
|
|
}
|
|
/**
|
|
* helper to match names
|
|
* @param entity
|
|
* @param name
|
|
* @returns true if the downcased name properties are present and match in the filter and the entity
|
|
*/
|
|
matchName(entity, name) {
|
|
return !!(name.toLowerCase() === entity.name?.toLowerCase());
|
|
}
|
|
/**
|
|
* helper to match assertion
|
|
* @param value
|
|
* @param oboAssertion
|
|
*/
|
|
matchUserAssertionHash(entity, userAssertionHash) {
|
|
return !!(entity.userAssertionHash &&
|
|
userAssertionHash === entity.userAssertionHash);
|
|
}
|
|
/**
|
|
* helper to match environment
|
|
* @param value
|
|
* @param environment
|
|
*/
|
|
matchEnvironment(entity, environment) {
|
|
// Check static authority options first for cases where authority metadata has not been resolved and cached yet
|
|
if (this.staticAuthorityOptions) {
|
|
const staticAliases = getAliasesFromStaticSources(this.staticAuthorityOptions, this.commonLogger);
|
|
if (staticAliases.includes(environment) &&
|
|
staticAliases.includes(entity.environment)) {
|
|
return true;
|
|
}
|
|
}
|
|
// Query metadata cache if no static authority configuration has aliases that match enviroment
|
|
const cloudMetadata = this.getAuthorityMetadataByAlias(environment);
|
|
if (cloudMetadata &&
|
|
cloudMetadata.aliases.indexOf(entity.environment) > -1) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
/**
|
|
* helper to match credential type
|
|
* @param entity
|
|
* @param credentialType
|
|
*/
|
|
matchCredentialType(entity, credentialType) {
|
|
return (entity.credentialType &&
|
|
credentialType.toLowerCase() === entity.credentialType.toLowerCase());
|
|
}
|
|
/**
|
|
* helper to match client ids
|
|
* @param entity
|
|
* @param clientId
|
|
*/
|
|
matchClientId(entity, clientId) {
|
|
return !!(entity.clientId && clientId === entity.clientId);
|
|
}
|
|
/**
|
|
* helper to match family ids
|
|
* @param entity
|
|
* @param familyId
|
|
*/
|
|
matchFamilyId(entity, familyId) {
|
|
return !!(entity.familyId && familyId === entity.familyId);
|
|
}
|
|
/**
|
|
* helper to match realm
|
|
* @param entity
|
|
* @param realm
|
|
*/
|
|
matchRealm(entity, realm) {
|
|
return !!(entity.realm && realm === entity.realm);
|
|
}
|
|
/**
|
|
* helper to match nativeAccountId
|
|
* @param entity
|
|
* @param nativeAccountId
|
|
* @returns boolean indicating the match result
|
|
*/
|
|
matchNativeAccountId(entity, nativeAccountId) {
|
|
return !!(entity.nativeAccountId && nativeAccountId === entity.nativeAccountId);
|
|
}
|
|
/**
|
|
* helper to match loginHint which can be either:
|
|
* 1. login_hint ID token claim
|
|
* 2. username in cached account object
|
|
* 3. upn in ID token claims
|
|
* @param entity
|
|
* @param loginHint
|
|
* @returns
|
|
*/
|
|
matchLoginHint(idTokenClaims, loginHint) {
|
|
if (idTokenClaims?.login_hint === loginHint) {
|
|
return true;
|
|
}
|
|
if (idTokenClaims.preferred_username === loginHint) {
|
|
return true;
|
|
}
|
|
if (idTokenClaims?.upn === loginHint) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
/**
|
|
* Helper to match sid
|
|
* @param idTokenClaims
|
|
* @param sid
|
|
* @returns true if the sid claim is present and matches the filter
|
|
*/
|
|
matchSid(idTokenClaims, sid) {
|
|
return !!(idTokenClaims?.sid && idTokenClaims.sid === sid);
|
|
}
|
|
matchAuthorityType(entity, authorityType) {
|
|
return !!(entity.authorityType &&
|
|
authorityType.toLowerCase() === entity.authorityType.toLowerCase());
|
|
}
|
|
/**
|
|
* Returns true if the target scopes are a subset of the current entity's scopes, false otherwise.
|
|
* @param entity
|
|
* @param target
|
|
*/
|
|
matchTarget(entity, target) {
|
|
const isNotAccessTokenCredential = entity.credentialType !== CredentialType.ACCESS_TOKEN &&
|
|
entity.credentialType !==
|
|
CredentialType.ACCESS_TOKEN_WITH_AUTH_SCHEME;
|
|
if (isNotAccessTokenCredential || !entity.target) {
|
|
return false;
|
|
}
|
|
const entityScopeSet = ScopeSet.fromString(entity.target);
|
|
return entityScopeSet.containsScopeSet(target);
|
|
}
|
|
/**
|
|
* Returns true if the credential's tokenType or Authentication Scheme matches the one in the request, false otherwise
|
|
* @param entity
|
|
* @param tokenType
|
|
*/
|
|
matchTokenType(entity, tokenType) {
|
|
return !!(entity.tokenType && entity.tokenType === tokenType);
|
|
}
|
|
/**
|
|
* Returns true if the credential's keyId matches the one in the request, false otherwise
|
|
* @param entity
|
|
* @param tokenType
|
|
*/
|
|
matchKeyId(entity, keyId) {
|
|
return !!(entity.keyId && entity.keyId === keyId);
|
|
}
|
|
/**
|
|
* returns if a given cache entity is of the type appmetadata
|
|
* @param key
|
|
*/
|
|
isAppMetadata(key) {
|
|
return key.indexOf(APP_METADATA) !== -1;
|
|
}
|
|
/**
|
|
* returns if a given cache entity is of the type authoritymetadata
|
|
* @param key
|
|
*/
|
|
isAuthorityMetadata(key) {
|
|
return key.indexOf(AUTHORITY_METADATA_CONSTANTS.CACHE_KEY) !== -1;
|
|
}
|
|
/**
|
|
* returns cache key used for cloud instance metadata
|
|
*/
|
|
generateAuthorityMetadataCacheKey(authority) {
|
|
return `${AUTHORITY_METADATA_CONSTANTS.CACHE_KEY}-${this.clientId}-${authority}`;
|
|
}
|
|
/**
|
|
* Helper to convert serialized data to object
|
|
* @param obj
|
|
* @param json
|
|
*/
|
|
static toObject(obj, json) {
|
|
for (const propertyName in json) {
|
|
obj[propertyName] = json[propertyName];
|
|
}
|
|
return obj;
|
|
}
|
|
}
|
|
/** @internal */
|
|
class DefaultStorageClass extends CacheManager {
|
|
setAccount() {
|
|
throw createClientAuthError(methodNotImplemented);
|
|
}
|
|
getAccount() {
|
|
throw createClientAuthError(methodNotImplemented);
|
|
}
|
|
setIdTokenCredential() {
|
|
throw createClientAuthError(methodNotImplemented);
|
|
}
|
|
getIdTokenCredential() {
|
|
throw createClientAuthError(methodNotImplemented);
|
|
}
|
|
setAccessTokenCredential() {
|
|
throw createClientAuthError(methodNotImplemented);
|
|
}
|
|
getAccessTokenCredential() {
|
|
throw createClientAuthError(methodNotImplemented);
|
|
}
|
|
setRefreshTokenCredential() {
|
|
throw createClientAuthError(methodNotImplemented);
|
|
}
|
|
getRefreshTokenCredential() {
|
|
throw createClientAuthError(methodNotImplemented);
|
|
}
|
|
setAppMetadata() {
|
|
throw createClientAuthError(methodNotImplemented);
|
|
}
|
|
getAppMetadata() {
|
|
throw createClientAuthError(methodNotImplemented);
|
|
}
|
|
setServerTelemetry() {
|
|
throw createClientAuthError(methodNotImplemented);
|
|
}
|
|
getServerTelemetry() {
|
|
throw createClientAuthError(methodNotImplemented);
|
|
}
|
|
setAuthorityMetadata() {
|
|
throw createClientAuthError(methodNotImplemented);
|
|
}
|
|
getAuthorityMetadata() {
|
|
throw createClientAuthError(methodNotImplemented);
|
|
}
|
|
getAuthorityMetadataKeys() {
|
|
throw createClientAuthError(methodNotImplemented);
|
|
}
|
|
setThrottlingCache() {
|
|
throw createClientAuthError(methodNotImplemented);
|
|
}
|
|
getThrottlingCache() {
|
|
throw createClientAuthError(methodNotImplemented);
|
|
}
|
|
removeItem() {
|
|
throw createClientAuthError(methodNotImplemented);
|
|
}
|
|
containsKey() {
|
|
throw createClientAuthError(methodNotImplemented);
|
|
}
|
|
getKeys() {
|
|
throw createClientAuthError(methodNotImplemented);
|
|
}
|
|
getAccountKeys() {
|
|
throw createClientAuthError(methodNotImplemented);
|
|
}
|
|
getTokenKeys() {
|
|
throw createClientAuthError(methodNotImplemented);
|
|
}
|
|
async clear() {
|
|
throw createClientAuthError(methodNotImplemented);
|
|
}
|
|
updateCredentialCacheKey() {
|
|
throw createClientAuthError(methodNotImplemented);
|
|
}
|
|
}
|
|
|
|
export { CacheManager, DefaultStorageClass };
|
|
//# sourceMappingURL=CacheManager.mjs.map
|