diff --git a/src/cache/ICacheStorage.ts b/src/cache/ICacheStorage.ts index 0c2f5289d0..7a9c53fbe5 100644 --- a/src/cache/ICacheStorage.ts +++ b/src/cache/ICacheStorage.ts @@ -29,6 +29,14 @@ export interface ICacheStorage { */ setItem(key: string, value: string): void; + /** + * Function to set item in Memory + * @param key + * @param value + * @param type + */ + setItemInMemory(key: string, value: object, type?: string): void; + /** * Function which retrieves item from cache. * @param key @@ -36,10 +44,24 @@ export interface ICacheStorage { getItem(key: string): string; /** - * Function which removes item from cache. + * Function to get an item from memory + * @param key + * @param type + */ + getItemFromMemory(key: string, type?: string): object; + + /** + * Function to remove an item from cache given its key. + * @param key + */ + removeItem(key: string): boolean; + + /** + * Function to remove an item from memory given its key * @param key + * @param type */ - removeItem(key: string): void; + removeItemFromMemory(key: string, type?: string): boolean; /** * Function which returns boolean whether cache contains a specific key. diff --git a/src/config/ClientConfiguration.ts b/src/config/ClientConfiguration.ts index 0e94e6439d..a4c61b3b81 100644 --- a/src/config/ClientConfiguration.ts +++ b/src/config/ClientConfiguration.ts @@ -125,6 +125,10 @@ const DEFAULT_STORAGE_IMPLEMENTATION: ICacheStorage = { const notImplErr = "Storage interface - getItem() has not been implemented for the cacheStorage interface."; throw AuthError.createUnexpectedError(notImplErr); }, + getItemFromMemory: (): object => { + const notImplErr = "Storage interface - getItemFromMemory() has not been implemented for the cacheStorage interface."; + throw AuthError.createUnexpectedError(notImplErr); + }, getKeys: (): string[] => { const notImplErr = "Storage interface - getKeys() has not been implemented for the cacheStorage interface."; throw AuthError.createUnexpectedError(notImplErr); @@ -133,10 +137,18 @@ const DEFAULT_STORAGE_IMPLEMENTATION: ICacheStorage = { const notImplErr = "Storage interface - removeItem() has not been implemented for the cacheStorage interface."; throw AuthError.createUnexpectedError(notImplErr); }, + removeItemFromMemory: () => { + const notImplErr = "Storage interface - removeItemFromMemory() has not been implemented for the cacheStorage interface."; + throw AuthError.createUnexpectedError(notImplErr); + }, setItem: () => { const notImplErr = "Storage interface - setItem() has not been implemented for the cacheStorage interface."; throw AuthError.createUnexpectedError(notImplErr); }, + setItemInMemory: () => { + const notImplErr = "Storage interface - setItemInMemory() has not been implemented for the cacheStorage interface."; + throw AuthError.createUnexpectedError(notImplErr); + }, getCache: (): InMemoryCache => { const notImplErr = "Storage interface - getCache() has not been implemented for the cacheStorage interface."; throw AuthError.createUnexpectedError(notImplErr); diff --git a/src/index.ts b/src/index.ts index 8458e675c9..811ab433c2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,6 +21,7 @@ export { UnifiedCacheManager } from "./unifiedCache/UnifiedCacheManager"; export { JsonCache, InMemoryCache } from "./unifiedCache/utils/CacheTypes"; export { Serializer } from "./unifiedCache/serialize/Serializer"; export { Deserializer } from "./unifiedCache/serialize/Deserializer"; +export { CacheHelper } from "./unifiedCache/utils/CacheHelper"; // Network Interface export { INetworkModule, NetworkRequestOptions } from "./network/INetworkModule"; export { NetworkResponse } from "./network/NetworkManager"; @@ -50,7 +51,7 @@ export { ClientAuthError, ClientAuthErrorMessage } from "./error/ClientAuthError export { ClientConfigurationError, ClientConfigurationErrorMessage } from "./error/ClientConfigurationError"; // Constants and Utils export { - Constants, PromptValue, TemporaryCacheKeys, PersistentCacheKeys, Prompt, ResponseMode + Constants, PromptValue, TemporaryCacheKeys, PersistentCacheKeys, Prompt, ResponseMode, CacheSchemaType, CredentialType } from "./utils/Constants"; export { StringUtils } from "./utils/StringUtils"; export { StringDict } from "./utils/MsalTypes"; diff --git a/src/response/AuthenticationResult.ts b/src/response/AuthenticationResult.ts index bb5e4acc01..8fcde3ca08 100644 --- a/src/response/AuthenticationResult.ts +++ b/src/response/AuthenticationResult.ts @@ -8,18 +8,15 @@ import { StringDict } from "../utils/MsalTypes"; /** * Result returned from the authority's token endpoint. */ -// TODO: Also consider making an external type and use this as internal export class AuthenticationResult { - // TODO this is temp class, it will be updated. - uniqueId: string; // TODO: Check applicability - tenantId: string; // TODO: Check applicability + uniqueId: string; + tenantId: string; scopes: Array; - tokenType: string; // TODO: get rid of this if we can idToken: string; idTokenClaims: StringDict; accessToken: string; expiresOn: Date; - extExpiresOn?: Date; // TODO: Check what this maps to in other libraries - userRequestState?: string; // TODO: remove, just check how state is handled in other libraries - familyId?: string; // TODO: Check wider audience + extExpiresOn?: Date; + state?: string; + familyId?: string; } diff --git a/src/response/ResponseHandler.ts b/src/response/ResponseHandler.ts index f3ee4552f4..1cebdfe34c 100644 --- a/src/response/ResponseHandler.ts +++ b/src/response/ResponseHandler.ts @@ -22,6 +22,7 @@ import { IdTokenEntity } from "../unifiedCache/entities/IdTokenEntity"; import { AccessTokenEntity } from "../unifiedCache/entities/AccessTokenEntity"; import { RefreshTokenEntity } from "../unifiedCache/entities/RefreshTokenEntity"; import { InteractionRequiredAuthError } from "../error/InteractionRequiredAuthError"; +import { CacheRecord } from "../unifiedCache/entities/CacheRecord"; /** * Class that handles response parsing. @@ -104,81 +105,32 @@ export class ResponseHandler { * @param state */ generateAuthenticationResult(serverTokenResponse: ServerAuthorizationTokenResponse, authority: Authority): AuthenticationResult { - // Retrieve current account if in Cache - // TODO: add this once the req for cache look up for tokens is confirmed - const authenticationResult = this.processTokenResponse(serverTokenResponse, authority); - - const environment = authority.canonicalAuthorityUrlComponents.HostNameAndPort; - this.addCredentialsToCache(authenticationResult, environment, serverTokenResponse.refresh_token); - - return authenticationResult; - } - - /** - * Returns a new AuthenticationResult with the data from original result filled with the relevant data. - * @param authenticationResult - * @param idTokenString(raw idToken in the server response) - */ - processTokenResponse(serverTokenResponse: ServerAuthorizationTokenResponse, authority: Authority): AuthenticationResult { - const authenticationResult: AuthenticationResult = { - uniqueId: "", - tenantId: "", - tokenType: "", - idToken: null, - idTokenClaims: null, - accessToken: "", - scopes: [], - expiresOn: null, - familyId: null - }; - - // IdToken + // create an idToken object (not entity) const idTokenObj = new IdToken(serverTokenResponse.id_token, this.cryptoObj); - // if account is not in cache, append it to the cache - this.addAccountToCache(serverTokenResponse, idTokenObj, authority); - - // TODO: Check how this changes for auth code response - const expiresSeconds = Number(idTokenObj.claims.exp); - if (expiresSeconds && !authenticationResult.expiresOn) { - authenticationResult.expiresOn = new Date(expiresSeconds * 1000); - } + // save the response tokens + const cacheRecord = this.generateCacheRecord(serverTokenResponse, idTokenObj, authority); + this.uCacheManager.saveCacheRecord(cacheRecord); // Expiration calculation const expiresInSeconds = TimeUtils.nowSeconds() + serverTokenResponse.expires_in; const extendedExpiresInSeconds = expiresInSeconds + serverTokenResponse.ext_expires_in; - // Set consented scopes in response - const responseScopes = ScopeSet.fromString(serverTokenResponse.scope, this.clientId, true); - return { - ...authenticationResult, + const responseScopes = ScopeSet.fromString(serverTokenResponse.scope, this.clientId, true); + const authenticationResult: AuthenticationResult = { uniqueId: idTokenObj.claims.oid || idTokenObj.claims.sub, tenantId: idTokenObj.claims.tid, + scopes: responseScopes.asArray(), idToken: idTokenObj.rawIdToken, idTokenClaims: idTokenObj.claims, accessToken: serverTokenResponse.access_token, expiresOn: new Date(expiresInSeconds), extExpiresOn: new Date(extendedExpiresInSeconds), - scopes: responseScopes.asArray(), - familyId: serverTokenResponse.foci, + familyId: serverTokenResponse.foci || null, }; - } - /** - * if Account is not in the cache, generateAccount and append it to the cache - * @param serverTokenResponse - * @param idToken - * @param authority - */ - addAccountToCache(serverTokenResponse: ServerAuthorizationTokenResponse, idToken: IdToken, authority: Authority): void { - const environment = authority.canonicalAuthorityUrlComponents.HostNameAndPort; - let accountEntity: AccountEntity; - const cachedAccount: AccountEntity = this.uCacheManager.getAccount(this.homeAccountIdentifier, environment, idToken.claims.tid); - if (!cachedAccount) { - accountEntity = this.generateAccountEntity(serverTokenResponse, idToken, authority); - this.uCacheManager.addAccountEntity(accountEntity); - } + return authenticationResult; } /** @@ -205,35 +157,53 @@ export class ResponseHandler { } /** - * Appends the minted tokens to the in-memory cache - * @param authenticationResult + * Generates CacheRecord + * @param serverTokenResponse + * @param idTokenObj * @param authority */ - addCredentialsToCache( - authenticationResult: AuthenticationResult, - authority: string, - refreshToken: string - ): void { - const idTokenEntity = IdTokenEntity.createIdTokenEntity( + generateCacheRecord(serverTokenResponse: ServerAuthorizationTokenResponse, idTokenObj: IdToken, authority: Authority): CacheRecord { + + const cacheRecord = new CacheRecord(); + + // Account + cacheRecord.account = this.generateAccountEntity( + serverTokenResponse, + idTokenObj, + authority + ); + + // IdToken + cacheRecord.idToken = IdTokenEntity.createIdTokenEntity( this.homeAccountIdentifier, - authenticationResult, + authority.canonicalAuthorityUrlComponents.HostNameAndPort, + serverTokenResponse.id_token, this.clientId, - authority + idTokenObj.claims.tid ); - const accessTokenEntity = AccessTokenEntity.createAccessTokenEntity( + + // AccessToken + const responseScopes = ScopeSet.fromString(serverTokenResponse.scope, this.clientId, true); + cacheRecord.accessToken = AccessTokenEntity.createAccessTokenEntity( this.homeAccountIdentifier, - authenticationResult, + authority.canonicalAuthorityUrlComponents.HostNameAndPort, + serverTokenResponse.access_token, this.clientId, - authority + idTokenObj.claims.tid, + responseScopes.asArray().join(" "), + serverTokenResponse.expires_in, + serverTokenResponse.ext_expires_in ); - const refreshTokenEntity = RefreshTokenEntity.createRefreshTokenEntity( + + // refreshToken + cacheRecord.refreshToken = RefreshTokenEntity.createRefreshTokenEntity( this.homeAccountIdentifier, - authenticationResult, - refreshToken, + authority.canonicalAuthorityUrlComponents.HostNameAndPort, + serverTokenResponse.refresh_token, this.clientId, - authority + serverTokenResponse.foci ); - this.uCacheManager.addCredentialCache(accessTokenEntity, idTokenEntity, refreshTokenEntity); + return cacheRecord; } } diff --git a/src/unifiedCache/UnifiedCacheManager.ts b/src/unifiedCache/UnifiedCacheManager.ts index dc34524db0..00eb40a1e0 100644 --- a/src/unifiedCache/UnifiedCacheManager.ts +++ b/src/unifiedCache/UnifiedCacheManager.ts @@ -3,51 +3,63 @@ * Licensed under the MIT License. */ -import { InMemoryCache, JsonCache } from "./utils/CacheTypes"; -import { Separators } from "../utils/Constants"; -import { AccessTokenEntity } from "./entities/AccessTokenEntity"; -import { IdTokenEntity } from "./entities/IdTokenEntity"; -import { RefreshTokenEntity } from "./entities/RefreshTokenEntity"; +import { InMemoryCache, JsonCache, AccountFilter, CredentialFilter } from "./utils/CacheTypes"; import { AccountEntity } from "./entities/AccountEntity"; import { ICacheStorage } from "../cache/ICacheStorage"; import { Deserializer } from "./serialize/Deserializer"; import { Serializer } from "./serialize/Serializer"; -import { AccountCache } from "./utils/CacheTypes"; - -export class UnifiedCacheManager { +import { Credential } from "./entities/Credential"; +import { + CredentialType, + CacheSchemaType +} from "../utils/Constants"; +import { + AccountCache, + CredentialCache, + IdTokenCache, + AccessTokenCache, + RefreshTokenCache, +} from "./utils/CacheTypes"; +import { ICacheManager } from "./interface/ICacheManager"; +import { CacheHelper } from "./utils/CacheHelper"; +import { CacheRecord } from "./entities/CacheRecord"; +export class UnifiedCacheManager implements ICacheManager { // Storage interface - private inMemoryCache: InMemoryCache; private cacheStorage: ICacheStorage; constructor(cacheImpl: ICacheStorage) { this.cacheStorage = cacheImpl; - this.inMemoryCache = this.cacheStorage.getCache(); } /** - * setter for in cache memory + * sets the inMemory cache + * @param cache */ setCacheInMemory(cache: InMemoryCache): void { - this.inMemoryCache = cache; + this.cacheStorage.setCache(cache); } /** - * get the cache in memory + * get the inMemory Cache */ getCacheInMemory(): InMemoryCache { - return this.inMemoryCache; + return this.cacheStorage.getCache(); } /** * Initialize in memory cache from an exisiting cache vault + * @param cache */ generateInMemoryCache(cache: string): InMemoryCache { - return Deserializer.deserializeAllCache(Deserializer.deserializeJSONBlob(cache)); + return Deserializer.deserializeAllCache( + Deserializer.deserializeJSONBlob(cache) + ); } /** * retrieves the final JSON + * @param inMemoryCache */ generateJsonCache(inMemoryCache: InMemoryCache): JsonCache { return Serializer.serializeAllCache(inMemoryCache); @@ -57,51 +69,290 @@ export class UnifiedCacheManager { * Returns all accounts in memory */ getAllAccounts(): AccountCache { - return this.inMemoryCache.accounts; + return this.getCacheInMemory().accounts; + } + + /** + * saves a cache record + * @param cacheRecord + */ + saveCacheRecord(cacheRecord: CacheRecord): void { + this.saveAccount(cacheRecord.account); + this.saveCredential(cacheRecord.idToken); + this.saveCredential(cacheRecord.accessToken); + this.saveCredential(cacheRecord.refreshToken); + } + + /** + * saves account into cache + * @param account + */ + saveAccount(account: AccountEntity): void { + const key = account.generateAccountKey(); + this.cacheStorage.setItemInMemory(key, account, CacheSchemaType.ACCOUNT); + } + + /** + * saves credential - accessToken, idToken or refreshToken into cache + * @param credential + */ + saveCredential(credential: Credential): void { + console.log("in UCacheManager saving credential"); + const key = credential.generateCredentialKey(); + this.cacheStorage.setItemInMemory(key, credential, CacheSchemaType.CREDENTIAL); + } + + /** + * Given account key retrieve an account + * @param key + */ + getAccount(key: string): AccountEntity { + return this.cacheStorage.getItemFromMemory(key, CacheSchemaType.ACCOUNT) as AccountEntity; + } + + /** + * retrieve a credential - accessToken, idToken or refreshToken; given the cache key + * @param key + */ + getCredential(key: string): Credential { + return this.cacheStorage.getItemFromMemory(key, CacheSchemaType.CREDENTIAL) as 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 homeAccountId + * @param environment + * @param realm + */ + getAccountsFilteredBy( + accountFilter: AccountFilter + ): AccountCache { + return this.getAccountsFilteredByInternal( + accountFilter.homeAccountId, + accountFilter.environment, + accountFilter.realm + ); } /** - * Returns if the account is in Cache + * 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 homeAccountId * @param environment * @param realm */ - getAccount(homeAccountId: string, environment: string, realm: string): AccountEntity { - const accountCacheKey: Array = [ + getAccountsFilteredByInternal( + homeAccountId?: string, + environment?: string, + realm?: string + ): AccountCache { + const accounts: AccountCache = this.getCacheInMemory().accounts; + const matchingAccounts: AccountCache = {}; + + let matches: boolean = true; + + Object.keys(accounts).forEach((key) => { + if (!!homeAccountId) { + matches = CacheHelper.matchHomeAccountId(key, homeAccountId); + } + + if (!!environment) { + matches = matches && CacheHelper.matchEnvironment(key, environment); + } + + if (!!realm) { + matches = matches && CacheHelper.matchRealm(key, realm); + } + + if (matches) { + matchingAccounts[key] = accounts[key]; + } + }); + + return matchingAccounts; + } + + /** + * retrieve credentails matching all provided filters; if no filter is set, get all credentials + * @param homeAccountId + * @param environment + * @param credentialType + * @param clientId + * @param realm + * @param target + */ + getCredentialsFilteredBy( + filter: CredentialFilter + ): CredentialCache { + return this.getCredentialsFilteredByInternal( + filter.homeAccountId, + filter.environment, + filter.clientId, + filter.realm, + filter.target + ); + } + + /** + * retrieve credentails matching all provided filters; if no filter is set, get all credentials + * @param homeAccountId + * @param environment + * @param credentialType + * @param clientId + * @param realm + * @param target + */ + getCredentialsFilteredByInternal( + homeAccountId?: string, + environment?: string, + credentialType?: string, + clientId?: string, + realm?: string, + target?: string + ): CredentialCache { + const matchingCredentials: CredentialCache = { + idTokens: {}, + accessTokens: {}, + refreshTokens: {}, + }; + + matchingCredentials.idTokens = this.getCredentialsFilteredByCredentialType( + this.getCacheInMemory().idTokens, homeAccountId, environment, - realm - ]; + credentialType, + clientId, + realm, + target + ) as IdTokenCache; - const accountKey = accountCacheKey.join(Separators.CACHE_KEY_SEPARATOR).toLowerCase(); + matchingCredentials.accessTokens = this.getCredentialsFilteredByCredentialType( + this.getCacheInMemory().accessTokens, + homeAccountId, + environment, + credentialType, + clientId, + realm, + target + ) as AccessTokenCache; + + matchingCredentials.refreshTokens = this.getCredentialsFilteredByCredentialType( + this.getCacheInMemory().refreshTokens, + homeAccountId, + environment, + credentialType, + clientId, + realm, + target + ) as RefreshTokenCache; - return this.inMemoryCache.accounts[accountKey] || null; + return matchingCredentials; } /** - * append credential cache to in memory cache - * @param idT: IdTokenEntity - * @param at: AccessTokenEntity - * @param rt: RefreshTokenEntity + * Support function to help match credentials + * @param cacheCredentials + * @param homeAccountId + * @param environment + * @param credentialType + * @param clientId + * @param realm + * @param target */ - addCredentialCache( - accessToken: AccessTokenEntity, - idToken: IdTokenEntity, - refreshToken: RefreshTokenEntity - ): void { - this.inMemoryCache.accessTokens[accessToken.generateCredentialKey()] = accessToken; - this.inMemoryCache.idTokens[idToken.generateCredentialKey()] = idToken; - this.inMemoryCache.refreshTokens[refreshToken.generateCredentialKey()] = refreshToken; + private getCredentialsFilteredByCredentialType( + cacheCredentials: object, + homeAccountId?: string, + environment?: string, + credentialType?: string, + clientId?: string, + realm?: string, + target?: string + ): Object { + const matchingCredentials = {}; + let matches: boolean = true; + + Object.keys(cacheCredentials).forEach((key) => { + if (!!homeAccountId) { + matches = CacheHelper.matchHomeAccountId( + key, + homeAccountId + ); + } + + if (!!environment) { + matches = matches && CacheHelper.matchEnvironment(key, environment); + } + + if (!!realm) { + matches = matches && CacheHelper.matchRealm(key, realm); + } + + if (!!credentialType) { + matches = matches && CacheHelper.matchCredentialType(key, credentialType); + } + + if (!!clientId) { + matches = matches && CacheHelper.matchClientId(key, clientId); + } + + // idTokens do not have "target", target specific refreshTokens do exist for some types of authentication + if (!!target && CacheHelper.getCredentialType(key) != CredentialType.ID_TOKEN) { + matches = matches && CacheHelper.matchTarget(key, target); + } + + if (matches) { + matchingCredentials[key] = cacheCredentials[key]; + } + }); + + return matchingCredentials; } /** - * append account to the in memory cache + * returns a boolean if the given account is removed * @param account */ - addAccountEntity(account: AccountEntity): void { - const accKey = account.generateAccountKey(); - if (!this.inMemoryCache.accounts[accKey]) { - this.inMemoryCache.accounts[accKey] = account; - } + removeAccount(account: AccountEntity): boolean { + const key = account.generateAccountKey(); + return this.cacheStorage.removeItemFromMemory(key, CacheSchemaType.ACCOUNT); + } + + /** + * returns a boolean if the given account is removed + * @param account + */ + removeAccountContext(account: AccountEntity): boolean { + const cache = this.getCacheInMemory(); + const accountId = account.generateAccountId(); + + Object.keys(cache.idTokens).forEach((key) => { + if (cache.idTokens[key].generateAccountId() === accountId) { + this.cacheStorage.removeItemFromMemory(key, CacheSchemaType.CREDENTIAL); + } + }); + + Object.keys(cache.accessTokens).forEach((key) => { + if (cache.accessTokens[key].generateAccountId() === accountId) { + this.cacheStorage.removeItemFromMemory(key, CacheSchemaType.CREDENTIAL); + } + }); + + Object.keys(cache.refreshTokens).forEach((key) => { + if (cache.refreshTokens[key].generateAccountId() === accountId) { + this.cacheStorage.removeItemFromMemory(key, CacheSchemaType.CREDENTIAL); + } + }); + + return this.removeAccount(account); + } + + /** + * returns a boolean if the given credential is removed + * @param credential + */ + removeCredential(credential: Credential): boolean { + const key = credential.generateCredentialKey(); + return this.cacheStorage.removeItemFromMemory(key, CacheSchemaType.CREDENTIAL); } } diff --git a/src/unifiedCache/entities/AccessTokenEntity.ts b/src/unifiedCache/entities/AccessTokenEntity.ts index 72268536d3..f3fc461c84 100644 --- a/src/unifiedCache/entities/AccessTokenEntity.ts +++ b/src/unifiedCache/entities/AccessTokenEntity.ts @@ -5,7 +5,6 @@ import { Credential } from "./Credential"; import { CredentialType } from "../../utils/Constants"; -import { AuthenticationResult } from "../../response/AuthenticationResult"; /** * ACCESS_TOKEN Credential Type @@ -22,22 +21,22 @@ export class AccessTokenEntity extends Credential { /** * Create AccessTokenEntity - * @param homeAccountId - * @param authenticationResult - * @param clientId - * @param authority - */ + */ static createAccessTokenEntity( homeAccountId: string, - authenticationResult: AuthenticationResult, + environment: string, + accessToken: string, clientId: string, - environment: string + tenantId: string, + scopes: string, + expiresOn: number, + extExpiresOn: number ): AccessTokenEntity { const atEntity: AccessTokenEntity = new AccessTokenEntity(); atEntity.homeAccountId = homeAccountId; atEntity.credentialType = CredentialType.ACCESS_TOKEN; - atEntity.secret = authenticationResult.accessToken; + atEntity.secret = accessToken; const date = new Date(); const currentTime = date.getMilliseconds() / 1000; @@ -46,17 +45,13 @@ export class AccessTokenEntity extends Credential { // TODO: Crosscheck the exact conversion UTC // Token expiry time. // This value should be  calculated based on the current UTC time measured locally and the value  expires_in Represented as a string in JSON. - atEntity.expiresOn = authenticationResult.expiresOn - .getMilliseconds() - .toString(); - atEntity.extendedExpiresOn = authenticationResult.extExpiresOn - .getMilliseconds() - .toString(); + atEntity.expiresOn = expiresOn.toString(); + atEntity.extendedExpiresOn = extExpiresOn.toString(); atEntity.environment = environment; atEntity.clientId = clientId; - atEntity.realm = authenticationResult.tenantId; - atEntity.target = authenticationResult.scopes.join(" "); + atEntity.realm = tenantId; + atEntity.target = scopes; return atEntity; } diff --git a/src/unifiedCache/entities/AccountEntity.ts b/src/unifiedCache/entities/AccountEntity.ts index c2682fdf69..ba5d49c49a 100644 --- a/src/unifiedCache/entities/AccountEntity.ts +++ b/src/unifiedCache/entities/AccountEntity.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -import { Separators, CacheAccountType } from "../../utils/Constants"; +import { Separators, CacheAccountType, CacheType } from "../../utils/Constants"; import { Authority } from "../../authority/Authority"; import { IdToken } from "../../account/IdToken"; import { ICrypto } from "../../crypto/ICrypto"; @@ -52,6 +52,26 @@ export class AccountEntity { return accountKey.join(Separators.CACHE_KEY_SEPARATOR).toLowerCase(); } + /** + * returns the type of the cache (in this case account) + */ + generateType(): number { + switch (this.authorityType) { + case CacheAccountType.ADFS_ACCOUNT_TYPE: + return CacheType.ADFS; + case CacheAccountType.MSAV1_ACCOUNT_TYPE: + return CacheType.MSA; + case CacheAccountType.MSSTS_ACCOUNT_TYPE: + return CacheType.MSSTS; + case CacheAccountType.GENERIC_ACCOUNT_TYPE: + return CacheType.GENERIC; + default: { + console.log("Unexpected account type"); + return null; + } + } + } + /** * Build Account cache from IdToken, clientInfo and authority/policy * @param clientInfo diff --git a/src/unifiedCache/entities/CacheRecord.ts b/src/unifiedCache/entities/CacheRecord.ts new file mode 100644 index 0000000000..acf5493a59 --- /dev/null +++ b/src/unifiedCache/entities/CacheRecord.ts @@ -0,0 +1,16 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { AccountEntity } from "./AccountEntity"; +import { IdTokenEntity } from "./IdTokenEntity"; +import { AccessTokenEntity } from "./AccessTokenEntity"; +import { RefreshTokenEntity } from "./RefreshTokenEntity"; + +export class CacheRecord { + account: AccountEntity; + idToken: IdTokenEntity; + accessToken: AccessTokenEntity; + refreshToken: RefreshTokenEntity; +} diff --git a/src/unifiedCache/entities/Credential.ts b/src/unifiedCache/entities/Credential.ts index e384585713..4e6592b7b9 100644 --- a/src/unifiedCache/entities/Credential.ts +++ b/src/unifiedCache/entities/Credential.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -import { Separators, CredentialType } from "../../utils/Constants"; +import { Separators, CredentialType, CacheType } from "../../utils/Constants"; /** * Base type for credentials to be stored in the cache: eg: ACCESS_TOKEN, ID_TOKEN etc @@ -61,4 +61,22 @@ export class Credential { return credentialKey.join(Separators.CACHE_KEY_SEPARATOR).toLowerCase(); } + + /** + * returns the type of the cache (in this case credential) + */ + generateType(): number { + switch (this.credentialType) { + case CredentialType.ID_TOKEN: + return CacheType.ID_TOKEN; + case CredentialType.ACCESS_TOKEN: + return CacheType.ACCESS_TOKEN; + case CredentialType.REFRESH_TOKEN: + return CacheType.REFRESH_TOKEN; + default: { + console.log("Unexpected credential type"); + return null; + } + } + } } diff --git a/src/unifiedCache/entities/IdTokenEntity.ts b/src/unifiedCache/entities/IdTokenEntity.ts index e1addac5cd..d7765e77be 100644 --- a/src/unifiedCache/entities/IdTokenEntity.ts +++ b/src/unifiedCache/entities/IdTokenEntity.ts @@ -5,7 +5,6 @@ import { Credential } from "./Credential"; import { CredentialType } from "../../utils/Constants"; -import { AuthenticationResult } from "../../response/AuthenticationResult"; /** * ID_TOKEN Cache @@ -22,9 +21,10 @@ export class IdTokenEntity extends Credential { */ static createIdTokenEntity( homeAccountId: string, - authenticationResult: AuthenticationResult, + environment: string, + idToken: string, clientId: string, - environment: string + tenantId: string ): IdTokenEntity { const idTokenEntity = new IdTokenEntity(); @@ -32,8 +32,8 @@ export class IdTokenEntity extends Credential { idTokenEntity.homeAccountId = homeAccountId; idTokenEntity.environment = environment; idTokenEntity.clientId = clientId; - idTokenEntity.secret = authenticationResult.idToken; - idTokenEntity.realm = authenticationResult.tenantId; + idTokenEntity.secret = idToken; + idTokenEntity.realm = tenantId; return idTokenEntity; } diff --git a/src/unifiedCache/entities/RefreshTokenEntity.ts b/src/unifiedCache/entities/RefreshTokenEntity.ts index 2397e59473..ac7ae2e08c 100644 --- a/src/unifiedCache/entities/RefreshTokenEntity.ts +++ b/src/unifiedCache/entities/RefreshTokenEntity.ts @@ -5,7 +5,6 @@ import { Credential } from "./Credential"; import { CredentialType } from "../../utils/Constants"; -import { AuthenticationResult } from "../../response/AuthenticationResult"; /** * REFRESH_TOKEN Cache @@ -22,10 +21,10 @@ export class RefreshTokenEntity extends Credential { */ static createRefreshTokenEntity( homeAccountId: string, - authenticationResult: AuthenticationResult, + environment: string, refreshToken: string, clientId: string, - environment: string + familyId?: string ): RefreshTokenEntity { const rtEntity = new RefreshTokenEntity(); @@ -35,8 +34,8 @@ export class RefreshTokenEntity extends Credential { rtEntity.homeAccountId = homeAccountId; rtEntity.secret = refreshToken; - if (authenticationResult.familyId) - rtEntity.familyId = authenticationResult.familyId; + if (familyId) + rtEntity.familyId = familyId; return rtEntity; } diff --git a/src/unifiedCache/interface/ICacheManager.ts b/src/unifiedCache/interface/ICacheManager.ts new file mode 100644 index 0000000000..f67ecab230 --- /dev/null +++ b/src/unifiedCache/interface/ICacheManager.ts @@ -0,0 +1,69 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { AccountEntity } from "../entities/AccountEntity"; +import { Credential } from "../entities/Credential"; +import { AccountCache, CredentialCache, AccountFilter, CredentialFilter } from "../utils/CacheTypes"; + +export interface ICacheManager { + /** + * saves account into cache + * @param account + */ + saveAccount(account: AccountEntity): void; + + /** + * saves credential - accessToken, idToken or refreshToken into cache + * @param credential + */ + saveCredential(credential: Credential): void; + + /** + * Given account key retrieve an account + * @param key + */ + getAccount(key: string): AccountEntity; + + /** + * retrieve a credential - accessToken, idToken or refreshToken; given the cache key + * @param key + */ + getCredential(key: string): Credential; + + /** + * retrieve accounts matching all provided filters; if no filter is set, get all accounts + * @param homeAccountId + * @param environment + * @param realm + */ + getAccountsFilteredBy( + filter: AccountFilter + ): AccountCache; + + /** + * retrieve credentials matching all provided filters; if no filter is set, get all credentials + * @param homeAccountId + * @param environment + * @param credentialType + * @param clientId + * @param realm + * @param target + */ + getCredentialsFilteredBy( + filter: CredentialFilter + ): CredentialCache; + + /** + * returns a boolean if the given account is removed + * @param account + */ + removeAccount(account: AccountEntity): boolean; + + /** + * returns a boolean if the given credential is removed + * @param credential + */ + removeCredential(credential: Credential): boolean; +} diff --git a/src/unifiedCache/serialize/EntitySerializer.ts b/src/unifiedCache/serialize/EntitySerializer.ts new file mode 100644 index 0000000000..def1ac3226 --- /dev/null +++ b/src/unifiedCache/serialize/EntitySerializer.ts @@ -0,0 +1,69 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { AccountCacheMaps, AccessTokenCacheMaps, IdTokenCacheMaps, RefreshTokenCacheMaps, AppMetadataCacheMaps } from "../serialize/JsonKeys"; +import { AccountCache, AccessTokenCache, IdTokenCache, RefreshTokenCache, AppMetadataCache } from "../utils/CacheTypes"; +import { CacheHelper } from "../utils/CacheHelper"; + +export class EntitySerializer { + /** + * Convert AccountEntity to string + * @param accCache + * @param key + */ + static mapAccountKeys(accCache: AccountCache, key: string): object { + return CacheHelper.renameKeys( + accCache[key], + AccountCacheMaps.toCacheMap + ); + } + + /** + * Convert IdTokenEntity to string + * @param idTCache + * @param key + */ + static mapIdTokenKeys(idTCache: IdTokenCache, key: string): object { + return CacheHelper.renameKeys( + idTCache[key], + IdTokenCacheMaps.toCacheMap + ); + } + + /** + * Convert AccessTokenEntity to string + * @param atCache + * @param key + */ + static mapAccessTokenKeys(atCache: AccessTokenCache, key: string): object { + return CacheHelper.renameKeys( + atCache[key], + AccessTokenCacheMaps.toCacheMap + ); + } + + /** + * Convert RefreshTokenEntity to string + * @param rtCache + * @param key + */ + static mapRefreshTokenKeys(rtCache: RefreshTokenCache, key: string): object { + return CacheHelper.renameKeys( + rtCache[key], + RefreshTokenCacheMaps.toCacheMap + ); + } + + /** + * Convert AppMetaDataEntity to string + * @param amdtCache + * @param key + */ + static mapAppMetadataKeys(amdtCache: AppMetadataCache, key: string): object { + return CacheHelper.renameKeys( + amdtCache[key], + AppMetadataCacheMaps.toCacheMap + ); + } +} diff --git a/src/unifiedCache/serialize/Serializer.ts b/src/unifiedCache/serialize/Serializer.ts index a4b5215402..d9792ff565 100644 --- a/src/unifiedCache/serialize/Serializer.ts +++ b/src/unifiedCache/serialize/Serializer.ts @@ -3,8 +3,7 @@ * Licensed under the MIT License. */ -import { CacheHelper } from "../utils/CacheHelper"; -import { AccountCacheMaps, AccessTokenCacheMaps, IdTokenCacheMaps, RefreshTokenCacheMaps, AppMetadataCacheMaps } from "./JsonKeys"; +import { EntitySerializer } from "./EntitySerializer"; import { AccountCache, AccessTokenCache, IdTokenCache, RefreshTokenCache, AppMetadataCache, JsonCache, InMemoryCache } from "../utils/CacheTypes"; import { StringDict } from "../../utils/MsalTypes"; @@ -25,11 +24,7 @@ export class Serializer { static serializeAccounts(accCache: AccountCache): StringDict { const accounts = {}; Object.keys(accCache).map(function (key) { - const mappedAcc = CacheHelper.renameKeys( - accCache[key], - AccountCacheMaps.toCacheMap - ); - accounts[key] = mappedAcc; + accounts[key] = EntitySerializer.mapAccountKeys(accCache, key); }); return accounts; @@ -42,11 +37,7 @@ export class Serializer { static serializeIdTokens(idTCache: IdTokenCache): StringDict{ const idTokens = {}; Object.keys(idTCache).map(function (key) { - const mappedIdT = CacheHelper.renameKeys( - idTCache[key], - IdTokenCacheMaps.toCacheMap - ); - idTokens[key] = mappedIdT; + idTokens[key] = EntitySerializer.mapIdTokenKeys(idTCache, key); }); return idTokens; @@ -57,14 +48,9 @@ export class Serializer { * @param atCache */ static serializeAccessTokens(atCache: AccessTokenCache): StringDict { - // access tokens const accessTokens = {}; Object.keys(atCache).map(function (key) { - const mappedAT = CacheHelper.renameKeys( - atCache[key], - AccessTokenCacheMaps.toCacheMap - ); - accessTokens[key] = mappedAT; + accessTokens[key] = EntitySerializer.mapAccessTokenKeys(atCache, key); }); return accessTokens; @@ -77,11 +63,7 @@ export class Serializer { static serializeRefreshTokens(rtCache: RefreshTokenCache): StringDict{ const refreshTokens = {}; Object.keys(rtCache).map(function (key) { - const mappedRT = CacheHelper.renameKeys( - rtCache[key], - RefreshTokenCacheMaps.toCacheMap - ); - refreshTokens[key] = mappedRT; + refreshTokens[key] = EntitySerializer.mapRefreshTokenKeys(rtCache, key); }); return refreshTokens; @@ -94,11 +76,7 @@ export class Serializer { static serializeAppMetadata(amdtCache: AppMetadataCache): StringDict { const appMetadata = {}; Object.keys(amdtCache).map(function (key) { - const mappedAmdt = CacheHelper.renameKeys( - amdtCache[key], - AppMetadataCacheMaps.toCacheMap - ); - appMetadata[key] = mappedAmdt; + appMetadata[key] = EntitySerializer.mapAppMetadataKeys(amdtCache, key); }); return appMetadata; diff --git a/src/unifiedCache/utils/CacheHelper.ts b/src/unifiedCache/utils/CacheHelper.ts index 2cd6f06764..2ee8b581fa 100644 --- a/src/unifiedCache/utils/CacheHelper.ts +++ b/src/unifiedCache/utils/CacheHelper.ts @@ -3,6 +3,13 @@ * Licensed under the MIT License. */ +import { + Separators, + CredentialKeyPosition, + CacheType, + CacheSchemaType, +} from "../../utils/Constants"; + export class CacheHelper { /** * Helper to convert serialized data to object @@ -43,4 +50,144 @@ export class CacheHelper { }); return Object.assign({}, ...keyValues); } + + /** + * + * @param key + * @param homeAccountId + */ + static matchHomeAccountId(key: string, homeAccountId: string): boolean { + return ( + homeAccountId === + key.split(Separators.CACHE_KEY_SEPARATOR)[ + CredentialKeyPosition.HOME_ACCOUNT_ID + ] + ); + } + + /** + * + * @param key + * @param environment + */ + static matchEnvironment(key: string, environment: string): boolean { + return ( + environment === + key.split(Separators.CACHE_KEY_SEPARATOR)[ + CredentialKeyPosition.ENVIRONMENT + ] + ); + } + + /** + * + * @param key + * @param credentialType + * // TODO: Confirm equality for enum vs string here + */ + static matchCredentialType(key: string, credentialType: string): boolean { + return ( + credentialType.toLowerCase() === + key + .split(Separators.CACHE_KEY_SEPARATOR) + [CredentialKeyPosition.CREDENTIAL_TYPE].toString() + .toLowerCase() + ); + } + + /** + * + * @param key + * @param clientId + */ + static matchClientId(key: string, clientId: string): boolean { + return ( + clientId === + key.split(Separators.CACHE_KEY_SEPARATOR)[ + CredentialKeyPosition.CLIENT_ID + ] + ); + } + + /** + * + * @param key + * @param realm + */ + static matchRealm(key: string, realm: string): boolean { + return ( + realm === + key.split(Separators.CACHE_KEY_SEPARATOR)[ + CredentialKeyPosition.REALM + ] + ); + } + + /** + * + * @param key + * @param target + */ + static matchTarget(key: string, target: string): boolean { + return CacheHelper.targetsIntersect( + key.split(Separators.CACHE_KEY_SEPARATOR)[ + CredentialKeyPosition.TARGET + ], + target + ); + } + + /** + * returns a boolean if the sets of scopes intersect (scopes are stored as "target" in cache) + * @param target + * @param credentialTarget + */ + static targetsIntersect(credentialTarget: string, target: string): boolean { + const targetSet = new Set(target.split(" ")); + const credentialTargetSet = new Set(credentialTarget.split(" ")); + + let isSubset = true; + targetSet.forEach((key) => { + isSubset = isSubset && credentialTargetSet.has(key); + }); + + return isSubset; + } + + /** + * helper function to return `CredentialType` + * @param key + */ + static getCredentialType(key: string): string { + return key.split(Separators.CACHE_KEY_SEPARATOR)[ + CredentialKeyPosition.CREDENTIAL_TYPE + ]; + } + + /** + * helper function to return `CacheSchemaType` + * @param key + */ + static getCacheType(type: number): string { + switch (type) { + case CacheType.ADFS: + case CacheType.MSA: + case CacheType.MSSTS: + case CacheType.GENERIC: + return CacheSchemaType.ACCOUNT; + + case CacheType.ACCESS_TOKEN: + case CacheType.REFRESH_TOKEN: + case CacheType.ID_TOKEN: + return CacheSchemaType.CREDENTIAL; + + case CacheType.APP_META_DATA: + return CacheSchemaType.APP_META_DATA; + + default: { + console.log("Invalid cache type"); + return null; + } + } + } } diff --git a/src/unifiedCache/utils/CacheTypes.ts b/src/unifiedCache/utils/CacheTypes.ts index ad6ee315fe..8786e6d758 100644 --- a/src/unifiedCache/utils/CacheTypes.ts +++ b/src/unifiedCache/utils/CacheTypes.ts @@ -32,3 +32,30 @@ export type InMemoryCache = { refreshTokens: RefreshTokenCache; appMetadata: AppMetadataCache; }; + +export type CredentialCache = { + idTokens: IdTokenCache; + accessTokens: AccessTokenCache; + refreshTokens: RefreshTokenCache; +}; + +/** + * Account: -- + */ +export type AccountFilter = { + homeAccountId?: string; + environment?: string; + realm?: string; +}; + +/** + * Credential: ----- + */ +export type CredentialFilter = { + homeAccountId?: string; + environment?: string; + credentialType?: string; + clientId?: string; + realm?: string; + target?: string; +}; diff --git a/src/utils/Constants.ts b/src/utils/Constants.ts index 17e52f1b25..4dbe3f96ef 100644 --- a/src/utils/Constants.ts +++ b/src/utils/Constants.ts @@ -236,7 +236,7 @@ export enum CacheAccountType { MSSTS_ACCOUNT_TYPE = "MSSTS", ADFS_ACCOUNT_TYPE = "ADFS", MSAV1_ACCOUNT_TYPE = "MSA", - OTHER_ACCOUNT_TYPE = "Other" + GENERIC_ACCOUNT_TYPE = "Generic" // NTLM, Kerberos, FBA, Basic etc } /** @@ -251,28 +251,45 @@ export enum Separators { * Credentail Type stored in the cache */ export enum CredentialType { - ID_TOKEN = "IdToken", - ACCESS_TOKEN = "AccessToken", - REFRESH_TOKEN = "RefreshToken" + ID_TOKEN = "idtoken", + ACCESS_TOKEN = "accesstoken", + REFRESH_TOKEN = "refreshtoken", } /** - * cache Type + * Credentail Type stored in the cache */ -export enum CacheEntity { +export enum CacheSchemaType { ACCOUNT = "Account", - APP_META_DATA = "AppMetaData" + CREDENTIAL = "Credential", + APP_META_DATA = "AppMetadata" } /** * Combine all cache types */ -export enum CacheTypes { - ACCESS_TOKEN, - ID_TOKEN, - REFRESH_TOKEN, - ACCOUNT, - APP_META_DATA +export enum CacheType { + ADFS = 1001, + MSA = 1002, + MSSTS = 1003, + GENERIC = 1004, + ACCESS_TOKEN = 2001, + REFRESH_TOKEN = 2002, + ID_TOKEN = 2003, + APP_META_DATA = 3001 +}; + +/** + * accountId: - + * credentialId: -- + */ +export enum CredentialKeyPosition { + HOME_ACCOUNT_ID = 0, + ENVIRONMENT = 1, + CREDENTIAL_TYPE = 2, + CLIENT_ID = 3, + REALM = 4, + TARGET = 5 }; /** diff --git a/test/unifiedCache/UnifiedCacheManager.spec.ts b/test/unifiedCache/UnifiedCacheManager.spec.ts index ec1064f249..bc57b13d4a 100644 --- a/test/unifiedCache/UnifiedCacheManager.spec.ts +++ b/test/unifiedCache/UnifiedCacheManager.spec.ts @@ -1,35 +1,183 @@ import { expect } from "chai"; import { UnifiedCacheManager } from "../../src/unifiedCache/UnifiedCacheManager"; import { mockCache } from "./entities/cacheConstants"; -import { InMemoryCache, JsonCache } from "../../src/unifiedCache/utils/CacheTypes"; +import { InMemoryCache, CredentialFilter, AccountFilter} from "../../src/unifiedCache/utils/CacheTypes"; import { ICacheStorage } from "../../src/cache/ICacheStorage"; import { Deserializer } from "../../src/unifiedCache/serialize/Deserializer"; +import { AccountEntity } from "../../src/unifiedCache/entities/AccountEntity"; +import { AccessTokenEntity } from "../../src/unifiedCache/entities/AccessTokenEntity"; +import { CacheSchemaType, CacheHelper, CredentialType } from "../../src"; +import { IdTokenEntity } from "../../src/unifiedCache/entities/IdTokenEntity"; +import { RefreshTokenEntity } from "../../src/unifiedCache/entities/RefreshTokenEntity"; +import { AppMetadataEntity } from "../../src/unifiedCache/entities/AppMetadataEntity"; const cacheJson = require("./serialize/cache.json"); describe("UnifiedCacheManager test cases", () => { - let store = {}; let storageInterface: ICacheStorage; const cache = JSON.stringify(cacheJson); - const inMemCache: InMemoryCache = Deserializer.deserializeAllCache(Deserializer.deserializeJSONBlob(cache)); + const inMemCache: InMemoryCache = Deserializer.deserializeAllCache( + Deserializer.deserializeJSONBlob(cache) + ); beforeEach(() => { storageInterface = { getCache(): InMemoryCache { return inMemCache; }, - setCache(): void { - // do nothing + setCache(inMemCache): void { + this.inMemCache = inMemCache; }, setItem(key: string, value: string): void { store[key] = value; }, + setItemInMemory(key: string, value: object, type?: string): void { + // read inMemoryCache + const cache = this.getCache(); + + // save the cacheItem + switch (type) { + case CacheSchemaType.ACCOUNT: { + cache.accounts[key] = value as AccountEntity; + break; + } + case CacheSchemaType.CREDENTIAL: { + const credentialType = CacheHelper.getCredentialType(key); + console.log(credentialType); + switch (credentialType) { + case CredentialType.ID_TOKEN: { + cache.idTokens[key] = value as IdTokenEntity; + break; + } + case CredentialType.ACCESS_TOKEN: { + cache.accessTokens[key] = value as AccessTokenEntity; + console.log(value); + break; + } + case CredentialType.REFRESH_TOKEN: { + cache.refreshTokens[key] = value as RefreshTokenEntity; + break; + } + } + break; + } + case CacheSchemaType.APP_META_DATA: { + cache.appMetadata[key] = value as AppMetadataEntity; + break; + } + default: { + console.log("Invalid Cache Type"); + return; + } + } + + // update inMemoryCache + this.setCache(cache); + }, getItem(key: string): string { return store[key]; }, - removeItem(key: string): void { + getItemFromMemory(key: string, type?: string): object { + // read inMemoryCache + const cache = this.getCache(); + + // save the cacheItem + switch (type) { + case CacheSchemaType.ACCOUNT: { + return cache.accounts[key] as AccountEntity || null; + } + case CacheSchemaType.CREDENTIAL: { + const credentialType = CacheHelper.getCredentialType(key); + let credential = null; + switch (credentialType) { + case CredentialType.ID_TOKEN: { + credential = cache.idTokens[key] as IdTokenEntity || null; + break; + } + case CredentialType.ACCESS_TOKEN: { + credential = cache.accessTokens[key] as AccessTokenEntity || null; + break; + } + case CredentialType.REFRESH_TOKEN: { + credential = cache.refreshTokens[key] as RefreshTokenEntity || null; + break; + } + } + return credential!; + } + case CacheSchemaType.APP_META_DATA: { + return cache.appMetadata[key] as AppMetadataEntity || null; + } + default: { + console.log("Invalid Cache Type"); + return {}; + } + } + }, + removeItem(key: string): boolean { delete store[key]; + return true; + }, + removeItemFromMemory(key: string, type?: string): boolean { + // read inMemoryCache + const cache = this.getCache(); + let result: boolean = false; + + // save the cacheItem + switch (type) { + case CacheSchemaType.ACCOUNT: { + if (!!cache.accounts[key]) { + delete cache.accounts[key]; + result = true; + } + break; + } + case CacheSchemaType.CREDENTIAL: { + const credentialType = CacheHelper.getCredentialType(key); + switch (credentialType) { + case CredentialType.ID_TOKEN: { + if (!!cache.idTokens[key]) { + delete cache.idTokens[key]; + result = true; + } + break; + } + case CredentialType.ACCESS_TOKEN: { + if (!!cache.accessTokens[key]) { + delete cache.accessTokens[key]; + result = true; + } + break; + } + case CredentialType.REFRESH_TOKEN: { + if (!!cache.refreshTokens[key]) { + delete cache.refreshTokens[key]; + result = true; + } + break; + } + } + break; + } + case CacheSchemaType.APP_META_DATA: { + if (!!cache.appMetadata[key]) { + delete cache.appMetadata[key]; + result = true; + } + break; + } + default: { + console.log("Invalid Cache Type"); + break; + } + } + + // write to the cache after removal + if (result) { + this.setCache(cache); + } + return result; }, containsKey(key: string): boolean { return !!store[key]; @@ -40,11 +188,10 @@ describe("UnifiedCacheManager test cases", () => { clear(): void { store = {}; }, - } + }; }); it("initCache", () => { - let unifiedCacheManager = new UnifiedCacheManager(storageInterface); // create mock AccessToken @@ -53,26 +200,161 @@ describe("UnifiedCacheManager test cases", () => { const atTwo = mockCache.createMockATTwo(); const atTwoKey = atTwo.generateCredentialKey(); - expect(Object.keys(unifiedCacheManager.getCacheInMemory().accessTokens).length).to.equal(2); - expect(unifiedCacheManager.getCacheInMemory().accessTokens[atOneKey]).to.eql(atOne); - expect(unifiedCacheManager.getCacheInMemory().accessTokens[atTwoKey]).to.eql(atTwo); + expect( + Object.keys(unifiedCacheManager.getCacheInMemory().accessTokens) + .length + ).to.equal(2); + expect( + unifiedCacheManager.getCacheInMemory().accessTokens[atOneKey] + ).to.eql(atOne); + expect( + unifiedCacheManager.getCacheInMemory().accessTokens[atTwoKey] + ).to.eql(atTwo); + }); + + it("save account", () => { + let ac = new AccountEntity(); + Object.assign(ac, { + homeAccountId: "someUid.someUtid", + environment: "login.microsoftonline.com", + realm: "microsoft", + localAccountId: "object1234", + username: "Jane Goodman", + authorityType: "MSSTS", + clientInfo: "eyJ1aWQiOiJzb21lVWlkIiwgInV0aWQiOiJzb21lVXRpZCJ9", + }); + + let unifiedCacheManager = new UnifiedCacheManager(storageInterface); + + const accountKey = ac.generateAccountKey(); + unifiedCacheManager.saveAccount(ac); + expect( + unifiedCacheManager.getCacheInMemory().accounts[accountKey] + .homeAccountId + ).to.eql("someUid.someUtid"); + }); + + it("save credential", () => { + let at = new AccessTokenEntity(); + Object.assign(at, { + homeAccountId: "someUid.someUtid", + environment: "login.microsoftonline.com", + credentialType: "AccessToken", + clientId: "mock_client_id", + secret: "an access token sample", + realm: "microsoft", + target: "scope6 scope7", + cachedAt: "1000", + expiresOn: "4600", + extendedExpiresOn: "4600", + }); + + let unifiedCacheManager = new UnifiedCacheManager(storageInterface); + const atKey = at.generateCredentialKey(); + unifiedCacheManager.saveCredential(at); + expect( + unifiedCacheManager.getCacheInMemory().accessTokens[atKey] + .homeAccountId + ).to.eql("someUid.someUtid"); }); it("getAccount", () => { + let unifiedCacheManager = new UnifiedCacheManager(storageInterface); + expect( + unifiedCacheManager.getAccount( + "someuid.someutid-login.microsoftonline.com-microsoft" + ).homeAccountId + ).to.eql("someUid.someUtid"); + }); + + it("getCredential", () => { + let unifiedCacheManager = new UnifiedCacheManager(storageInterface); + + expect( + unifiedCacheManager.getCredential( + "someuid.someutid-login.microsoftonline.com-accesstoken-mock_client_id-microsoft-scope6 scope7" + ).homeAccountId + ).to.eql("someUid.someUtid"); + }); + + it("getAccounts", () => { + let unifiedCacheManager = new UnifiedCacheManager(storageInterface); + + const filterOne: AccountFilter = { homeAccountId: "uid.utid" }; + let accounts = unifiedCacheManager.getAccountsFilteredBy(filterOne); + expect(Object.keys(accounts).length).to.eql(1); + + const filterTwo: AccountFilter = { environment: "login.microsoftonline.com" }; + accounts = unifiedCacheManager.getAccountsFilteredBy(filterTwo); + expect(Object.keys(accounts).length).to.eql(2); + }); + + it("getCredentials", () => { let unifiedCacheManager = new UnifiedCacheManager(storageInterface); - // create mock Account - const acc = mockCache.createMockAcc(); - const homeAccountId = "uid.utid"; - const environment = "login.microsoftonline.com"; - const realm = "microsoft"; + // filter by homeAccountId + const filterOne: CredentialFilter = { homeAccountId: "uid.utid" }; + let credentials = unifiedCacheManager.getCredentialsFilteredBy(filterOne); + expect(Object.keys(credentials.idTokens).length).to.eql(1); + expect(Object.keys(credentials.accessTokens).length).to.eql(2); + expect(Object.keys(credentials.refreshTokens).length).to.eql(2); + + // filter by homeAccountId + const filterTwo: CredentialFilter = { homeAccountId: "someuid.someutid" }; + credentials = unifiedCacheManager.getCredentialsFilteredBy(filterTwo); + expect(Object.keys(credentials.idTokens).length).to.eql(0); + expect(Object.keys(credentials.accessTokens).length).to.eql(1); + expect(Object.keys(credentials.refreshTokens).length).to.eql(0); + + // filter by target + const filterThree = { target: "scope1 scope2 scope3" }; + credentials = unifiedCacheManager.getCredentialsFilteredBy(filterThree); + console.log(credentials.accessTokens); + }); + + it("removeAccount", () => { + let unifiedCacheManager = new UnifiedCacheManager(storageInterface); - const genAcc = unifiedCacheManager.getAccount(homeAccountId, environment, realm); - expect(acc).to.eql(genAcc); + let ac = new AccountEntity(); + Object.assign(ac, { + homeAccountId: "someUid.someUtid", + environment: "login.microsoftonline.com", + realm: "microsoft", + localAccountId: "object1234", + username: "Jane Goodman", + authorityType: "MSSTS", + clientInfo: "eyJ1aWQiOiJzb21lVWlkIiwgInV0aWQiOiJzb21lVXRpZCJ9", + }); - const randomAcc = unifiedCacheManager.getAccount("", "", ""); - expect(randomAcc).to.be.null; + unifiedCacheManager.removeAccount(ac); + const accountKey = ac.generateAccountKey(); + expect( + unifiedCacheManager.getCacheInMemory().accounts[accountKey] + ).to.eql(undefined); }); + it("removeCredential", () => { + let unifiedCacheManager = new UnifiedCacheManager(storageInterface); + + let at = new AccessTokenEntity(); + Object.assign(at, { + homeAccountId: "someUid.someUtid", + environment: "login.microsoftonline.com", + credentialType: "AccessToken", + clientId: "mock_client_id", + secret: "an access token sample", + realm: "microsoft", + target: "scope6 scope7", + cachedAt: "1000", + expiresOn: "4600", + extendedExpiresOn: "4600", + }); + + unifiedCacheManager.removeCredential(at); + const atKey = at.generateCredentialKey(); + expect( + unifiedCacheManager.getCacheInMemory().accessTokens[atKey] + ).to.eql(undefined); + }); });