diff --git a/lib/msal-browser/src/cache/BrowserStorage.ts b/lib/msal-browser/src/cache/BrowserStorage.ts index 1564eee226..c4be0b6a9a 100644 --- a/lib/msal-browser/src/cache/BrowserStorage.ts +++ b/lib/msal-browser/src/cache/BrowserStorage.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ -import { ICacheStorage, Constants, PersistentCacheKeys, TemporaryCacheKeys } from "@azure/msal-common"; +import { ICacheStorage, Constants, PersistentCacheKeys, TemporaryCacheKeys, InMemoryCache } from "@azure/msal-common"; import { CacheOptions } from "../config/Configuration"; import { BrowserAuthError } from "../error/BrowserAuthError"; import { BrowserConfigurationAuthError } from "../error/BrowserConfigurationAuthError"; @@ -251,4 +251,24 @@ export class BrowserStorage implements ICacheStorage { const expr = new Date(today.getTime() + cookieLifeDays * COOKIE_LIFE_MULTIPLIER); return expr.toUTCString(); } + + /** + * Dummy implementation until browser cache is migrated + */ + getCache(): InMemoryCache { + return { + accounts: {}, + idTokens: {}, + accessTokens: {}, + refreshTokens: {}, + appMetadata: {} + }; + } + + /** + * Dummy implementation until browser cache is migrated + */ + setCache() { + // sets nothing + } } diff --git a/lib/msal-browser/test/interaction_handler/InteractionHandler.spec.ts b/lib/msal-browser/test/interaction_handler/InteractionHandler.spec.ts index 5736b48951..2f7b8bf600 100644 --- a/lib/msal-browser/test/interaction_handler/InteractionHandler.spec.ts +++ b/lib/msal-browser/test/interaction_handler/InteractionHandler.spec.ts @@ -1,6 +1,6 @@ import { expect } from "chai"; import { InteractionHandler } from "../../src/interaction_handler/InteractionHandler"; -import { SPAClient, PkceCodes, NetworkRequestOptions, LogLevel, CodeResponse, Account, TokenResponse } from "@azure/msal-common"; +import { SPAClient, PkceCodes, NetworkRequestOptions, LogLevel, CodeResponse, Account, TokenResponse, InMemoryCache } from "@azure/msal-common"; import { Configuration, buildConfiguration } from "../../src/config/Configuration"; import { TEST_CONFIG, RANDOM_TEST_GUID, TEST_URIS, TEST_DATA_CLIENT_INFO, TEST_TOKENS, TEST_TOKEN_LIFETIMES, TEST_HASHES } from "../utils/StringConstants"; import { BrowserStorage } from "../../src/cache/BrowserStorage"; @@ -15,11 +15,11 @@ class TestInteractionHandler extends InteractionHandler { showUI(requestUrl: string): Window { throw new Error("Method not implemented."); - } - + } + initiateAuthRequest(requestUrl: string): Window | Promise { throw new Error("Method not implemented."); - } + } } const clearFunc = (): void => { @@ -77,6 +77,18 @@ describe("InteractionHandler.ts Unit Tests", () => { } }, storageInterface: { + getCache: (): InMemoryCache => { + return { + accounts: {}, + idTokens: {}, + accessTokens: {}, + refreshTokens: {}, + appMetadata: {}, + }; + }, + setCache: (): void => { + // dummy impl; + }, clear: clearFunc, containsKey: (key: string): boolean => { return true; diff --git a/lib/msal-browser/test/interaction_handler/PopupHandler.spec.ts b/lib/msal-browser/test/interaction_handler/PopupHandler.spec.ts index 240c0f0391..b1549be7f2 100644 --- a/lib/msal-browser/test/interaction_handler/PopupHandler.spec.ts +++ b/lib/msal-browser/test/interaction_handler/PopupHandler.spec.ts @@ -2,11 +2,11 @@ import chai from "chai"; import chaiAsPromised from "chai-as-promised" chai.use(chaiAsPromised); const expect = chai.expect; -import { PkceCodes, SPAClient, NetworkRequestOptions, LogLevel, Account, TokenResponse, CodeResponse } from "@azure/msal-common"; +import { PkceCodes, SPAClient, NetworkRequestOptions, LogLevel, InMemoryCache } from "@azure/msal-common"; import { PopupHandler } from "../../src/interaction_handler/PopupHandler"; import { BrowserStorage } from "../../src/cache/BrowserStorage"; import { Configuration, buildConfiguration } from "../../src/config/Configuration"; -import { TEST_CONFIG, TEST_TOKENS, TEST_TOKEN_LIFETIMES, TEST_DATA_CLIENT_INFO, TEST_URIS, RANDOM_TEST_GUID, TEST_HASHES } from "../utils/StringConstants"; +import { TEST_CONFIG, TEST_URIS } from "../utils/StringConstants"; import sinon from "sinon"; import { InteractionHandler } from "../../src/interaction_handler/InteractionHandler"; import { BrowserAuthErrorMessage, BrowserAuthError } from "../../src/error/BrowserAuthError"; @@ -50,8 +50,9 @@ describe("PopupHandler.ts Unit Tests", () => { const authCodeModule = new SPAClient({ authOptions: configObj.auth, systemOptions: { - tokenRenewalOffsetSeconds: configObj.system.tokenRenewalOffsetSeconds, - telemetry: configObj.system.telemetry + tokenRenewalOffsetSeconds: + configObj.system.tokenRenewalOffsetSeconds, + telemetry: configObj.system.telemetry, }, cryptoInterface: { createNewGuid: (): string => { @@ -65,9 +66,21 @@ describe("PopupHandler.ts Unit Tests", () => { }, generatePkceCodes: async (): Promise => { return testPkceCodes; - } + }, }, storageInterface: { + getCache: (): InMemoryCache => { + return { + accounts: {}, + idTokens: {}, + accessTokens: {}, + refreshTokens: {}, + appMetadata: {}, + }; + }, + setCache: (): void => { + // dummy impl; + }, clear: clearFunc, containsKey: (key: string): boolean => { return true; @@ -79,24 +92,34 @@ describe("PopupHandler.ts Unit Tests", () => { return testKeySet; }, removeItem: removeFunc, - setItem: setFunc + setItem: setFunc, }, networkInterface: { - sendGetRequestAsync: async (url: string, options?: NetworkRequestOptions): Promise => { + sendGetRequestAsync: async ( + url: string, + options?: NetworkRequestOptions + ): Promise => { return testNetworkResult; }, - sendPostRequestAsync: async (url: string, options?: NetworkRequestOptions): Promise => { + sendPostRequestAsync: async ( + url: string, + options?: NetworkRequestOptions + ): Promise => { return testNetworkResult; - } + }, }, loggerOptions: { - loggerCallback: (level: LogLevel, message: string, containsPii: boolean): void => { + loggerCallback: ( + level: LogLevel, + message: string, + containsPii: boolean + ): void => { if (containsPii) { console.log(`Log level: ${level} Message: ${message}`); } }, - piiLoggingEnabled: true - } + piiLoggingEnabled: true, + }, }); browserStorage = new BrowserStorage(TEST_CONFIG.MSAL_CLIENT_ID, configObj.cache); popupHandler = new PopupHandler(authCodeModule, browserStorage); diff --git a/lib/msal-browser/test/interaction_handler/RedirectHandler.spec.ts b/lib/msal-browser/test/interaction_handler/RedirectHandler.spec.ts index 1e8eb03eaf..2abeec7585 100644 --- a/lib/msal-browser/test/interaction_handler/RedirectHandler.spec.ts +++ b/lib/msal-browser/test/interaction_handler/RedirectHandler.spec.ts @@ -3,7 +3,7 @@ import chaiAsPromised from "chai-as-promised" chai.use(chaiAsPromised); const expect = chai.expect; import { Configuration, buildConfiguration } from "../../src/config/Configuration"; -import { SPAClient, PkceCodes, NetworkRequestOptions, LogLevel, TemporaryCacheKeys, CodeResponse, TokenResponse, Account } from "@azure/msal-common"; +import { SPAClient, PkceCodes, NetworkRequestOptions, LogLevel, TemporaryCacheKeys, CodeResponse, TokenResponse, Account, InMemoryCache } from "@azure/msal-common"; import { TEST_CONFIG, TEST_URIS, TEST_TOKENS, TEST_DATA_CLIENT_INFO, RANDOM_TEST_GUID, TEST_HASHES, TEST_TOKEN_LIFETIMES } from "../utils/StringConstants"; import { BrowserStorage } from "../../src/cache/BrowserStorage"; import { RedirectHandler } from "../../src/interaction_handler/RedirectHandler"; @@ -50,8 +50,9 @@ describe("RedirectHandler.ts Unit Tests", () => { const authCodeModule = new SPAClient({ authOptions: configObj.auth, systemOptions: { - tokenRenewalOffsetSeconds: configObj.system.tokenRenewalOffsetSeconds, - telemetry: configObj.system.telemetry + tokenRenewalOffsetSeconds: + configObj.system.tokenRenewalOffsetSeconds, + telemetry: configObj.system.telemetry, }, cryptoInterface: { createNewGuid: (): string => { @@ -65,9 +66,21 @@ describe("RedirectHandler.ts Unit Tests", () => { }, generatePkceCodes: async (): Promise => { return testPkceCodes; - } + }, }, storageInterface: { + getCache: (): InMemoryCache => { + return { + accounts: {}, + idTokens: {}, + accessTokens: {}, + refreshTokens: {}, + appMetadata: {}, + }; + }, + setCache: (): void => { + // dummy impl; + }, clear: clearFunc, containsKey: (key: string): boolean => { return true; @@ -79,24 +92,34 @@ describe("RedirectHandler.ts Unit Tests", () => { return testKeySet; }, removeItem: removeFunc, - setItem: setFunc + setItem: setFunc, }, networkInterface: { - sendGetRequestAsync: async (url: string, options?: NetworkRequestOptions): Promise => { + sendGetRequestAsync: async ( + url: string, + options?: NetworkRequestOptions + ): Promise => { return testNetworkResult; }, - sendPostRequestAsync: async (url: string, options?: NetworkRequestOptions): Promise => { + sendPostRequestAsync: async ( + url: string, + options?: NetworkRequestOptions + ): Promise => { return testNetworkResult; - } + }, }, loggerOptions: { - loggerCallback: (level: LogLevel, message: string, containsPii: boolean): void => { + loggerCallback: ( + level: LogLevel, + message: string, + containsPii: boolean + ): void => { if (containsPii) { console.log(`Log level: ${level} Message: ${message}`); } }, - piiLoggingEnabled: true - } + piiLoggingEnabled: true, + }, }); browserStorage = new BrowserStorage(TEST_CONFIG.MSAL_CLIENT_ID, configObj.cache); redirectHandler = new RedirectHandler(authCodeModule, browserStorage); diff --git a/lib/msal-browser/test/interaction_handler/SilentHandler.spec.ts b/lib/msal-browser/test/interaction_handler/SilentHandler.spec.ts index 0db63bd99f..01bc214db2 100644 --- a/lib/msal-browser/test/interaction_handler/SilentHandler.spec.ts +++ b/lib/msal-browser/test/interaction_handler/SilentHandler.spec.ts @@ -2,7 +2,7 @@ import chai from "chai"; import chaiAsPromised from "chai-as-promised" chai.use(chaiAsPromised); const expect = chai.expect; -import { PkceCodes, SPAClient, NetworkRequestOptions, LogLevel } from "@azure/msal-common"; +import { PkceCodes, SPAClient, NetworkRequestOptions, LogLevel, InMemoryCache } from "@azure/msal-common"; import sinon from "sinon"; import { SilentHandler } from "../../src/interaction_handler/SilentHandler"; import { BrowserStorage } from "../../src/cache/BrowserStorage"; @@ -50,8 +50,9 @@ describe("SilentHandler.ts Unit Tests", () => { authCodeModule = new SPAClient({ authOptions: configObj.auth, systemOptions: { - tokenRenewalOffsetSeconds: configObj.system.tokenRenewalOffsetSeconds, - telemetry: configObj.system.telemetry + tokenRenewalOffsetSeconds: + configObj.system.tokenRenewalOffsetSeconds, + telemetry: configObj.system.telemetry, }, cryptoInterface: { createNewGuid: (): string => { @@ -65,9 +66,21 @@ describe("SilentHandler.ts Unit Tests", () => { }, generatePkceCodes: async (): Promise => { return testPkceCodes; - } + }, }, storageInterface: { + getCache: (): InMemoryCache => { + return { + accounts: {}, + idTokens: {}, + accessTokens: {}, + refreshTokens: {}, + appMetadata: {}, + }; + }, + setCache: (): void => { + // dummy impl; + }, clear: clearFunc, containsKey: (key: string): boolean => { return true; @@ -79,24 +92,34 @@ describe("SilentHandler.ts Unit Tests", () => { return testKeySet; }, removeItem: removeFunc, - setItem: setFunc + setItem: setFunc, }, networkInterface: { - sendGetRequestAsync: async (url: string, options?: NetworkRequestOptions): Promise => { + sendGetRequestAsync: async ( + url: string, + options?: NetworkRequestOptions + ): Promise => { return testNetworkResult; }, - sendPostRequestAsync: async (url: string, options?: NetworkRequestOptions): Promise => { + sendPostRequestAsync: async ( + url: string, + options?: NetworkRequestOptions + ): Promise => { return testNetworkResult; - } + }, }, loggerOptions: { - loggerCallback: (level: LogLevel, message: string, containsPii: boolean): void => { + loggerCallback: ( + level: LogLevel, + message: string, + containsPii: boolean + ): void => { if (containsPii) { console.log(`Log level: ${level} Message: ${message}`); } }, - piiLoggingEnabled: true - } + piiLoggingEnabled: true, + }, }); browserStorage = new BrowserStorage(TEST_CONFIG.MSAL_CLIENT_ID, configObj.cache); silentHandler = new SilentHandler(authCodeModule, browserStorage, DEFAULT_IFRAME_TIMEOUT_MS); diff --git a/lib/msal-common/src/authority/AuthorityFactory.ts b/lib/msal-common/src/authority/AuthorityFactory.ts index b496bcbc93..373c3f9f29 100644 --- a/lib/msal-common/src/authority/AuthorityFactory.ts +++ b/lib/msal-common/src/authority/AuthorityFactory.ts @@ -13,7 +13,7 @@ import { StringUtils } from "./../utils/StringUtils"; import { UrlString } from "./../url/UrlString"; import { Constants } from "../utils/Constants"; -export class AuthorityFactory { +export class AuthorityFactory { /** * Parse the url and determine the type of authority @@ -43,6 +43,7 @@ export class AuthorityFactory { } const type = AuthorityFactory.detectAuthorityFromUrl(authorityUrl); + // Depending on above detection, create the right type. switch (type) { case AuthorityType.Aad: diff --git a/lib/msal-common/src/cache/CacheHelpers.ts b/lib/msal-common/src/cache/CacheHelpers.ts index 6ee21f3f19..1710e7092c 100644 --- a/lib/msal-common/src/cache/CacheHelpers.ts +++ b/lib/msal-common/src/cache/CacheHelpers.ts @@ -23,6 +23,7 @@ export class CacheHelpers { constructor(cacheImpl: ICacheStorage) { this.cacheStorage = cacheImpl; + } /** @@ -167,11 +168,11 @@ export class CacheHelpers { /** * Checks that any parameters are exact matches for key value, since key.match in the above functions only do contains checks, not exact matches. - * @param atKey - * @param clientId - * @param authority - * @param resource - * @param homeAccountIdentifier + * @param atKey + * @param clientId + * @param authority + * @param resource + * @param homeAccountIdentifier */ private checkForExactKeyMatch(atKey: AccessTokenKey, clientId: string, authority: string, resource?: string, homeAccountIdentifier?: string): boolean { const hasClientId = (atKey.clientId === clientId); diff --git a/lib/msal-common/src/cache/ICacheStorage.ts b/lib/msal-common/src/cache/ICacheStorage.ts index bb97e7c9e6..0c2f5289d0 100644 --- a/lib/msal-common/src/cache/ICacheStorage.ts +++ b/lib/msal-common/src/cache/ICacheStorage.ts @@ -3,35 +3,55 @@ * Licensed under the MIT License. */ +import { InMemoryCache } from "../unifiedCache/utils/CacheTypes"; + /** * Interface class which implement cache storage functions used by MSAL to perform validity checks, and store tokens. */ export interface ICacheStorage { + /** + * Function to read serialized Cache from disk + * @param key + * @param value + */ + getCache(): InMemoryCache; + + /** + * Function to write serialized Cache to disk + * @param cache + */ + setCache(cache: InMemoryCache): void; + /** * Function to set item in cache. - * @param key - * @param value + * @param key + * @param value */ setItem(key: string, value: string): void; + /** * Function which retrieves item from cache. - * @param key + * @param key */ getItem(key: string): string; + /** * Function which removes item from cache. - * @param key + * @param key */ removeItem(key: string): void; + /** * Function which returns boolean whether cache contains a specific key. - * @param key + * @param key */ containsKey(key: string): boolean; + /** * Function which retrieves all current keys from the cache. */ getKeys(): string[]; + /** * Function which clears cache. */ diff --git a/lib/msal-common/src/client/AuthorizationCodeClient.ts b/lib/msal-common/src/client/AuthorizationCodeClient.ts index 406ad46d79..fa7b0ca0a1 100644 --- a/lib/msal-common/src/client/AuthorizationCodeClient.ts +++ b/lib/msal-common/src/client/AuthorizationCodeClient.ts @@ -11,9 +11,11 @@ import { RequestParameterBuilder } from "../server/RequestParameterBuilder"; import { RequestValidator } from "../request/RequestValidator"; import { GrantType } from "../utils/Constants"; import { ClientConfiguration } from "../config/ClientConfiguration"; -import {ServerAuthorizationTokenResponse} from "../server/ServerAuthorizationTokenResponse"; -import {NetworkResponse} from "../network/NetworkManager"; -import {ScopeSet} from "../request/ScopeSet"; +import { ServerAuthorizationTokenResponse } from "../server/ServerAuthorizationTokenResponse"; +import { NetworkResponse } from "../network/NetworkManager"; +import { ScopeSet } from "../request/ScopeSet"; +import { ResponseHandler } from "../response/ResponseHandler"; +import { AuthenticationResult } from "../response/AuthenticationResult"; /** * Oauth2.0 Authorization Code client @@ -45,11 +47,28 @@ export class AuthorizationCodeClient extends BaseClient { * authorization_code_grant * @param request */ - async acquireToken(request: AuthorizationCodeRequest): Promise { + async acquireToken(request: AuthorizationCodeRequest): Promise { + + this.logger.info("in acquireToken call"); const response = await this.executeTokenRequest(this.defaultAuthority, request); - return JSON.stringify(response.body); - // TODO add response_handler here to send the response + + const responseHandler = new ResponseHandler( + this.config.authOptions.clientId, + this.unifiedCacheManager, + this.cryptoUtils, + this.logger + ); + + responseHandler.validateTokenResponse(response.body); + const tokenResponse = await responseHandler.generateAuthenticationResult( + response.body, + this.defaultAuthority + ); + + // set the final cache and return the auth response + this.updateCache(); + return tokenResponse; } /** @@ -94,6 +113,7 @@ export class AuthorizationCodeClient extends BaseClient { } parameterBuilder.addGrantType(GrantType.AUTHORIZATION_CODE_GRANT); + parameterBuilder.addClientInfo(); return parameterBuilder.createQueryString(); } diff --git a/lib/msal-common/src/client/BaseClient.ts b/lib/msal-common/src/client/BaseClient.ts index a887c49999..f5fbaf85eb 100644 --- a/lib/msal-common/src/client/BaseClient.ts +++ b/lib/msal-common/src/client/BaseClient.ts @@ -14,6 +14,7 @@ import { Logger } from "../logger/Logger"; import { AADServerParamKeys, Constants, HeaderNames } from "../utils/Constants"; import { NetworkResponse } from "../network/NetworkManager"; import { ServerAuthorizationTokenResponse } from "../server/ServerAuthorizationTokenResponse"; +import { UnifiedCacheManager } from "../unifiedCache/UnifiedCacheManager"; /** * Base application class which will construct requests to send to and handle responses from the Microsoft STS using the authorization code flow. @@ -36,7 +37,10 @@ export abstract class BaseClient { protected networkClient: INetworkModule; // Helper API object for running cache functions - protected cacheManager: CacheHelpers; + protected spaCacheManager: CacheHelpers; + + // Helper API object for serialized cache operations + protected unifiedCacheManager: UnifiedCacheManager; // Account object protected account: Account; @@ -58,7 +62,10 @@ export abstract class BaseClient { this.cacheStorage = this.config.storageInterface; // Initialize storage helper object - this.cacheManager = new CacheHelpers(this.cacheStorage); + this.spaCacheManager = new CacheHelpers(this.cacheStorage); + + // Initialize serialized cache manager + this.unifiedCacheManager = new UnifiedCacheManager(this.cacheStorage); // Set the network interface this.networkClient = this.config.networkInterface; @@ -110,4 +117,13 @@ export abstract class BaseClient { headers: headers, }); } + + /** + * TODO: modify this soon + * Set the cache post acquireToken call + */ + protected updateCache(): void { + const cache = this.unifiedCacheManager.getCacheInMemory(); + this.cacheStorage.setCache(cache); + } } diff --git a/lib/msal-common/src/client/SPAClient.ts b/lib/msal-common/src/client/SPAClient.ts index 9d0054523a..947ef72946 100644 --- a/lib/msal-common/src/client/SPAClient.ts +++ b/lib/msal-common/src/client/SPAClient.ts @@ -11,7 +11,7 @@ import { ServerCodeRequestParameters } from "../server/ServerCodeRequestParamete import { ServerTokenRequestParameters } from "../server/ServerTokenRequestParameters"; import { CodeResponse } from "../response/CodeResponse"; import { TokenResponse } from "../response/TokenResponse"; -import { ResponseHandler } from "../response/ResponseHandler"; +import { SPAResponseHandler } from "../response/SPAResponseHandler"; import { ServerAuthorizationCodeResponse } from "../server/ServerAuthorizationCodeResponse"; import { ServerAuthorizationTokenResponse } from "../server/ServerAuthorizationTokenResponse"; import { ClientAuthError } from "../error/ClientAuthError"; @@ -98,7 +98,7 @@ export class SPAClient extends BaseClient { } // Update required cache entries for request. - this.cacheManager.updateCacheEntries(requestParameters, request.account); + this.spaCacheManager.updateCacheEntries(requestParameters, request.account); // Populate query parameters (sid/login_hint/domain_hint) and any other extraQueryParameters set by the developer. requestParameters.populateQueryParams(adalIdToken); @@ -120,7 +120,7 @@ export class SPAClient extends BaseClient { return urlNavigate; } catch (e) { // Reset cache items before re-throwing. - this.cacheManager.resetTempCacheItems(requestParameters && requestParameters.state); + this.spaCacheManager.resetTempCacheItems(requestParameters && requestParameters.state); throw e; } } @@ -168,7 +168,7 @@ export class SPAClient extends BaseClient { return await this.getTokenResponse(tokenEndpoint, tokenReqParams, tokenRequest, codeResponse); } catch (e) { // Reset cache items and set account to null before re-throwing. - this.cacheManager.resetTempCacheItems(codeResponse && codeResponse.userRequestState); + this.spaCacheManager.resetTempCacheItems(codeResponse && codeResponse.userRequestState); this.account = null; throw e; } @@ -230,7 +230,7 @@ export class SPAClient extends BaseClient { // Only populate id token if it exists in cache item. return StringUtils.isEmpty(cachedTokenItem.value.idToken) ? defaultTokenResponse : - ResponseHandler.setResponseIdToken(defaultTokenResponse, new IdToken(cachedTokenItem.value.idToken, this.cryptoUtils)); + SPAResponseHandler.setResponseIdToken(defaultTokenResponse, new IdToken(cachedTokenItem.value.idToken, this.cryptoUtils)); } else { // Renew the tokens. request.authority = cachedTokenItem.key.authority; @@ -240,7 +240,7 @@ export class SPAClient extends BaseClient { } } catch (e) { // Reset cache items and set account to null before re-throwing. - this.cacheManager.resetTempCacheItems(); + this.spaCacheManager.resetTempCacheItems(); this.account = null; throw e; } @@ -258,7 +258,7 @@ export class SPAClient extends BaseClient { // Check for homeAccountIdentifier. Do not send anything if it doesn't exist. const homeAccountIdentifier = currentAccount ? currentAccount.homeAccountIdentifier : ""; // Remove all pertinent access tokens. - this.cacheManager.removeAllAccessTokens(this.config.authOptions.clientId, authorityUri, "", homeAccountIdentifier); + this.spaCacheManager.removeAllAccessTokens(this.config.authOptions.clientId, authorityUri, "", homeAccountIdentifier); // Clear remaining cache items. this.cacheStorage.clear(); // Clear current account. @@ -295,7 +295,7 @@ export class SPAClient extends BaseClient { */ public handleFragmentResponse(hashFragment: string): CodeResponse { // Handle responses. - const responseHandler = new ResponseHandler(this.config.authOptions.clientId, this.cacheStorage, this.cacheManager, this.cryptoUtils, this.logger); + const responseHandler = new SPAResponseHandler(this.config.authOptions.clientId, this.cacheStorage, this.spaCacheManager, this.cryptoUtils, this.logger); // Deserialize hash fragment response parameters. const hashUrlString = new UrlString(hashFragment); const serverParams = hashUrlString.getDeserializedHash(); @@ -312,7 +312,7 @@ export class SPAClient extends BaseClient { */ public cancelRequest(): void { const cachedState = this.cacheStorage.getItem(TemporaryCacheKeys.REQUEST_STATE); - this.cacheManager.resetTempCacheItems(cachedState || ""); + this.spaCacheManager.resetTempCacheItems(cachedState || ""); } /** @@ -326,7 +326,7 @@ export class SPAClient extends BaseClient { this.cacheStorage.removeItem(TemporaryCacheKeys.REQUEST_PARAMS); // Get cached authority and use if no authority is cached with request. if (StringUtils.isEmpty(parsedRequest.authority)) { - const authorityKey: string = this.cacheManager.generateAuthorityKey(state); + const authorityKey: string = this.spaCacheManager.generateAuthorityKey(state); const cachedAuthority: string = this.cacheStorage.getItem(authorityKey); parsedRequest.authority = cachedAuthority; } @@ -345,7 +345,7 @@ export class SPAClient extends BaseClient { */ private getCachedTokens(requestScopes: ScopeSet, authorityUri: string, resourceId: string, homeAccountIdentifier: string): AccessTokenCacheItem { // Get all access tokens with matching authority, resource id and home account ID - const tokenCacheItems: Array = this.cacheManager.getAllAccessTokens(this.config.authOptions.clientId, authorityUri || "", resourceId || "", homeAccountIdentifier || ""); + const tokenCacheItems: Array = this.spaCacheManager.getAllAccessTokens(this.config.authOptions.clientId, authorityUri || "", resourceId || "", homeAccountIdentifier || ""); if (tokenCacheItems.length === 0) { throw ClientAuthError.createNoTokensFoundError(requestScopes.printScopes()); } @@ -385,7 +385,7 @@ export class SPAClient extends BaseClient { ); // Create response handler - const responseHandler = new ResponseHandler(this.config.authOptions.clientId, this.cacheStorage, this.cacheManager, this.cryptoUtils, this.logger); + const responseHandler = new SPAResponseHandler(this.config.authOptions.clientId, this.cacheStorage, this.spaCacheManager, this.cryptoUtils, this.logger); // Validate response. This function throws a server error if an error is returned by the server. responseHandler.validateServerAuthorizationTokenResponse(acquiredTokenResponse.body); // Return token response with given parameters diff --git a/lib/msal-common/src/config/ClientConfiguration.ts b/lib/msal-common/src/config/ClientConfiguration.ts index 019e8bf1ca..0e94e6439d 100644 --- a/lib/msal-common/src/config/ClientConfiguration.ts +++ b/lib/msal-common/src/config/ClientConfiguration.ts @@ -10,6 +10,7 @@ import { AuthError } from "../error/AuthError"; import { ILoggerCallback, LogLevel } from "../logger/Logger"; import { Constants } from "../utils/Constants"; import { version } from "../../package.json"; +import { InMemoryCache } from "../unifiedCache/utils/CacheTypes"; import { Authority } from "../authority/Authority"; // Token renewal offset default in seconds @@ -135,6 +136,14 @@ const DEFAULT_STORAGE_IMPLEMENTATION: ICacheStorage = { setItem: () => { const notImplErr = "Storage interface - setItem() 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); + }, + setCache: () => { + const notImplErr = "Storage interface - setCache() has not been implemented for the cacheStorage interface."; + throw AuthError.createUnexpectedError(notImplErr); } }; diff --git a/lib/msal-common/src/index.ts b/lib/msal-common/src/index.ts index b7f0950749..b2579d9d1b 100644 --- a/lib/msal-common/src/index.ts +++ b/lib/msal-common/src/index.ts @@ -1,6 +1,6 @@ // App Auth Modules and Configuration export { SPAClient } from "./client/SPAClient"; -export { AuthorizationCodeClient } from "./client/AuthorizationCodeClient"; +export { AuthorizationCodeClient} from "./client/AuthorizationCodeClient"; export { DeviceCodeClient } from "./client/DeviceCodeClient"; export { RefreshTokenClient } from "./client/RefreshTokenClient"; export { @@ -16,6 +16,10 @@ export { B2cAuthority } from "./authority/B2cAuthority"; export { AuthorityFactory } from "./authority/AuthorityFactory"; // Cache export { ICacheStorage } from "./cache/ICacheStorage"; +export { UnifiedCacheManager } from "./unifiedCache/UnifiedCacheManager"; +export { JsonCache, InMemoryCache } from "./unifiedCache/utils/CacheTypes"; +export { Serializer } from "./unifiedCache/serialize/Serializer"; +export { Deserializer } from "./unifiedCache/serialize/Deserializer"; // Network Interface export { INetworkModule, NetworkRequestOptions } from "./network/INetworkModule"; export { NetworkResponse } from "./network/NetworkManager"; @@ -48,3 +52,4 @@ export { Constants, PromptValue, TemporaryCacheKeys, PersistentCacheKeys, Prompt, ResponseMode } from "./utils/Constants"; export { StringUtils } from "./utils/StringUtils"; +export { StringDict } from "./utils/MsalTypes"; diff --git a/lib/msal-common/src/request/AuthorizationCodeUrlRequest.ts b/lib/msal-common/src/request/AuthorizationCodeUrlRequest.ts index 3b5eb6fb86..055196b438 100644 --- a/lib/msal-common/src/request/AuthorizationCodeUrlRequest.ts +++ b/lib/msal-common/src/request/AuthorizationCodeUrlRequest.ts @@ -81,6 +81,7 @@ export type AuthorizationCodeUrlRequest = { /** * Provides a hint about the tenant or domain that the user should use to sign in. The value * of the domain hint is a registered domain for the tenant. + * TODO: Name this as "extraQueryParameters" */ domainHint?: string; diff --git a/lib/msal-common/src/response/AuthenticationResult.ts b/lib/msal-common/src/response/AuthenticationResult.ts index 300f620f55..bb5e4acc01 100644 --- a/lib/msal-common/src/response/AuthenticationResult.ts +++ b/lib/msal-common/src/response/AuthenticationResult.ts @@ -3,13 +3,23 @@ * Licensed under the MIT License. */ +import { StringDict } from "../utils/MsalTypes"; + /** * Result returned from the authority's token endpoint. */ -export type AuthenticationResult = { +// TODO: Also consider making an external type and use this as internal +export class AuthenticationResult { // TODO this is temp class, it will be updated. - accessToken: string; - refreshToken: string; + uniqueId: string; // TODO: Check applicability + tenantId: string; // TODO: Check applicability + scopes: Array; + tokenType: string; // TODO: get rid of this if we can idToken: string; - expiresOn: 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 +} diff --git a/lib/msal-common/src/response/ResponseHandler.ts b/lib/msal-common/src/response/ResponseHandler.ts index 0843983877..f3ee4552f4 100644 --- a/lib/msal-common/src/response/ResponseHandler.ts +++ b/lib/msal-common/src/response/ResponseHandler.ts @@ -2,26 +2,25 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ -import { IdToken } from "../account/IdToken"; -import { CacheHelpers } from "../cache/CacheHelpers"; import { ServerAuthorizationTokenResponse } from "../server/ServerAuthorizationTokenResponse"; -import { ScopeSet } from "../request/ScopeSet"; import { buildClientInfo, ClientInfo } from "../account/ClientInfo"; -import { Account } from "../account/Account"; -import { ProtocolUtils } from "../utils/ProtocolUtils"; import { ICrypto } from "../crypto/ICrypto"; -import { ICacheStorage } from "../cache/ICacheStorage"; -import { TokenResponse } from "./TokenResponse"; -import { PersistentCacheKeys, TemporaryCacheKeys } from "../utils/Constants"; import { ClientAuthError } from "../error/ClientAuthError"; -import { TimeUtils } from "../utils/TimeUtils"; -import { AccessTokenKey } from "../cache/AccessTokenKey"; -import { AccessTokenValue } from "../cache/AccessTokenValue"; import { StringUtils } from "../utils/StringUtils"; import { ServerAuthorizationCodeResponse } from "../server/ServerAuthorizationCodeResponse"; -import { CodeResponse } from "./CodeResponse"; import { Logger } from "../logger/Logger"; import { ServerError } from "../error/ServerError"; +import { IdToken } from "../account/IdToken"; +import { UnifiedCacheManager } from "../unifiedCache/UnifiedCacheManager"; +import { ScopeSet } from "../request/ScopeSet"; +import { TimeUtils } from "../utils/TimeUtils"; +import { AuthenticationResult } from "./AuthenticationResult"; +import { AccountEntity } from "../unifiedCache/entities/AccountEntity"; +import { Authority } from "../authority/Authority"; +import { AuthorityType } from "../authority/AuthorityType"; +import { IdTokenEntity } from "../unifiedCache/entities/IdTokenEntity"; +import { AccessTokenEntity } from "../unifiedCache/entities/AccessTokenEntity"; +import { RefreshTokenEntity } from "../unifiedCache/entities/RefreshTokenEntity"; import { InteractionRequiredAuthError } from "../error/InteractionRequiredAuthError"; /** @@ -29,83 +28,30 @@ import { InteractionRequiredAuthError } from "../error/InteractionRequiredAuthEr */ export class ResponseHandler { private clientId: string; - private cacheStorage: ICacheStorage; - private cacheManager: CacheHelpers; + private uCacheManager: UnifiedCacheManager; private cryptoObj: ICrypto; private logger: Logger; + private clientInfo: ClientInfo; + private homeAccountIdentifier: string; - constructor(clientId: string, cacheStorage: ICacheStorage, cacheManager: CacheHelpers, cryptoObj: ICrypto, logger: Logger) { + constructor(clientId: string, unifiedCacheManager: UnifiedCacheManager, cryptoObj: ICrypto, logger: Logger) { this.clientId = clientId; - this.cacheStorage = cacheStorage; - this.cacheManager = cacheManager; + this.uCacheManager = unifiedCacheManager; this.cryptoObj = cryptoObj; this.logger = logger; } - /** - * Returns a new response with the data from original response filled with the relevant IdToken data. - * - raw id token - * - id token claims - * - unique id (oid or sub claim of token) - * - tenant id (tid claim of token) - * @param originalResponse - * @param idTokenObj - */ - static setResponseIdToken(originalResponse: TokenResponse, idTokenObj: IdToken) : TokenResponse { - if (!originalResponse) { - return null; - } else if (!idTokenObj) { - return originalResponse; - } - - const expiresSeconds = Number(idTokenObj.claims.exp); - if (expiresSeconds && !originalResponse.expiresOn) { - originalResponse.expiresOn = new Date(expiresSeconds * 1000); - } - - return { - ...originalResponse, - idToken: idTokenObj.rawIdToken, - idTokenClaims: idTokenObj.claims, - uniqueId: idTokenObj.claims.oid || idTokenObj.claims.sub, - tenantId: idTokenObj.claims.tid, - }; - } - - /** - * Validates and handles a response from the server, and returns a constructed object with the authorization code and state. - * @param serverParams - */ - public handleServerCodeResponse(serverParams: ServerAuthorizationCodeResponse): CodeResponse { - try { - // Validate hash fragment response parameters - this.validateServerAuthorizationCodeResponse(serverParams, this.cacheStorage.getItem(TemporaryCacheKeys.REQUEST_STATE), this.cryptoObj); - - // Cache client info - if (serverParams.client_info) { - this.cacheStorage.setItem(PersistentCacheKeys.CLIENT_INFO, serverParams.client_info); - } - - // Create response object - const response: CodeResponse = { - code: serverParams.code, - userRequestState: serverParams.state - }; - - return response; - } catch(e) { - this.cacheManager.resetTempCacheItems(serverParams && serverParams.state); - throw e; - } - } - /** * Function which validates server authorization code response. * @param serverResponseHash * @param cachedState * @param cryptoObj */ - private validateServerAuthorizationCodeResponse(serverResponseHash: ServerAuthorizationCodeResponse, cachedState: string, cryptoObj: ICrypto): void { + validateServerAuthorizationCodeResponse( + serverResponseHash: ServerAuthorizationCodeResponse, + cachedState: string, + cryptoObj: ICrypto + ): void { if (serverResponseHash.state !== cachedState) { throw ClientAuthError.createStateMismatchError(); } @@ -128,7 +74,9 @@ export class ResponseHandler { * Function which validates server authorization token response. * @param serverResponse */ - public validateServerAuthorizationTokenResponse(serverResponse: ServerAuthorizationTokenResponse): void { + validateTokenResponse( + serverResponse: ServerAuthorizationTokenResponse + ): void { // Check for error if (serverResponse.error || serverResponse.error_description || serverResponse.suberror) { if (InteractionRequiredAuthError.isInteractionRequiredError(serverResponse.error, serverResponse.error_description, serverResponse.suberror)) { @@ -138,163 +86,154 @@ export class ResponseHandler { const errString = `${serverResponse.error_codes} - [${serverResponse.timestamp}]: ${serverResponse.error_description} - Correlation ID: ${serverResponse.correlation_id} - Trace ID: ${serverResponse.trace_id}`; throw new ServerError(serverResponse.error, errString); } + + // generate homeAccountId + if (serverResponse.client_info) { + this.clientInfo = buildClientInfo(serverResponse.client_info, this.cryptoObj); + if (!StringUtils.isEmpty(this.clientInfo.uid) && !StringUtils.isEmpty(this.clientInfo.utid)) { + this.homeAccountIdentifier = this.cryptoObj.base64Encode(this.clientInfo.uid) + "." + this.cryptoObj.base64Encode(this.clientInfo.utid); + } + } } /** - * Helper function which saves or updates the token in the cache and constructs the final token response to send back to the user. - * @param originalTokenResponse - * @param authority - * @param resource + * Returns a constructed token response based on given string. Also manages the cache updates and cleanups. * @param serverTokenResponse - * @param clientInfo + * @param authorityString + * @param resource + * @param state */ - private saveToken(originalTokenResponse: TokenResponse, authority: string, resource: string, serverTokenResponse: ServerAuthorizationTokenResponse, clientInfo: ClientInfo): TokenResponse { - // Set consented scopes in response - const responseScopes = ScopeSet.fromString(serverTokenResponse.scope, this.clientId, true); - const responseScopeArray = responseScopes.asArray(); - - // Expiration calculation - const expiresIn = serverTokenResponse.expires_in; - const expirationSec = TimeUtils.nowSeconds() + expiresIn; - const extendedExpirationSec = expirationSec + serverTokenResponse.ext_expires_in; - - // Get id token - if (!StringUtils.isEmpty(originalTokenResponse.idToken)) { - this.cacheStorage.setItem(PersistentCacheKeys.ID_TOKEN, originalTokenResponse.idToken); - } - - // Save access token in cache - const newAccessTokenValue = new AccessTokenValue(serverTokenResponse.token_type, serverTokenResponse.access_token, originalTokenResponse.idToken, serverTokenResponse.refresh_token, expirationSec.toString(), extendedExpirationSec.toString()); - const homeAccountIdentifier = originalTokenResponse.account && originalTokenResponse.account.homeAccountIdentifier; - const accessTokenCacheItems = this.cacheManager.getAllAccessTokens(this.clientId, authority || "", resource || "", homeAccountIdentifier || ""); + 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 - // If no items in cache with these parameters, set new item. - if (accessTokenCacheItems.length < 1) { - this.logger.info("No tokens found, creating new item."); - } else { - // Check if scopes are intersecting. If they are, combine scopes and replace cache item. - accessTokenCacheItems.forEach(accessTokenCacheItem => { - const cachedScopes = ScopeSet.fromString(accessTokenCacheItem.key.scopes, this.clientId, true); - if(cachedScopes.intersectingScopeSets(responseScopes)) { - this.cacheStorage.removeItem(JSON.stringify(accessTokenCacheItem.key)); - responseScopes.appendScopes(cachedScopes.asArray()); - if (StringUtils.isEmpty(newAccessTokenValue.idToken)) { - newAccessTokenValue.idToken = accessTokenCacheItem.value.idToken; - } - } - }); - } + const authenticationResult = this.processTokenResponse(serverTokenResponse, authority); - const newTokenKey = new AccessTokenKey( - authority, - this.clientId, - responseScopes.printScopes(), - resource, - clientInfo && clientInfo.uid, - clientInfo && clientInfo.utid, - this.cryptoObj - ); - this.cacheStorage.setItem(JSON.stringify(newTokenKey), JSON.stringify(newAccessTokenValue)); + const environment = authority.canonicalAuthorityUrlComponents.HostNameAndPort; + this.addCredentialsToCache(authenticationResult, environment, serverTokenResponse.refresh_token); - // Save tokens in response and return - return { - ...originalTokenResponse, - tokenType: serverTokenResponse.token_type, - scopes: responseScopeArray, - accessToken: serverTokenResponse.access_token, - refreshToken: serverTokenResponse.refresh_token, - expiresOn: new Date(expirationSec * 1000) - }; + return authenticationResult; } /** - * Gets account cached with given key. Returns null if parsing could not be completed. - * @param accountKey + * 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) */ - private getCachedAccount(accountKey: string): Account { - try { - return JSON.parse(this.cacheStorage.getItem(accountKey)) as Account; - } catch (e) { - this.logger.warning(`Account could not be parsed: ${JSON.stringify(e)}`); - return null; - } - } - - /** - * Returns a constructed token response based on given string. Also manages the cache updates and cleanups. - * @param serverTokenResponse - * @param authorityString - * @param resource - * @param state - */ - public createTokenResponse(serverTokenResponse: ServerAuthorizationTokenResponse, authorityString: string, resource: string, state?: string): TokenResponse { - let tokenResponse: TokenResponse = { + processTokenResponse(serverTokenResponse: ServerAuthorizationTokenResponse, authority: Authority): AuthenticationResult { + const authenticationResult: AuthenticationResult = { uniqueId: "", tenantId: "", tokenType: "", idToken: null, idTokenClaims: null, accessToken: "", - refreshToken: "", scopes: [], expiresOn: null, - account: null, - userRequestState: "" + familyId: null }; - // Retrieve current id token object - let idTokenObj: IdToken; - const cachedIdToken: string = this.cacheStorage.getItem(PersistentCacheKeys.ID_TOKEN); - if (serverTokenResponse.id_token) { - idTokenObj = new IdToken(serverTokenResponse.id_token, this.cryptoObj); - tokenResponse = ResponseHandler.setResponseIdToken(tokenResponse, idTokenObj); + // IdToken + const idTokenObj = new IdToken(serverTokenResponse.id_token, this.cryptoObj); - // If state is empty, refresh token is being used - if (!StringUtils.isEmpty(state)) { - this.logger.info("State was detected - nonce should be available."); - // check nonce integrity if refresh token is not used - throw an error if not matched - if (StringUtils.isEmpty(idTokenObj.claims.nonce)) { - throw ClientAuthError.createInvalidIdTokenError(idTokenObj); - } + // if account is not in cache, append it to the cache + this.addAccountToCache(serverTokenResponse, idTokenObj, authority); - const nonce = this.cacheStorage.getItem(this.cacheManager.generateNonceKey(state)); - if (idTokenObj.claims.nonce !== nonce) { - throw ClientAuthError.createNonceMismatchError(); - } - } - } else if (cachedIdToken) { - idTokenObj = new IdToken(cachedIdToken, this.cryptoObj); - tokenResponse = ResponseHandler.setResponseIdToken(tokenResponse, idTokenObj); - } else { - idTokenObj = null; + // 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); } - let clientInfo: ClientInfo = null; - let cachedAccount: Account = null; - if (idTokenObj) { - // Retrieve client info - clientInfo = buildClientInfo(this.cacheStorage.getItem(PersistentCacheKeys.CLIENT_INFO), this.cryptoObj); + // 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); - // Create account object for request - tokenResponse.account = Account.createAccount(idTokenObj, clientInfo, this.cryptoObj); + return { + ...authenticationResult, + uniqueId: idTokenObj.claims.oid || idTokenObj.claims.sub, + tenantId: idTokenObj.claims.tid, + idToken: idTokenObj.rawIdToken, + idTokenClaims: idTokenObj.claims, + accessToken: serverTokenResponse.access_token, + expiresOn: new Date(expiresInSeconds), + extExpiresOn: new Date(extendedExpiresInSeconds), + scopes: responseScopes.asArray(), + familyId: serverTokenResponse.foci, + }; + } - // Save the access token if it exists - const accountKey = this.cacheManager.generateAcquireTokenAccountKey(tokenResponse.account.homeAccountIdentifier); + /** + * 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); + } + } - // Get cached account - cachedAccount = this.getCachedAccount(accountKey); + /** + * Generate Account + * @param serverTokenResponse + * @param idToken + * @param authority + */ + generateAccountEntity(serverTokenResponse: ServerAuthorizationTokenResponse, idToken: IdToken, authority: Authority): AccountEntity { + const authorityType = authority.authorityType; + + if (!serverTokenResponse.client_info) + throw ClientAuthError.createClientInfoEmptyError(serverTokenResponse.client_info); + + switch (authorityType) { + case AuthorityType.B2C: + return AccountEntity.createAccount(serverTokenResponse.client_info, authority, idToken, "policy", this.cryptoObj); + case AuthorityType.Adfs: + return AccountEntity.createADFSAccount(authority, idToken); + // default to AAD + default: + return AccountEntity.createAccount(serverTokenResponse.client_info, authority, idToken, null, this.cryptoObj); } + } - // Return user set state in the response - tokenResponse.userRequestState = ProtocolUtils.getUserRequestState(state); + /** + * Appends the minted tokens to the in-memory cache + * @param authenticationResult + * @param authority + */ + addCredentialsToCache( + authenticationResult: AuthenticationResult, + authority: string, + refreshToken: string + ): void { + const idTokenEntity = IdTokenEntity.createIdTokenEntity( + this.homeAccountIdentifier, + authenticationResult, + this.clientId, + authority + ); + const accessTokenEntity = AccessTokenEntity.createAccessTokenEntity( + this.homeAccountIdentifier, + authenticationResult, + this.clientId, + authority + ); + const refreshTokenEntity = RefreshTokenEntity.createRefreshTokenEntity( + this.homeAccountIdentifier, + authenticationResult, + refreshToken, + this.clientId, + authority + ); - this.cacheManager.resetTempCacheItems(state); - if (!cachedAccount || !tokenResponse.account || Account.compareAccounts(cachedAccount, tokenResponse.account)) { - return this.saveToken(tokenResponse, authorityString, resource, serverTokenResponse, clientInfo); - } else { - this.logger.error("Accounts do not match."); - this.logger.errorPii(`Cached Account: ${JSON.stringify(cachedAccount)}, New Account: ${JSON.stringify(tokenResponse.account)}`); - throw ClientAuthError.createAccountMismatchError(); - } + this.uCacheManager.addCredentialCache(accessTokenEntity, idTokenEntity, refreshTokenEntity); } } diff --git a/lib/msal-common/src/response/SPAResponseHandler.ts b/lib/msal-common/src/response/SPAResponseHandler.ts new file mode 100644 index 0000000000..27087c715e --- /dev/null +++ b/lib/msal-common/src/response/SPAResponseHandler.ts @@ -0,0 +1,314 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { IdToken } from "../account/IdToken"; +import { CacheHelpers } from "../cache/CacheHelpers"; +import { ServerAuthorizationTokenResponse } from "../server/ServerAuthorizationTokenResponse"; +import { ScopeSet } from "../request/ScopeSet"; +import { buildClientInfo, ClientInfo } from "../account/ClientInfo"; +import { Account } from "../account/Account"; +import { ProtocolUtils } from "../utils/ProtocolUtils"; +import { ICrypto } from "../crypto/ICrypto"; +import { ICacheStorage } from "../cache/ICacheStorage"; +import { TokenResponse } from "./TokenResponse"; +import { PersistentCacheKeys, TemporaryCacheKeys } from "../utils/Constants"; +import { ClientAuthError } from "../error/ClientAuthError"; +import { TimeUtils } from "../utils/TimeUtils"; +import { AccessTokenKey } from "../cache/AccessTokenKey"; +import { AccessTokenValue } from "../cache/AccessTokenValue"; +import { StringUtils } from "../utils/StringUtils"; +import { ServerAuthorizationCodeResponse } from "../server/ServerAuthorizationCodeResponse"; +import { CodeResponse } from "./CodeResponse"; +import { Logger } from "../logger/Logger"; +import { ServerError } from "../error/ServerError"; +import { InteractionRequiredAuthError } from "../error/InteractionRequiredAuthError"; + +/** + * Class that handles response parsing. + */ +export class SPAResponseHandler { + private clientId: string; + private cacheStorage: ICacheStorage; + private spaCacheManager: CacheHelpers; + private cryptoObj: ICrypto; + private logger: Logger; + private clientInfo: ClientInfo; + private homeAccountIdentifier: string; + + constructor(clientId: string, cacheStorage: ICacheStorage, spaCacheManager: CacheHelpers, cryptoObj: ICrypto, logger: Logger) { + this.clientId = clientId; + this.cacheStorage = cacheStorage; + this.spaCacheManager = spaCacheManager; + this.cryptoObj = cryptoObj; + this.logger = logger; + } + + /** + * Returns a new response with the data from original response filled with the relevant IdToken data. + * - raw id token + * - id token claims + * - unique id (oid or sub claim of token) + * - tenant id (tid claim of token) + * @param originalResponse + * @param idTokenObj + */ + static setResponseIdToken(originalResponse: TokenResponse, idTokenObj: IdToken) : TokenResponse { + if (!originalResponse) { + return null; + } else if (!idTokenObj) { + return originalResponse; + } + + const expiresSeconds = Number(idTokenObj.claims.exp); + if (expiresSeconds && !originalResponse.expiresOn) { + originalResponse.expiresOn = new Date(expiresSeconds * 1000); + } + + return { + ...originalResponse, + idToken: idTokenObj.rawIdToken, + idTokenClaims: idTokenObj.claims, + uniqueId: idTokenObj.claims.oid || idTokenObj.claims.sub, + tenantId: idTokenObj.claims.tid, + }; + } + + /** + * Validates and handles a response from the server, and returns a constructed object with the authorization code and state. + * @param serverParams + */ + public handleServerCodeResponse(serverParams: ServerAuthorizationCodeResponse): CodeResponse { + try { + // Validate hash fragment response parameters + this.validateServerAuthorizationCodeResponse(serverParams, this.cacheStorage.getItem(TemporaryCacheKeys.REQUEST_STATE), this.cryptoObj); + + // Cache client info + if (serverParams.client_info) { + this.cacheStorage.setItem(PersistentCacheKeys.CLIENT_INFO, serverParams.client_info); + } + + // Create response object + const response: CodeResponse = { + code: serverParams.code, + userRequestState: serverParams.state + }; + + return response; + } catch(e) { + this.spaCacheManager.resetTempCacheItems(serverParams && serverParams.state); + throw e; + } + } + + /** + * Function which validates server authorization code response. + * @param serverResponseHash + * @param cachedState + * @param cryptoObj + */ + validateServerAuthorizationCodeResponse( + serverResponseHash: ServerAuthorizationCodeResponse, + cachedState: string, + cryptoObj: ICrypto + ): void { + if (serverResponseHash.state !== cachedState) { + throw ClientAuthError.createStateMismatchError(); + } + + // Check for error + if (serverResponseHash.error || serverResponseHash.error_description || serverResponseHash.suberror) { + if (InteractionRequiredAuthError.isInteractionRequiredError(serverResponseHash.error, serverResponseHash.error_description, serverResponseHash.suberror)) { + throw new InteractionRequiredAuthError(serverResponseHash.error, serverResponseHash.error_description, serverResponseHash.suberror); + } + + throw new ServerError(serverResponseHash.error, serverResponseHash.error_description, serverResponseHash.suberror); + } + + if (serverResponseHash.client_info) { + buildClientInfo(serverResponseHash.client_info, cryptoObj); + } + } + + /** + * Function which validates server authorization token response. + * @param serverResponse + */ + public validateServerAuthorizationTokenResponse(serverResponse: ServerAuthorizationTokenResponse): void { + // Check for error + if (serverResponse.error || serverResponse.error_description || serverResponse.suberror) { + if (InteractionRequiredAuthError.isInteractionRequiredError(serverResponse.error, serverResponse.error_description, serverResponse.suberror)) { + throw new InteractionRequiredAuthError(serverResponse.error, serverResponse.error_description, serverResponse.suberror); + } + + const errString = `${serverResponse.error_codes} - [${serverResponse.timestamp}]: ${serverResponse.error_description} - Correlation ID: ${serverResponse.correlation_id} - Trace ID: ${serverResponse.trace_id}`; + throw new ServerError(serverResponse.error, errString); + } + + // generate homeAccountId + if (serverResponse.client_info) { + this.clientInfo = buildClientInfo(serverResponse.client_info, this.cryptoObj); + if (!StringUtils.isEmpty(this.clientInfo.uid) && !StringUtils.isEmpty(this.clientInfo.utid)) { + this.homeAccountIdentifier = this.cryptoObj.base64Encode(this.clientInfo.uid) + "." + this.cryptoObj.base64Encode(this.clientInfo.utid); + } + } + } + + /** + * Helper function which saves or updates the token in the cache and constructs the final token response to send back to the user. + * @param originalTokenResponse + * @param authority + * @param resource + * @param serverTokenResponse + * @param clientInfo + */ + private saveToken(originalTokenResponse: TokenResponse, authority: string, resource: string, serverTokenResponse: ServerAuthorizationTokenResponse, clientInfo: ClientInfo): TokenResponse { + // Set consented scopes in response + const responseScopes = ScopeSet.fromString(serverTokenResponse.scope, this.clientId, true); + const responseScopeArray = responseScopes.asArray(); + + // Expiration calculation + const expiresIn = serverTokenResponse.expires_in; + const expirationSec = TimeUtils.nowSeconds() + expiresIn; + const extendedExpirationSec = expirationSec + serverTokenResponse.ext_expires_in; + + // Get id token + if (!StringUtils.isEmpty(originalTokenResponse.idToken)) { + this.cacheStorage.setItem(PersistentCacheKeys.ID_TOKEN, originalTokenResponse.idToken); + } + + // Save access token in cache + const newAccessTokenValue = new AccessTokenValue(serverTokenResponse.token_type, serverTokenResponse.access_token, originalTokenResponse.idToken, serverTokenResponse.refresh_token, expirationSec.toString(), extendedExpirationSec.toString()); + const homeAccountIdentifier = originalTokenResponse.account && originalTokenResponse.account.homeAccountIdentifier; + const accessTokenCacheItems = this.spaCacheManager.getAllAccessTokens(this.clientId, authority || "", resource || "", homeAccountIdentifier || ""); + + // If no items in cache with these parameters, set new item. + if (accessTokenCacheItems.length < 1) { + this.logger.info("No tokens found, creating new item."); + } else { + // Check if scopes are intersecting. If they are, combine scopes and replace cache item. + accessTokenCacheItems.forEach(accessTokenCacheItem => { + const cachedScopes = ScopeSet.fromString(accessTokenCacheItem.key.scopes, this.clientId, true); + if(cachedScopes.intersectingScopeSets(responseScopes)) { + this.cacheStorage.removeItem(JSON.stringify(accessTokenCacheItem.key)); + responseScopes.appendScopes(cachedScopes.asArray()); + if (StringUtils.isEmpty(newAccessTokenValue.idToken)) { + newAccessTokenValue.idToken = accessTokenCacheItem.value.idToken; + } + } + }); + } + + const newTokenKey = new AccessTokenKey( + authority, + this.clientId, + responseScopes.printScopes(), + resource, + clientInfo && clientInfo.uid, + clientInfo && clientInfo.utid, + this.cryptoObj + ); + this.cacheStorage.setItem(JSON.stringify(newTokenKey), JSON.stringify(newAccessTokenValue)); + + // Save tokens in response and return + return { + ...originalTokenResponse, + tokenType: serverTokenResponse.token_type, + scopes: responseScopeArray, + accessToken: serverTokenResponse.access_token, + refreshToken: serverTokenResponse.refresh_token, + expiresOn: new Date(expirationSec * 1000) + }; + } + + /** + * Gets account cached with given key. Returns null if parsing could not be completed. + * @param accountKey + */ + private getCachedAccount(accountKey: string): Account { + try { + return JSON.parse(this.cacheStorage.getItem(accountKey)) as Account; + } catch (e) { + this.logger.warning(`Account could not be parsed: ${JSON.stringify(e)}`); + return null; + } + } + + /** + * Returns a constructed token response based on given string. Also manages the cache updates and cleanups. + * @param serverTokenResponse + * @param authorityString + * @param resource + * @param state + */ + public createTokenResponse(serverTokenResponse: ServerAuthorizationTokenResponse, authorityString: string, resource: string, state?: string): TokenResponse { + let tokenResponse: TokenResponse = { + uniqueId: "", + tenantId: "", + tokenType: "", + idToken: null, + idTokenClaims: null, + accessToken: "", + refreshToken: "", + scopes: [], + expiresOn: null, + account: null, + userRequestState: "" + }; + + // Retrieve current id token object + let idTokenObj: IdToken; + const cachedIdToken: string = this.cacheStorage.getItem(PersistentCacheKeys.ID_TOKEN); + if (serverTokenResponse.id_token) { + idTokenObj = new IdToken(serverTokenResponse.id_token, this.cryptoObj); + tokenResponse = SPAResponseHandler.setResponseIdToken(tokenResponse, idTokenObj); + + // If state is empty, refresh token is being used + if (!StringUtils.isEmpty(state)) { + this.logger.info("State was detected - nonce should be available."); + // check nonce integrity if refresh token is not used - throw an error if not matched + if (StringUtils.isEmpty(idTokenObj.claims.nonce)) { + throw ClientAuthError.createInvalidIdTokenError(idTokenObj); + } + + const nonce = this.cacheStorage.getItem(this.spaCacheManager.generateNonceKey(state)); + if (idTokenObj.claims.nonce !== nonce) { + throw ClientAuthError.createNonceMismatchError(); + } + } + } else if (cachedIdToken) { + idTokenObj = new IdToken(cachedIdToken, this.cryptoObj); + tokenResponse = SPAResponseHandler.setResponseIdToken(tokenResponse, idTokenObj); + } else { + idTokenObj = null; + } + + let clientInfo: ClientInfo = null; + let cachedAccount: Account = null; + if (idTokenObj) { + // Retrieve client info + clientInfo = buildClientInfo(this.cacheStorage.getItem(PersistentCacheKeys.CLIENT_INFO), this.cryptoObj); + + // Create account object for request + tokenResponse.account = Account.createAccount(idTokenObj, clientInfo, this.cryptoObj); + + // Save the access token if it exists + const accountKey = this.spaCacheManager.generateAcquireTokenAccountKey(tokenResponse.account.homeAccountIdentifier); + + // Get cached account + cachedAccount = this.getCachedAccount(accountKey); + } + + // Return user set state in the response + tokenResponse.userRequestState = ProtocolUtils.getUserRequestState(state); + + this.spaCacheManager.resetTempCacheItems(state); + if (!cachedAccount || !tokenResponse.account || Account.compareAccounts(cachedAccount, tokenResponse.account)) { + return this.saveToken(tokenResponse, authorityString, resource, serverTokenResponse, clientInfo); + } else { + this.logger.error("Accounts do not match."); + this.logger.errorPii(`Cached Account: ${JSON.stringify(cachedAccount)}, New Account: ${JSON.stringify(tokenResponse.account)}`); + throw ClientAuthError.createAccountMismatchError(); + } + } +} diff --git a/lib/msal-common/src/server/RequestParameterBuilder.ts b/lib/msal-common/src/server/RequestParameterBuilder.ts index e35bc3a832..7e79f1b614 100644 --- a/lib/msal-common/src/server/RequestParameterBuilder.ts +++ b/lib/msal-common/src/server/RequestParameterBuilder.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -import { AADServerParamKeys, Constants, Prompt, ResponseMode, SSOTypes } from "../utils/Constants"; +import { AADServerParamKeys, Constants, Prompt, ResponseMode, SSOTypes, ClientInfo } from "../utils/Constants"; import { ScopeSet } from "../request/ScopeSet"; import { ClientConfigurationError } from "../error/ClientConfigurationError"; @@ -186,6 +186,14 @@ export class RequestParameterBuilder { this.parameters.set(AADServerParamKeys.GRANT_TYPE, encodeURIComponent(grantType)); } + /** + * add client info + * + */ + addClientInfo(): void { + this.parameters.set(ClientInfo, "1"); + } + /** * Utility to create a URL from the params map */ diff --git a/lib/msal-common/src/server/ServerAuthorizationTokenResponse.ts b/lib/msal-common/src/server/ServerAuthorizationTokenResponse.ts index aba203c812..fe12e451f0 100644 --- a/lib/msal-common/src/server/ServerAuthorizationTokenResponse.ts +++ b/lib/msal-common/src/server/ServerAuthorizationTokenResponse.ts @@ -12,7 +12,7 @@ * - access_token: The requested access token. The app can use this token to authenticate to the secured resource, such as a web API. * - refresh_token: An OAuth 2.0 refresh token. The app can use this token acquire additional access tokens after the current access token expires. * - id_token: A JSON Web Token (JWT). The app can decode the segments of this token to request information about the user who signed in. - * + * * In case of error: * - error: An error code string that can be used to classify types of errors that occur, and can be used to react to errors. * - error_description: A specific error message that can help a developer identify the root cause of an authentication error. @@ -30,6 +30,8 @@ export type ServerAuthorizationTokenResponse = { access_token?: string; refresh_token?: string; id_token?: string; + client_info?: string; + foci?: string // Error error?: string; error_description?: string; diff --git a/lib/msal-common/src/unifiedCache/UnifiedCacheManager.ts b/lib/msal-common/src/unifiedCache/UnifiedCacheManager.ts new file mode 100644 index 0000000000..44d78f0842 --- /dev/null +++ b/lib/msal-common/src/unifiedCache/UnifiedCacheManager.ts @@ -0,0 +1,107 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * 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 { 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 { + + // Storage interface + private inMemoryCache: InMemoryCache; + private cacheStorage: ICacheStorage; + + constructor(cacheImpl: ICacheStorage) { + this.cacheStorage = cacheImpl; + this.inMemoryCache = this.cacheStorage.getCache(); + } + + /** + * setter for in cache memory + */ + setCacheInMemory(cache: InMemoryCache): void { + this.inMemoryCache = cache; + } + + /** + * get the cache in memory + */ + getCacheInMemory(): InMemoryCache { + return this.inMemoryCache; + } + + /** + * Initialize in memory cache from an exisiting cache vault + */ + generateInMemoryCache(cache: string): InMemoryCache { + return Deserializer.deserializeAllCache(Deserializer.deserializeJSONBlob(cache)); + } + + /** + * retrieves the final JSON + */ + generateJsonCache(inMemoryCache: InMemoryCache): JsonCache { + return Serializer.serializeAllCache(inMemoryCache); + } + + /** + * Returns all accounts in memory + */ + getAllAccounts(): AccountCache { + return this.inMemoryCache.accounts; + } + + /** + * Returns if the account is in Cache + * @param homeAccountId + * @param environment + * @param realm + */ + getAccount(homeAccountId: string, environment: string, realm: string): AccountEntity { + const accountCacheKey: Array = [ + homeAccountId, + environment, + realm + ]; + + const accountKey = accountCacheKey.join(Separators.CACHE_KEY_SEPARATOR).toLowerCase(); + + return this.inMemoryCache.accounts[accountKey] || null; + } + + /** + * append credential cache to in memory cache + * @param idT: IdTokenEntity + * @param at: AccessTokenEntity + * @param rt: RefreshTokenEntity + */ + addCredentialCache( + accessToken: AccessTokenEntity, + idToken: IdTokenEntity, + refreshToken: RefreshTokenEntity + ): void { + this.inMemoryCache.accessTokens[accessToken.generateAccessTokenEntityKey()] = accessToken; + this.inMemoryCache.idTokens[idToken.generateIdTokenEntityKey()] = idToken; + this.inMemoryCache.refreshTokens[refreshToken.generateRefreshTokenEntityKey()] = refreshToken; + } + + /** + * append account to the in memory cache + * @param account + */ + addAccountEntity(account: AccountEntity): void { + const accKey = account.generateAccountEntityKey(); + if (!this.inMemoryCache.accounts[accKey]) { + this.inMemoryCache.accounts[accKey] = account; + } + } +} diff --git a/lib/msal-common/src/unifiedCache/entities/AccessTokenEntity.ts b/lib/msal-common/src/unifiedCache/entities/AccessTokenEntity.ts new file mode 100644 index 0000000000..586316bf19 --- /dev/null +++ b/lib/msal-common/src/unifiedCache/entities/AccessTokenEntity.ts @@ -0,0 +1,79 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { Credential } from "./Credential"; +import { Separators } from "../../utils/Constants"; +import { AuthenticationResult } from "../../response/AuthenticationResult"; + +/** + * ACCESS_TOKEN Credential Type + */ +export class AccessTokenEntity extends Credential { + realm: string; + target: string; + cachedAt: string; + expiresOn: string; + extendedExpiresOn?: string; + refreshOn?: string; + keyId?: string; // for POP and SSH tokenTypes + tokenType?: string; + + /** + * Generate Account Cache Key as per the schema: -- + */ + public generateAccessTokenEntityKey(): string { + const accessTokenKeyArray: Array = [ + this.homeAccountId, + this.environment, + this.credentialType, + this.clientId, + this.realm, + this.target + ]; + + return accessTokenKeyArray.join(Separators.CACHE_KEY_SEPARATOR).toLowerCase(); + } + + /** + * Create AccessTokenEntity + * @param homeAccountId + * @param authenticationResult + * @param clientId + * @param authority + */ + static createAccessTokenEntity( + homeAccountId: string, + authenticationResult: AuthenticationResult, + clientId: string, + environment: string + ): AccessTokenEntity { + const atEntity: AccessTokenEntity = new AccessTokenEntity(); + + atEntity.homeAccountId = homeAccountId; + atEntity.credentialType = "AccessToken"; + atEntity.secret = authenticationResult.accessToken; + + const date = new Date(); + const currentTime = date.getMilliseconds() / 1000; + atEntity.cachedAt = currentTime.toString(); + + // 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.environment = environment; + atEntity.clientId = clientId; + atEntity.realm = authenticationResult.tenantId; + atEntity.target = authenticationResult.scopes.join(" "); + + return atEntity; + } +} diff --git a/lib/msal-common/src/unifiedCache/entities/AccountEntity.ts b/lib/msal-common/src/unifiedCache/entities/AccountEntity.ts new file mode 100644 index 0000000000..0515fb3b76 --- /dev/null +++ b/lib/msal-common/src/unifiedCache/entities/AccountEntity.ts @@ -0,0 +1,95 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { Separators, CacheAccountType } from "../../utils/Constants"; +import { Authority } from "../../authority/Authority"; +import { IdToken } from "../../account/IdToken"; +import { ICrypto } from "../../crypto/ICrypto"; +import { buildClientInfo } from "../../account/ClientInfo"; +import { StringUtils } from "../../utils/StringUtils"; + +/** + * Type that defines required and optional parameters for an Account field (based on universal cache schema implemented by all MSALs) + */ +export class AccountEntity { + homeAccountId: string; + environment: string; + realm: string; + localAccountId: string; + username: string; + authorityType: string; + name?: string; + clientInfo?: string; + lastModificationTime?: string; + lastModificationApp?: string; + + /** + * Generate Account Cache Key as per the schema: -- + */ + public generateAccountEntityKey(): string { + const accountCacheKeyArray: Array = [ + this.homeAccountId, + this.environment, + this.realm + ]; + + return accountCacheKeyArray.join(Separators.CACHE_KEY_SEPARATOR).toLowerCase(); + } + + /** + * Build Account cache from IdToken, clientInfo and authority/policy + * @param clientInfo + * @param authority + * @param idToken + * @param policy + */ + static createAccount(clientInfo: string, authority: Authority, idToken: IdToken, policy: string, crypto: ICrypto): AccountEntity { + const account: AccountEntity = new AccountEntity(); + + account.authorityType = CacheAccountType.MSSTS_ACCOUNT_TYPE; + account.clientInfo = clientInfo; + // TBD: Clarify "policy" addition + const clientInfoObj = buildClientInfo(clientInfo, crypto); + const homeAccountId = `${clientInfoObj.uid}${Separators.CLIENT_INFO_SEPARATOR}${clientInfoObj.utid}`; + account.homeAccountId = + policy !== null + ? homeAccountId + Separators.CACHE_KEY_SEPARATOR + policy + : homeAccountId; + account.environment = + authority.canonicalAuthorityUrlComponents.HostNameAndPort; + account.realm = authority.tenant; + + if (idToken) { + // How do you account for MSA CID here? + const localAccountId = !StringUtils.isEmpty(idToken.claims.oid) + ? idToken.claims.oid + : idToken.claims.sid; + account.localAccountId = localAccountId; + account.username = idToken.claims.preferred_username; + account.name = idToken.claims.name; + } + + return account; + } + + /** + * Build ADFS account type + * @param authority + * @param idToken + */ + static createADFSAccount(authority: Authority, idToken: IdToken): AccountEntity { + const account: AccountEntity = new AccountEntity(); + + account.authorityType = CacheAccountType.ADFS_ACCOUNT_TYPE; + account.homeAccountId = idToken.claims.sub; + account.environment = + authority.canonicalAuthorityUrlComponents.HostNameAndPort; + account.username = idToken.claims.upn; + // add uniqueName to claims + // account.name = idToken.claims.uniqueName; + + return account; + } +} diff --git a/lib/msal-common/src/unifiedCache/entities/AppMetadataEntity.ts b/lib/msal-common/src/unifiedCache/entities/AppMetadataEntity.ts new file mode 100644 index 0000000000..e1fbdaac76 --- /dev/null +++ b/lib/msal-common/src/unifiedCache/entities/AppMetadataEntity.ts @@ -0,0 +1,22 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { APP_META_DATA, Separators } from "../../utils/Constants"; + +/** + * APP_META_DATA Cache + */ +export class AppMetadataEntity { + clientId: string; + environment: string; + familyId?: string; + + /** + * Generate Account Cache Key as per the schema: -- + */ + generateAppMetaDataEntityKey(): string { + const appMetaDataKeyArray: Array = [APP_META_DATA, this.environment, this.clientId]; + return appMetaDataKeyArray.join(Separators.CACHE_KEY_SEPARATOR).toLowerCase(); + } +} diff --git a/lib/msal-common/src/unifiedCache/entities/Credential.ts b/lib/msal-common/src/unifiedCache/entities/Credential.ts new file mode 100644 index 0000000000..78309fd362 --- /dev/null +++ b/lib/msal-common/src/unifiedCache/entities/Credential.ts @@ -0,0 +1,15 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +/** + * Base type for credentials to be stored in the cache: eg: ACCESS_TOKEN, ID_TOKEN etc + */ +export class Credential { + homeAccountId: string; + environment: string; + credentialType: string; + clientId: string; + secret: string; +}; diff --git a/lib/msal-common/src/unifiedCache/entities/IdTokenEntity.ts b/lib/msal-common/src/unifiedCache/entities/IdTokenEntity.ts new file mode 100644 index 0000000000..fd674ac5d5 --- /dev/null +++ b/lib/msal-common/src/unifiedCache/entities/IdTokenEntity.ts @@ -0,0 +1,56 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { Credential } from "./Credential"; +import { Separators } from "../../utils/Constants"; +import { AuthenticationResult } from "../../response/AuthenticationResult"; + +/** + * ID_TOKEN Cache + */ +export class IdTokenEntity extends Credential { + realm: string; + + /** + * Generate Account Cache Key as per the schema: -- + */ + generateIdTokenEntityKey(): string { + const idTokenKeyArray: Array = [ + this.homeAccountId, + this.environment, + this.credentialType, + this.clientId, + this.realm, + "" // target + ]; + + return idTokenKeyArray.join(Separators.CACHE_KEY_SEPARATOR).toLowerCase(); + } + + /** + * Create IdTokenEntity + * @param homeAccountId + * @param authenticationResult + * @param clientId + * @param authority + */ + static createIdTokenEntity( + homeAccountId: string, + authenticationResult: AuthenticationResult, + clientId: string, + environment: string + ): IdTokenEntity { + const idTokenEntity = new IdTokenEntity(); + + idTokenEntity.credentialType = "IdToken"; + idTokenEntity.homeAccountId = homeAccountId; + idTokenEntity.environment = environment; + idTokenEntity.clientId = clientId; + idTokenEntity.secret = authenticationResult.idToken; + idTokenEntity.realm = authenticationResult.tenantId; + + return idTokenEntity; + } +} diff --git a/lib/msal-common/src/unifiedCache/entities/RefreshTokenEntity.ts b/lib/msal-common/src/unifiedCache/entities/RefreshTokenEntity.ts new file mode 100644 index 0000000000..ce81f4d98b --- /dev/null +++ b/lib/msal-common/src/unifiedCache/entities/RefreshTokenEntity.ts @@ -0,0 +1,63 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { Credential } from "./Credential"; +import { Separators } from "../../utils/Constants"; +import { AuthenticationResult } from "../../response/AuthenticationResult"; + +/** + * REFRESH_TOKEN Cache + */ +export class RefreshTokenEntity extends Credential { + familyId?: string; + + /** + * Generate Account Cache Key as per the schema: -- + */ + generateRefreshTokenEntityKey(): string { + const refreshTokenKeyArray: Array = [ + this.homeAccountId, + this.environment, + this.credentialType + ]; + + // append familyId if populted, else fallback to clientId + refreshTokenKeyArray.push(this.familyId || this.clientId); + + // realm and target - empty string "" for REFRESH_TOKEN type; target (scopes) is added only if it is resource specific refresh token + refreshTokenKeyArray.push(""); + refreshTokenKeyArray.push(""); + + return refreshTokenKeyArray.join(Separators.CACHE_KEY_SEPARATOR).toLowerCase(); + } + + /** + * Create RefreshTokenEntity + * @param homeAccountId + * @param authenticationResult + * @param clientId + * @param authority + */ + static createRefreshTokenEntity( + homeAccountId: string, + authenticationResult: AuthenticationResult, + refreshToken: string, + clientId: string, + environment: string + ): RefreshTokenEntity { + const rtEntity = new RefreshTokenEntity(); + + rtEntity.clientId = clientId; + rtEntity.credentialType = "RefreshToken"; + rtEntity.environment = environment; + rtEntity.homeAccountId = homeAccountId; + rtEntity.secret = refreshToken; + + if (authenticationResult.familyId) + rtEntity.familyId = authenticationResult.familyId; + + return rtEntity; + } +} diff --git a/lib/msal-common/src/unifiedCache/serialize/Deserializer.ts b/lib/msal-common/src/unifiedCache/serialize/Deserializer.ts new file mode 100644 index 0000000000..547007d18c --- /dev/null +++ b/lib/msal-common/src/unifiedCache/serialize/Deserializer.ts @@ -0,0 +1,139 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { AccountEntity } from "../entities/AccountEntity"; +import { IdTokenEntity } from "../entities/IdTokenEntity"; +import { AccessTokenEntity } from "../entities/AccessTokenEntity"; +import { RefreshTokenEntity } from "../entities/RefreshTokenEntity"; +import { AppMetadataEntity } from "../entities/AppMetadataEntity"; +import { CacheHelper } from "../utils/CacheHelper"; +import { AccountCacheMaps, IdTokenCacheMaps, AccessTokenCacheMaps, RefreshTokenCacheMaps, AppMetadataCacheMaps } from "../serialize/JsonKeys"; +import { AccountCache, IdTokenCache, AccessTokenCache, RefreshTokenCache, AppMetadataCache, InMemoryCache, JsonCache } from "../utils/CacheTypes"; +import { StringDict } from "../../utils/MsalTypes"; +import { StringUtils } from "../../utils/StringUtils"; + +// TODO: Can we write this with Generics? +export class Deserializer { + + /** + * Parse the JSON blob in memory and deserialize the content + * @param cachedJson + */ + static deserializeJSONBlob(jsonFile: string): JsonCache { + const deserializedCache = StringUtils.isEmpty(jsonFile) + ? {} + : JSON.parse(jsonFile); + return deserializedCache; + } + + /** + * Deserializes accounts to AccountEntity objects + * @param accounts + */ + static deserializeAccounts(accounts: StringDict): AccountCache { + const accountObjects = {}; + Object.keys(accounts).map(function (key) { + const mappedAcc = CacheHelper.renameKeys( + accounts[key], + AccountCacheMaps.fromCacheMap + ); + const account: AccountEntity = new AccountEntity(); + CacheHelper.toObject(account, mappedAcc); + accountObjects[key] = account; + }); + + return accountObjects; + } + + /** + * Deserializes id tokens to IdTokenEntity objects + * @param idTokens + */ + static deserializeIdTokens(idTokens: StringDict): IdTokenCache { + const idObjects = {}; + Object.keys(idTokens).map(function (key) { + const mappedIdT = CacheHelper.renameKeys( + idTokens[key], + IdTokenCacheMaps.fromCacheMap + ); + const idToken: IdTokenEntity = new IdTokenEntity(); + CacheHelper.toObject(idToken, mappedIdT); + idObjects[key] = idToken; + }); + + return idObjects; + } + + /** + * Deserializes access tokens to AccessTokenEntity objects + * @param accessTokens + */ + static deserializeAccessTokens(accessTokens: StringDict): AccessTokenCache { + const atObjects = {}; + Object.keys(accessTokens).map(function (key) { + const mappedAT = CacheHelper.renameKeys( + accessTokens[key], + AccessTokenCacheMaps.fromCacheMap + ); + const accessToken: AccessTokenEntity = new AccessTokenEntity(); + CacheHelper.toObject(accessToken, mappedAT); + atObjects[key] = accessToken; + }); + + return atObjects; + } + + /** + * Deserializes refresh tokens to RefreshTokenEntity objects + * @param refreshTokens + */ + static deserializeRefreshTokens(refreshTokens: StringDict): RefreshTokenCache { + const rtObjects = {}; + Object.keys(refreshTokens).map(function (key) { + const mappedRT = CacheHelper.renameKeys( + refreshTokens[key], + RefreshTokenCacheMaps.fromCacheMap + ); + const refreshToken: RefreshTokenEntity = new RefreshTokenEntity(); + CacheHelper.toObject(refreshToken, mappedRT); + rtObjects[key] = refreshToken; + }); + + return rtObjects; + } + + /** + * Deserializes appMetadata to AppMetaData objects + * @param appMetadata + */ + static deserializeAppMetadata(appMetadata: StringDict): AppMetadataCache { + const appMetadataObjects = {}; + Object.keys(appMetadata).map(function (key) { + const mappedAmd = CacheHelper.renameKeys( + appMetadata[key], + AppMetadataCacheMaps.fromCacheMap + ); + const amd: AppMetadataEntity = new AppMetadataEntity(); + CacheHelper.toObject(amd, mappedAmd); + appMetadataObjects[key] = amd; + }); + + return appMetadataObjects; + } + + /** + * Deserialize an inMemory Cache + * @param jsonCache + */ + static deserializeAllCache(jsonCache: JsonCache): InMemoryCache { + return { + accounts: jsonCache.Account? this.deserializeAccounts(jsonCache.Account): {}, + idTokens: jsonCache.IdToken? this.deserializeIdTokens(jsonCache.IdToken): {}, + accessTokens: jsonCache.AccessToken? this.deserializeAccessTokens(jsonCache.AccessToken) : {}, + refreshTokens: jsonCache.RefreshToken? this.deserializeRefreshTokens(jsonCache.RefreshToken): {}, + appMetadata: jsonCache.AppMetadata? this.deserializeAppMetadata(jsonCache.AppMetadata): {} + }; + } +} diff --git a/lib/msal-common/src/unifiedCache/serialize/JsonKeys.ts b/lib/msal-common/src/unifiedCache/serialize/JsonKeys.ts new file mode 100644 index 0000000000..e2b930383a --- /dev/null +++ b/lib/msal-common/src/unifiedCache/serialize/JsonKeys.ts @@ -0,0 +1,93 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { CacheHelper } from "../utils/CacheHelper"; + +/** + * Map for AccountCache entity serialization and deserialization + */ +export class AccountCacheMaps { + static toCacheMap = { + homeAccountId: "home_account_id", + environment: "environment", + realm: "realm", + localAccountId: "local_account_id", + username: "username", + authorityType: "authority_type", + name: "name", + clientInfo: "client_info", + lastModificationTime: "last_modification_time", + lastModificationApp: "last_modification_app", + }; + + static fromCacheMap = CacheHelper.swap(AccountCacheMaps.toCacheMap); +} + +/** + * Map for IdTokenCache entity serialization and deserialization + */ +export class IdTokenCacheMaps { + static toCacheMap = { + homeAccountId: "home_account_id", + environment: "environment", + credentialType: "credential_type", + clientId: "client_id", + secret: "secret", + realm: "realm", + }; + + static fromCacheMap = CacheHelper.swap(IdTokenCacheMaps.toCacheMap); +} + +/** + * Map for AccessTokenCache entity serialization and deserialization + */ +export class AccessTokenCacheMaps { + static toCacheMap = { + homeAccountId: "home_account_id", + environment: "environment", + credentialType: "credential_type", + clientId: "client_id", + secret: "secret", + realm: "realm", + target: "target", + cachedAt: "cached_at", + expiresOn: "expires_on", + extendedExpiresOn: "extended_expires_on", + refreshOn: "refresh_on", + keyId: "key_id", + tokenType: "token_type", + }; + + static fromCacheMap = CacheHelper.swap(AccessTokenCacheMaps.toCacheMap); +} + +/** + * Map for RefreshTokenCache entity serialization and deserialization + */ +export class RefreshTokenCacheMaps { + static toCacheMap = { + homeAccountId: "home_account_id", + environment: "environment", + credentialType: "credential_type", + clientId: "client_id", + secret: "secret", + }; + + static fromCacheMap = CacheHelper.swap(RefreshTokenCacheMaps.toCacheMap); +} + +/** + * Map for AppMetadataCache entity serialization and deserialization + */ +export class AppMetadataCacheMaps { + static toCacheMap = { + clientId: "client_id", + environment: "environment", + familyId: "family_id", + }; + + static fromCacheMap = CacheHelper.swap(AppMetadataCacheMaps.toCacheMap); +} diff --git a/lib/msal-common/src/unifiedCache/serialize/Serializer.ts b/lib/msal-common/src/unifiedCache/serialize/Serializer.ts new file mode 100644 index 0000000000..a4b5215402 --- /dev/null +++ b/lib/msal-common/src/unifiedCache/serialize/Serializer.ts @@ -0,0 +1,120 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { CacheHelper } from "../utils/CacheHelper"; +import { AccountCacheMaps, AccessTokenCacheMaps, IdTokenCacheMaps, RefreshTokenCacheMaps, AppMetadataCacheMaps } from "./JsonKeys"; +import { AccountCache, AccessTokenCache, IdTokenCache, RefreshTokenCache, AppMetadataCache, JsonCache, InMemoryCache } from "../utils/CacheTypes"; +import { StringDict } from "../../utils/MsalTypes"; + +export class Serializer { + + /** + * serialize the JSON blob + * @param data + */ + static serializeJSONBlob(data: JsonCache): string { + return JSON.stringify(data); + } + + /** + * Serialize Accounts + * @param accCache + */ + static serializeAccounts(accCache: AccountCache): StringDict { + const accounts = {}; + Object.keys(accCache).map(function (key) { + const mappedAcc = CacheHelper.renameKeys( + accCache[key], + AccountCacheMaps.toCacheMap + ); + accounts[key] = mappedAcc; + }); + + return accounts; + } + + /** + * Serialize IdTokens + * @param idTCache + */ + static serializeIdTokens(idTCache: IdTokenCache): StringDict{ + const idTokens = {}; + Object.keys(idTCache).map(function (key) { + const mappedIdT = CacheHelper.renameKeys( + idTCache[key], + IdTokenCacheMaps.toCacheMap + ); + idTokens[key] = mappedIdT; + }); + + return idTokens; + } + + /** + * Serializes AccessTokens + * @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; + }); + + return accessTokens; + } + + /** + * Serialize refreshTokens + * @param rtCache + */ + static serializeRefreshTokens(rtCache: RefreshTokenCache): StringDict{ + const refreshTokens = {}; + Object.keys(rtCache).map(function (key) { + const mappedRT = CacheHelper.renameKeys( + rtCache[key], + RefreshTokenCacheMaps.toCacheMap + ); + refreshTokens[key] = mappedRT; + }); + + return refreshTokens; + } + + /** + * Serialize amdtCache + * @param amdtCache + */ + static serializeAppMetadata(amdtCache: AppMetadataCache): StringDict { + const appMetadata = {}; + Object.keys(amdtCache).map(function (key) { + const mappedAmdt = CacheHelper.renameKeys( + amdtCache[key], + AppMetadataCacheMaps.toCacheMap + ); + appMetadata[key] = mappedAmdt; + }); + + return appMetadata; + } + + /** + * Serialize the cache + * @param jsonContent + */ + static serializeAllCache(inMemCache: InMemoryCache): JsonCache { + return { + Account: this.serializeAccounts(inMemCache.accounts), + IdToken: this.serializeIdTokens(inMemCache.idTokens), + AccessToken: this.serializeAccessTokens(inMemCache.accessTokens), + RefreshToken: this.serializeRefreshTokens(inMemCache.refreshTokens), + AppMetadata: this.serializeAppMetadata(inMemCache.appMetadata), + }; + } +} diff --git a/lib/msal-common/src/unifiedCache/utils/CacheHelper.ts b/lib/msal-common/src/unifiedCache/utils/CacheHelper.ts new file mode 100644 index 0000000000..2cd6f06764 --- /dev/null +++ b/lib/msal-common/src/unifiedCache/utils/CacheHelper.ts @@ -0,0 +1,46 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export class CacheHelper { + /** + * Helper to convert serialized data to object + * @param obj + * @param json + */ + static toObject(obj: T, json: object): T { + for (const propertyName in json) { + obj[propertyName] = json[propertyName]; + } + return obj; + } + + /** + * helper function to swap keys and objects + * @param cacheMap + */ + static swap(cacheMap: object): object { + const ret = {}; + for (const key in cacheMap) { + ret[cacheMap[key]] = key; + } + return ret; + } + + /** + * helper function to map an obj to a new keyset + * @param objAT + * @param keysMap + */ + static renameKeys(objAT: Object, keysMap: Object): object { + const keyValues = Object.keys(objAT).map((key) => { + if (objAT[key]) { + const newKey = keysMap[key] || key; + return { [newKey]: objAT[key] }; + } + return null; + }); + return Object.assign({}, ...keyValues); + } +} diff --git a/lib/msal-common/src/unifiedCache/utils/CacheTypes.ts b/lib/msal-common/src/unifiedCache/utils/CacheTypes.ts new file mode 100644 index 0000000000..ad6ee315fe --- /dev/null +++ b/lib/msal-common/src/unifiedCache/utils/CacheTypes.ts @@ -0,0 +1,34 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { StringDict } from "../../utils/MsalTypes"; + +import { AccountEntity } from "../entities/AccountEntity"; +import { IdTokenEntity } from "../entities/IdTokenEntity"; +import { AccessTokenEntity } from "../entities/AccessTokenEntity"; +import { RefreshTokenEntity } from "../entities/RefreshTokenEntity"; +import { AppMetadataEntity } from "../entities/AppMetadataEntity"; + +export type AccountCache = { [key: string]: AccountEntity }; +export type IdTokenCache = { [key: string]: IdTokenEntity }; +export type AccessTokenCache = { [key: string]: AccessTokenEntity }; +export type RefreshTokenCache = { [key: string]: RefreshTokenEntity }; +export type AppMetadataCache = { [key: string]: AppMetadataEntity }; + +export type JsonCache = { + Account?: StringDict; + IdToken?: StringDict; + AccessToken?: StringDict; + RefreshToken?: StringDict; + AppMetadata?: StringDict; +}; + +export type InMemoryCache = { + accounts: AccountCache; + idTokens: IdTokenCache; + accessTokens: AccessTokenCache; + refreshTokens: RefreshTokenCache; + appMetadata: AppMetadataCache; +}; diff --git a/lib/msal-common/src/utils/Constants.ts b/lib/msal-common/src/utils/Constants.ts index 09153bee9c..27306c4515 100644 --- a/lib/msal-common/src/utils/Constants.ts +++ b/lib/msal-common/src/utils/Constants.ts @@ -232,3 +232,54 @@ export enum GrantType { DEVICE_CODE_GRANT = "device_code" } +/** + * Account types in Cache + */ +export enum CacheAccountType { + MSSTS_ACCOUNT_TYPE = "MSSTS", + ADFS_ACCOUNT_TYPE = "ADFS", + MSAV1_ACCOUNT_TYPE = "MSA", + OTHER_ACCOUNT_TYPE = "Other" +} + +/** + * Separators used in cache + */ +export enum Separators { + CACHE_KEY_SEPARATOR = "-", + CLIENT_INFO_SEPARATOR = "." +} + +/** + * Credentail Type stored in the cache + */ +export enum CredentialType { + ID_TOKEN = "IdToken", + ACCESS_TOKEN = "AccessToken", + REFRESH_TOKEN = "RefreshToken" +} + +/** + * cache Type + */ +export enum CacheEntity { + ACCOUNT = "Account", + APP_META_DATA = "AppMetaData" +} + +/** + * Combine all cache types + */ +export enum CacheTypes { + ACCESS_TOKEN, + ID_TOKEN, + REFRESH_TOKEN, + ACCOUNT, + APP_META_DATA +}; + +/** + * More Cache related constants + */ +export const APP_META_DATA = "appmetadata"; +export const ClientInfo = "client_info"; diff --git a/lib/msal-common/src/utils/MsalTypes.ts b/lib/msal-common/src/utils/MsalTypes.ts index 88215ac4a0..a918763b7a 100644 --- a/lib/msal-common/src/utils/MsalTypes.ts +++ b/lib/msal-common/src/utils/MsalTypes.ts @@ -6,4 +6,4 @@ /** * Key-Value type to support queryParams, extraQueryParams and claims */ -export type StringDict = {[key: string]: string}; +export type StringDict = { [key: string]: string }; diff --git a/lib/msal-common/test/authority/AuthorityFactory.spec.ts b/lib/msal-common/test/authority/AuthorityFactory.spec.ts index ad7ea2be8b..3483bef47e 100644 --- a/lib/msal-common/test/authority/AuthorityFactory.spec.ts +++ b/lib/msal-common/test/authority/AuthorityFactory.spec.ts @@ -10,12 +10,17 @@ import { ClientConfigurationErrorMessage } from "../../src/error/ClientConfigura import { Authority } from "../../src/authority/Authority"; describe("AuthorityFactory.ts Class Unit Tests", () => { - const networkInterface: INetworkModule = { - sendGetRequestAsync(url: string, options?: NetworkRequestOptions): T { + sendGetRequestAsync( + url: string, + options?: NetworkRequestOptions + ): T { return null; }, - sendPostRequestAsync(url: string, options?: NetworkRequestOptions): T { + sendPostRequestAsync( + url: string, + options?: NetworkRequestOptions + ): T { return null; } }; @@ -26,17 +31,34 @@ describe("AuthorityFactory.ts Class Unit Tests", () => { B2cAuthority.B2CTrustedHostList.pop(); } }); - + it("AuthorityFactory returns null if given url is null or empty", () => { expect(() => AuthorityFactory.createInstance("", networkInterface)).to.throw(ClientConfigurationErrorMessage.urlEmptyError.desc); expect(() => AuthorityFactory.createInstance(null, networkInterface)).to.throw(ClientConfigurationErrorMessage.urlEmptyError.desc); }); it("Throws error for malformed url strings", () => { - expect(() => AuthorityFactory.createInstance(`http://login.microsoftonline.com/common`, networkInterface)).to.throw(ClientConfigurationErrorMessage.authorityUriInsecure.desc); - expect(() => AuthorityFactory.createInstance(`https://login.microsoftonline.com/`, networkInterface)).to.throw(ClientConfigurationErrorMessage.urlParseError.desc); - expect(() => AuthorityFactory.createInstance("This is not a URI", networkInterface)).to.throw(ClientConfigurationErrorMessage.urlParseError.desc); - expect(() => AuthorityFactory.createInstance("", networkInterface)).to.throw(ClientConfigurationErrorMessage.urlEmptyError.desc); + expect(() => + AuthorityFactory.createInstance( + `http://login.microsoftonline.com/common`, + networkInterface + ) + ).to.throw(ClientConfigurationErrorMessage.authorityUriInsecure.desc); + expect(() => + AuthorityFactory.createInstance( + `https://login.microsoftonline.com/`, + networkInterface + ) + ).to.throw(ClientConfigurationErrorMessage.urlParseError.desc); + expect(() => + AuthorityFactory.createInstance( + "This is not a URI", + networkInterface + ) + ).to.throw(ClientConfigurationErrorMessage.urlParseError.desc); + expect(() => + AuthorityFactory.createInstance("", networkInterface) + ).to.throw(ClientConfigurationErrorMessage.urlEmptyError.desc); }); it("createInstance returns an AAD instance if knownAuthorities not provided", () => { @@ -64,7 +86,7 @@ describe("AuthorityFactory.ts Class Unit Tests", () => { it("Throws error if AuthorityType is not AAD or B2C", (done) => { //Right now only way to throw this is to send adfs authority. This will need to change when we implement ADFS const errorAuthority = "https://login.microsoftonline.com/adfs" - try{ + try{ const authorityInstance = AuthorityFactory.createInstance(errorAuthority, networkInterface); } catch(e) { diff --git a/lib/msal-common/test/authority/B2cAuthority.spec.ts b/lib/msal-common/test/authority/B2cAuthority.spec.ts index cdeeff4516..f9117c1a24 100644 --- a/lib/msal-common/test/authority/B2cAuthority.spec.ts +++ b/lib/msal-common/test/authority/B2cAuthority.spec.ts @@ -82,4 +82,4 @@ describe("B2cAuthority.ts Class Unit Tests", () => { } }); }); -}); \ No newline at end of file +}); diff --git a/lib/msal-common/test/client/AuthorizationCodeClient.spec.ts b/lib/msal-common/test/client/AuthorizationCodeClient.spec.ts index 5334412e5b..470cec58a0 100644 --- a/lib/msal-common/test/client/AuthorizationCodeClient.spec.ts +++ b/lib/msal-common/test/client/AuthorizationCodeClient.spec.ts @@ -17,13 +17,18 @@ import { } from "../utils/StringConstants"; import { ClientConfiguration } from "../../src/config/ClientConfiguration"; import { BaseClient } from "../../src/client/BaseClient"; -import { AADServerParamKeys, PromptValue, ResponseMode, SSOTypes } from "../../src/utils/Constants"; +import { AADServerParamKeys, PromptValue, ResponseMode, SSOTypes, Prompt } from "../../src/utils/Constants"; import { ClientTestUtils } from "./ClientTestUtils"; +import { B2cAuthority } from "../../src/authority/B2cAuthority"; describe("AuthorizationCodeClient unit tests", () => { afterEach(() => { + let config = null; sinon.restore(); + while (B2cAuthority.B2CTrustedHostList.length) { + B2cAuthority.B2CTrustedHostList.pop(); + } }); describe("Constructor", () => { @@ -97,7 +102,7 @@ describe("AuthorizationCodeClient unit tests", () => { codeChallenge: TEST_CONFIG.TEST_CHALLENGE, codeChallengeMethod: TEST_CONFIG.CODE_CHALLENGE_METHOD, state: TEST_CONFIG.STATE, - prompt: PromptValue.SELECT_ACCOUNT, + prompt: Prompt.SELECT_ACCOUNT, loginHint: TEST_CONFIG.LOGIN_HINT, domainHint: TEST_CONFIG.DOMAIN_HINT, claims: TEST_CONFIG.CLAIMS, @@ -123,7 +128,7 @@ describe("AuthorizationCodeClient unit tests", () => { describe("Acquire a token", () => { - it("Acquires a token successfully", async () => { + xit("Acquires a token successfully", async () => { sinon.stub(Authority.prototype, "discoverEndpoints").resolves(DEFAULT_OPENID_CONFIG_RESPONSE); sinon.stub(AuthorizationCodeClient.prototype, "executePostToTokenEndpoint").resolves(AUTHENTICATION_RESULT); @@ -140,7 +145,7 @@ describe("AuthorizationCodeClient unit tests", () => { const authenticationResult = await client.acquireToken(authCodeRequest); - expect(JSON.parse(authenticationResult)).to.deep.eq(AUTHENTICATION_RESULT.body); + expect(authenticationResult.accessToken).to.deep.eq(AUTHENTICATION_RESULT.body.access_token); expect(createTokenRequestBodySpy.calledWith(authCodeRequest)).to.be.ok; expect(createTokenRequestBodySpy.returnValues[0]).to.contain(`${AADServerParamKeys.SCOPE}=${TEST_CONFIG.DEFAULT_GRAPH_SCOPE}%20${Constants.OPENID_SCOPE}%20${Constants.PROFILE_SCOPE}%20${Constants.OFFLINE_ACCESS_SCOPE}`); diff --git a/lib/msal-common/test/client/BaseClient.spec.ts b/lib/msal-common/test/client/BaseClient.spec.ts index 1bcc92867e..38bd136188 100644 --- a/lib/msal-common/test/client/BaseClient.spec.ts +++ b/lib/msal-common/test/client/BaseClient.spec.ts @@ -26,7 +26,7 @@ class TestClient extends BaseClient { } getCacheStorage(){ - return this.cacheManager; + return this.spaCacheManager; } getNetworkClient(){ @@ -34,7 +34,7 @@ class TestClient extends BaseClient { } getCacheManger(){ - return this.cacheManager; + return this.spaCacheManager; } getAccount(){ diff --git a/lib/msal-common/test/client/ClientTestUtils.ts b/lib/msal-common/test/client/ClientTestUtils.ts index ae805956d4..7e0a644d0a 100644 --- a/lib/msal-common/test/client/ClientTestUtils.ts +++ b/lib/msal-common/test/client/ClientTestUtils.ts @@ -1,11 +1,4 @@ -import { - ClientAuthError, - ClientConfiguration, - Constants, - LogLevel, - NetworkRequestOptions, - PkceCodes -} from "../../src"; +import { ClientConfiguration, Constants, LogLevel, NetworkRequestOptions, PkceCodes, InMemoryCache, ClientAuthError} from "../../src"; import { RANDOM_TEST_GUID, TEST_CONFIG } from "../utils/StringConstants"; import { AuthorityFactory } from "../../src"; @@ -39,8 +32,21 @@ export class ClientTestUtils { authOptions: { clientId: TEST_CONFIG.MSAL_CLIENT_ID, authority: authority, + knownAuthorities: [], }, storageInterface: { + getCache(): InMemoryCache { + return { + accounts: {}, + idTokens: {}, + accessTokens: {}, + refreshTokens: {}, + appMetadata: {}, + }; + }, + setCache(): void { + // do nothing + }, setItem(key: string, value: string): void { store[key] = value; }, @@ -58,9 +64,22 @@ export class ClientTestUtils { }, clear(): void { store = {}; - } + }, + }, + networkInterface: { + sendGetRequestAsync( + url: string, + options?: NetworkRequestOptions + ): T { + return null; + }, + sendPostRequestAsync( + url: string, + options?: NetworkRequestOptions + ): T { + return null; + }, }, - networkInterface: mockHttpClient, cryptoInterface: { createNewGuid(): string { return RANDOM_TEST_GUID; @@ -74,19 +93,19 @@ export class ClientTestUtils { async generatePkceCodes(): Promise { return { challenge: TEST_CONFIG.TEST_CHALLENGE, - verifier: TEST_CONFIG.TEST_VERIFIER + verifier: TEST_CONFIG.TEST_VERIFIER, }; - } + }, }, loggerOptions: { - loggerCallback: testLoggerCallback + loggerCallback: testLoggerCallback, }, libraryInfo: { sku: Constants.SKU, version: TEST_CONFIG.TEST_VERSION, os: TEST_CONFIG.TEST_OS, cpu: TEST_CONFIG.TEST_CPU, - } + }, }; } } diff --git a/lib/msal-common/test/client/SPAClient.spec.ts b/lib/msal-common/test/client/SPAClient.spec.ts index 39beda135c..2e6851dbc8 100644 --- a/lib/msal-common/test/client/SPAClient.spec.ts +++ b/lib/msal-common/test/client/SPAClient.spec.ts @@ -31,6 +31,7 @@ import { TokenResponse } from "../../src/response/TokenResponse"; import { AuthorityFactory } from "../../src/authority/AuthorityFactory"; import { ServerError } from "../../src/error/ServerError"; import { ClientConfiguration } from "../../src/config/ClientConfiguration"; +import { InMemoryCache } from "../../src/unifiedCache/utils/CacheTypes"; describe("SPAClient.ts Class Unit Tests", () => { @@ -59,9 +60,21 @@ describe("SPAClient.ts Class Unit Tests", () => { clientId: TEST_CONFIG.MSAL_CLIENT_ID, authority: AuthorityFactory.createInstance(TEST_CONFIG.validAuthority, mockHttpClient), redirectUri: TEST_URIS.TEST_REDIR_URI, - postLogoutRedirectUri: TEST_URIS.TEST_LOGOUT_URI + postLogoutRedirectUri: TEST_URIS.TEST_LOGOUT_URI, }, storageInterface: { + getCache(): InMemoryCache { + return { + accounts: {}, + idTokens: {}, + accessTokens: {}, + refreshTokens: {}, + appMetadata: {}, + }; + }, + setCache(): void { + // do nothing + }, setItem(key: string, value: string): void { store[key] = value; }, @@ -79,9 +92,22 @@ describe("SPAClient.ts Class Unit Tests", () => { }, clear(): void { store = {}; - } + }, + }, + networkInterface: { + sendGetRequestAsync( + url: string, + options?: NetworkRequestOptions + ): T { + return null; + }, + sendPostRequestAsync( + url: string, + options?: NetworkRequestOptions + ): T { + return null; + }, }, - networkInterface: mockHttpClient, cryptoInterface: { createNewGuid(): string { return RANDOM_TEST_GUID; @@ -95,13 +121,13 @@ describe("SPAClient.ts Class Unit Tests", () => { async generatePkceCodes(): Promise { return { challenge: TEST_CONFIG.TEST_CHALLENGE, - verifier: TEST_CONFIG.TEST_VERIFIER + verifier: TEST_CONFIG.TEST_VERIFIER, }; - } + }, }, loggerOptions: { - loggerCallback: testLoggerCallback - } + loggerCallback: testLoggerCallback, + }, }; }); @@ -922,27 +948,89 @@ describe("SPAClient.ts Class Unit Tests", () => { clientId: TEST_CONFIG.MSAL_CLIENT_ID, authority: TEST_CONFIG.validAuthority, redirectUri: redirectUriFunc, - postLogoutRedirectUri: postLogoutRedirectUriFunc + postLogoutRedirectUri: postLogoutRedirectUriFunc, + }, + storageInterface: { + getCache(): InMemoryCache { + return { + accounts: {}, + idTokens: {}, + accessTokens: {}, + refreshTokens: {}, + appMetadata: {}, + }; + }, + setCache(): void { + // do nothing + }, + setItem(key: string, value: string): void { + store[key] = value; + }, + getItem(key: string): string { + return store[key]; + }, + removeItem(key: string): void { + delete store[key]; + }, + containsKey(key: string): boolean { + return !!store[key]; + }, + getKeys(): string[] { + return Object.keys(store); + }, + clear(): void { + store = {}; + }, }, - storageInterface: null, networkInterface: null, cryptoInterface: null, loggerOptions: { - loggerCallback: testLoggerCallback - } + loggerCallback: testLoggerCallback, + }, }); const Client_noRedirectUris = new SPAClient({ authOptions: { clientId: TEST_CONFIG.MSAL_CLIENT_ID, - authority: TEST_CONFIG.validAuthority + authority: TEST_CONFIG.validAuthority, + }, + storageInterface: { + getCache(): InMemoryCache { + return { + accounts: {}, + idTokens: {}, + accessTokens: {}, + refreshTokens: {}, + appMetadata: {}, + }; + }, + setCache(): void { + // do nothing + }, + setItem(key: string, value: string): void { + store[key] = value; + }, + getItem(key: string): string { + return store[key]; + }, + removeItem(key: string): void { + delete store[key]; + }, + containsKey(key: string): boolean { + return !!store[key]; + }, + getKeys(): string[] { + return Object.keys(store); + }, + clear(): void { + store = {}; + }, }, - storageInterface: null, networkInterface: null, cryptoInterface: null, loggerOptions: { - loggerCallback: testLoggerCallback - } + loggerCallback: testLoggerCallback, + }, }); it("gets configured redirect uri", () => { @@ -1012,6 +1100,18 @@ describe("SPAClient.ts Class Unit Tests", () => { }, networkInterface: null, storageInterface: { + getCache(): InMemoryCache { + return { + accounts: {}, + idTokens: {}, + accessTokens: {}, + refreshTokens: {}, + appMetadata: {} + } + }, + setCache(): void { + // do nothing + }, setItem(key: string, value: string): void { store[key] = value; }, diff --git a/lib/msal-common/test/response/ResponseHandler.spec.ts b/lib/msal-common/test/response/SPAResponseHandler.spec.ts similarity index 89% rename from lib/msal-common/test/response/ResponseHandler.spec.ts rename to lib/msal-common/test/response/SPAResponseHandler.spec.ts index d75a907975..f408124e1a 100644 --- a/lib/msal-common/test/response/ResponseHandler.spec.ts +++ b/lib/msal-common/test/response/SPAResponseHandler.spec.ts @@ -1,6 +1,6 @@ import { expect } from "chai"; import sinon from "sinon"; -import { ResponseHandler } from "../../src/response/ResponseHandler"; +import { SPAResponseHandler } from "../../src/response/SPAResponseHandler"; import { TEST_CONFIG, RANDOM_TEST_GUID, TEST_TOKENS, TEST_URIS, TEST_DATA_CLIENT_INFO, TEST_TOKEN_LIFETIMES } from "../utils/StringConstants"; import { CacheHelpers } from "../../src/cache/CacheHelpers"; import { ICacheStorage } from "../../src/cache/ICacheStorage"; @@ -22,7 +22,7 @@ import { InteractionRequiredAuthErrorMessage, InteractionRequiredAuthError, Inte import { AccessTokenKey } from "../../src/cache/AccessTokenKey"; import { AccessTokenValue } from "../../src/cache/AccessTokenValue"; -describe("ResponseHandler.ts Class Unit Tests", () => { +describe("SPAResponseHandler.ts Class Unit Tests", () => { let store = {}; let cacheStorage: ICacheStorage; @@ -108,9 +108,9 @@ describe("ResponseHandler.ts Class Unit Tests", () => { describe("Constructor", () => { - it("Correctly creates a ResponseHandler object", () => { - const responseHandler = new ResponseHandler(TEST_CONFIG.MSAL_CLIENT_ID, cacheStorage, cacheHelpers, cryptoInterface, logger); - expect(responseHandler instanceof ResponseHandler).to.be.true; + it("Correctly creates a SPAResponseHandler object", () => { + const spaResponseHandler = new SPAResponseHandler(TEST_CONFIG.MSAL_CLIENT_ID, cacheStorage, cacheHelpers, cryptoInterface, logger); + expect(spaResponseHandler instanceof SPAResponseHandler).to.be.true; }); }); @@ -139,11 +139,11 @@ describe("ResponseHandler.ts Class Unit Tests", () => { idTokenClaims: idToken.claims, expiresOn: new Date(Number(idToken.claims.exp) * 1000) }; - expect(ResponseHandler.setResponseIdToken(tokenResponse, idToken)).to.be.deep.eq(expectedTokenResponse); + expect(SPAResponseHandler.setResponseIdToken(tokenResponse, idToken)).to.be.deep.eq(expectedTokenResponse); }); it("returns null if original response is null or empty", () => { - expect(ResponseHandler.setResponseIdToken(null, null)).to.be.null; + expect(SPAResponseHandler.setResponseIdToken(null, null)).to.be.null; }); it("returns originalResponse if no idTokenObj given", () => { @@ -160,15 +160,15 @@ describe("ResponseHandler.ts Class Unit Tests", () => { account: testAccount, userRequestState: TEST_CONFIG.STATE }; - expect(ResponseHandler.setResponseIdToken(tokenResponse, null)).to.be.deep.eq(tokenResponse); + expect(SPAResponseHandler.setResponseIdToken(tokenResponse, null)).to.be.deep.eq(tokenResponse); }); }); describe("handleServerCodeResponse()", () => { - let responseHandler: ResponseHandler; + let spaResponseHandler: SPAResponseHandler; beforeEach(() => { - responseHandler = new ResponseHandler(TEST_CONFIG.MSAL_CLIENT_ID, cacheStorage, cacheHelpers, cryptoInterface, logger); + spaResponseHandler = new SPAResponseHandler(TEST_CONFIG.MSAL_CLIENT_ID, cacheStorage, cacheHelpers, cryptoInterface, logger); }); it("throws state mismatch error if cached state does not match hash state", () => { @@ -179,11 +179,11 @@ describe("ResponseHandler.ts Class Unit Tests", () => { }; cacheStorage.setItem(TemporaryCacheKeys.REQUEST_STATE, RANDOM_TEST_GUID); - expect(() => responseHandler.handleServerCodeResponse(testServerParams)).to.throw(ClientAuthErrorMessage.stateMismatchError.desc); + expect(() => spaResponseHandler.handleServerCodeResponse(testServerParams)).to.throw(ClientAuthErrorMessage.stateMismatchError.desc); expect(store).to.be.empty; cacheStorage.setItem(TemporaryCacheKeys.REQUEST_STATE, RANDOM_TEST_GUID); - expect(() => responseHandler.handleServerCodeResponse(testServerParams)).to.throw(ClientAuthError); + expect(() => spaResponseHandler.handleServerCodeResponse(testServerParams)).to.throw(ClientAuthError); expect(store).to.be.empty; }); @@ -197,11 +197,11 @@ describe("ResponseHandler.ts Class Unit Tests", () => { }; cacheStorage.setItem(TemporaryCacheKeys.REQUEST_STATE, RANDOM_TEST_GUID); - expect(() => responseHandler.handleServerCodeResponse(testServerParams)).to.throw(TEST_ERROR_MSG); + expect(() => spaResponseHandler.handleServerCodeResponse(testServerParams)).to.throw(TEST_ERROR_MSG); expect(store).to.be.empty; cacheStorage.setItem(TemporaryCacheKeys.REQUEST_STATE, RANDOM_TEST_GUID); - expect(() => responseHandler.handleServerCodeResponse(testServerParams)).to.throw(ServerError); + expect(() => spaResponseHandler.handleServerCodeResponse(testServerParams)).to.throw(ServerError); expect(store).to.be.empty; }); @@ -215,11 +215,11 @@ describe("ResponseHandler.ts Class Unit Tests", () => { }; cacheStorage.setItem(TemporaryCacheKeys.REQUEST_STATE, RANDOM_TEST_GUID); - expect(() => responseHandler.handleServerCodeResponse(testServerParams)).to.throw(TEST_ERROR_MSG); + expect(() => spaResponseHandler.handleServerCodeResponse(testServerParams)).to.throw(TEST_ERROR_MSG); expect(store).to.be.empty; cacheStorage.setItem(TemporaryCacheKeys.REQUEST_STATE, RANDOM_TEST_GUID); - expect(() => responseHandler.handleServerCodeResponse(testServerParams)).to.throw(InteractionRequiredAuthError); + expect(() => spaResponseHandler.handleServerCodeResponse(testServerParams)).to.throw(InteractionRequiredAuthError); expect(store).to.be.empty; }); @@ -233,13 +233,13 @@ describe("ResponseHandler.ts Class Unit Tests", () => { cryptoInterface.base64Decode = (input: string): string => { throw "decoding error"; }; - responseHandler = new ResponseHandler(TEST_CONFIG.MSAL_CLIENT_ID, cacheStorage, cacheHelpers, cryptoInterface, logger); + spaResponseHandler = new SPAResponseHandler(TEST_CONFIG.MSAL_CLIENT_ID, cacheStorage, cacheHelpers, cryptoInterface, logger); cacheStorage.setItem(TemporaryCacheKeys.REQUEST_STATE, RANDOM_TEST_GUID); - expect(() => responseHandler.handleServerCodeResponse(testServerParams)).to.throw(ClientAuthErrorMessage.clientInfoDecodingError.desc); + expect(() => spaResponseHandler.handleServerCodeResponse(testServerParams)).to.throw(ClientAuthErrorMessage.clientInfoDecodingError.desc); expect(store).to.be.empty; cacheStorage.setItem(TemporaryCacheKeys.REQUEST_STATE, RANDOM_TEST_GUID); - expect(() => responseHandler.handleServerCodeResponse(testServerParams)).to.throw(ClientAuthError); + expect(() => spaResponseHandler.handleServerCodeResponse(testServerParams)).to.throw(ClientAuthError); expect(store).to.be.empty; }); @@ -258,9 +258,9 @@ describe("ResponseHandler.ts Class Unit Tests", () => { return input; } }; - responseHandler = new ResponseHandler(TEST_CONFIG.MSAL_CLIENT_ID, cacheStorage, cacheHelpers, cryptoInterface, logger); + spaResponseHandler = new SPAResponseHandler(TEST_CONFIG.MSAL_CLIENT_ID, cacheStorage, cacheHelpers, cryptoInterface, logger); cacheStorage.setItem(TemporaryCacheKeys.REQUEST_STATE, RANDOM_TEST_GUID); - const codeResponse: CodeResponse = responseHandler.handleServerCodeResponse(testServerParams); + const codeResponse: CodeResponse = spaResponseHandler.handleServerCodeResponse(testServerParams); expect(codeResponse).to.be.not.null; expect(codeResponse.code).to.be.eq(testServerParams.code); expect(codeResponse.userRequestState).to.be.eq(testServerParams.state); @@ -279,9 +279,9 @@ describe("ResponseHandler.ts Class Unit Tests", () => { correlation_id: RANDOM_TEST_GUID }; - const responseHandler = new ResponseHandler(TEST_CONFIG.MSAL_CLIENT_ID, cacheStorage, cacheHelpers, cryptoInterface, logger); - expect(() => responseHandler.validateServerAuthorizationTokenResponse(testServerParams)).to.throw(testServerParams.error_description); - expect(() => responseHandler.validateServerAuthorizationTokenResponse(testServerParams)).to.throw(ServerError); + const spaResponseHandler = new SPAResponseHandler(TEST_CONFIG.MSAL_CLIENT_ID, cacheStorage, cacheHelpers, cryptoInterface, logger); + expect(() => spaResponseHandler.validateServerAuthorizationTokenResponse(testServerParams)).to.throw(testServerParams.error_description); + expect(() => spaResponseHandler.validateServerAuthorizationTokenResponse(testServerParams)).to.throw(ServerError); }); it("throws InteractionRequiredAuthError if hash contains error parameters", () => { @@ -292,7 +292,7 @@ describe("ResponseHandler.ts Class Unit Tests", () => { error_description: TEST_ERROR_MSG }; - const responseHandler = new ResponseHandler(TEST_CONFIG.MSAL_CLIENT_ID, cacheStorage, cacheHelpers, cryptoInterface, logger); + const responseHandler = new SPAResponseHandler(TEST_CONFIG.MSAL_CLIENT_ID, cacheStorage, cacheHelpers, cryptoInterface, logger); expect(() => responseHandler.validateServerAuthorizationTokenResponse(testServerParams)).to.throw(testServerParams.error_description); expect(() => responseHandler.validateServerAuthorizationTokenResponse(testServerParams)).to.throw(ServerError); expect(() => responseHandler.validateServerAuthorizationTokenResponse(testServerParams)).to.throw(InteractionRequiredAuthError); @@ -307,7 +307,7 @@ describe("ResponseHandler.ts Class Unit Tests", () => { suberror: InteractionRequiredAuthSubErrorMessage[0] }; - const responseHandler = new ResponseHandler(TEST_CONFIG.MSAL_CLIENT_ID, cacheStorage, cacheHelpers, cryptoInterface, logger); + const responseHandler = new SPAResponseHandler(TEST_CONFIG.MSAL_CLIENT_ID, cacheStorage, cacheHelpers, cryptoInterface, logger); expect(() => responseHandler.validateServerAuthorizationTokenResponse(testServerParams)).to.throw(testServerParams.error_description); expect(() => responseHandler.validateServerAuthorizationTokenResponse(testServerParams)).to.throw(ServerError); expect(() => responseHandler.validateServerAuthorizationTokenResponse(testServerParams)).to.throw(InteractionRequiredAuthError); @@ -316,14 +316,14 @@ describe("ResponseHandler.ts Class Unit Tests", () => { describe("createTokenResponse()", () => { - let responseHandler: ResponseHandler; + let responseHandler: SPAResponseHandler; let testServerParams: ServerAuthorizationTokenResponse; let expectedTokenResponse: TokenResponse; let atKey: AccessTokenKey; let atValue: AccessTokenValue; - + beforeEach(() => { - responseHandler = new ResponseHandler(TEST_CONFIG.MSAL_CLIENT_ID, cacheStorage, cacheHelpers, cryptoInterface, logger); + responseHandler = new SPAResponseHandler(TEST_CONFIG.MSAL_CLIENT_ID, cacheStorage, cacheHelpers, cryptoInterface, logger); testServerParams = { token_type: TEST_CONFIG.TOKEN_TYPE_BEARER, @@ -338,7 +338,7 @@ describe("ResponseHandler.ts Class Unit Tests", () => { expectedTokenResponse = { uniqueId: idToken.claims.oid, tenantId: idToken.claims.tid, - scopes: TEST_CONFIG.DEFAULT_SCOPES, + scopes: TEST_CONFIG.DEFAULT_SCOPES, tokenType: TEST_CONFIG.TOKEN_TYPE_BEARER, idToken: idToken.rawIdToken, idTokenClaims: idToken.claims, @@ -354,7 +354,7 @@ describe("ResponseHandler.ts Class Unit Tests", () => { clientId: TEST_CONFIG.MSAL_CLIENT_ID, scopes: TEST_CONFIG.DEFAULT_SCOPES.join(" "), resource: "", - homeAccountIdentifier: TEST_DATA_CLIENT_INFO.TEST_HOME_ACCOUNT_ID + homeAccountIdentifier: TEST_DATA_CLIENT_INFO.TEST_HOME_ACCOUNT_ID }; atValue = { tokenType: TEST_CONFIG.TOKEN_TYPE_BEARER, @@ -366,7 +366,7 @@ describe("ResponseHandler.ts Class Unit Tests", () => { }; }); - it("throws error if idToken nonce is null or empty", () => { + it("throws error if idToken nonce is null or empty", () => { sinon.restore(); const idTokenClaims: IdTokenClaims = { "ver": "2.0", @@ -401,7 +401,6 @@ describe("ResponseHandler.ts Class Unit Tests", () => { return input; } }; - const testAccount2: Account = { ...testAccount, accountIdentifier: RANDOM_TEST_GUID, @@ -458,9 +457,9 @@ describe("ResponseHandler.ts Class Unit Tests", () => { cacheStorage.setItem(JSON.stringify(atKey), JSON.stringify(atValue)); const expectedScopes = [...TEST_CONFIG.DEFAULT_SCOPES, "user.read"]; expectedTokenResponse.scopes= expectedScopes; - + testServerParams.scope = "openid profile offline_access user.read"; - + cacheStorage.setItem(cacheHelpers.generateNonceKey(RANDOM_TEST_GUID), idToken.claims.nonce); cacheStorage.setItem(PersistentCacheKeys.CLIENT_INFO, TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO); cacheStorage.setItem(cacheHelpers.generateAcquireTokenAccountKey(TEST_DATA_CLIENT_INFO.TEST_HOME_ACCOUNT_ID), JSON.stringify(testAccount)); @@ -495,7 +494,7 @@ describe("ResponseHandler.ts Class Unit Tests", () => { const expectedScopes = ["offline_access", "testscope"]; expectedTokenResponse.scopes = expectedScopes; - + const testScopes = "offline_access testscope"; testServerParams.scope = testScopes; diff --git a/lib/msal-common/test/unifiedCache/UnifiedCacheManager.spec.ts b/lib/msal-common/test/unifiedCache/UnifiedCacheManager.spec.ts new file mode 100644 index 0000000000..f65d723117 --- /dev/null +++ b/lib/msal-common/test/unifiedCache/UnifiedCacheManager.spec.ts @@ -0,0 +1,78 @@ +import { expect } from "chai"; +import { UnifiedCacheManager } from "../../src/unifiedCache/UnifiedCacheManager"; +import { mockCache } from "./entities/cacheConstants"; +import { InMemoryCache, JsonCache } from "../../src/unifiedCache/utils/CacheTypes"; +import { ICacheStorage } from "../../src/cache/ICacheStorage"; +import { Deserializer } from "../../src/unifiedCache/serialize/Deserializer"; + +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)); + + beforeEach(() => { + storageInterface = { + getCache(): InMemoryCache { + return inMemCache; + }, + setCache(): void { + // do nothing + }, + setItem(key: string, value: string): void { + store[key] = value; + }, + getItem(key: string): string { + return store[key]; + }, + removeItem(key: string): void { + delete store[key]; + }, + containsKey(key: string): boolean { + return !!store[key]; + }, + getKeys(): string[] { + return Object.keys(store); + }, + clear(): void { + store = {}; + }, + } + }); + + it("initCache", () => { + + let unifiedCacheManager = new UnifiedCacheManager(storageInterface); + + // create mock AccessToken + const atOne = mockCache.createMockATOne(); + const atOneKey = atOne.generateAccessTokenEntityKey(); + const atTwo = mockCache.createMockATTwo(); + const atTwoKey = atTwo.generateAccessTokenEntityKey(); + + 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("getAccount", () => { + + let unifiedCacheManager = new UnifiedCacheManager(storageInterface); + + // create mock Account + const acc = mockCache.createMockAcc(); + const homeAccountId = "uid.utid"; + const environment = "login.microsoftonline.com"; + const realm = "microsoft"; + + const genAcc = unifiedCacheManager.getAccount(homeAccountId, environment, realm); + expect(acc).to.eql(genAcc); + + const randomAcc = unifiedCacheManager.getAccount("", "", ""); + expect(randomAcc).to.be.null; + }); + +}); diff --git a/lib/msal-common/test/unifiedCache/entities/AccessTokenEntity.spec.ts b/lib/msal-common/test/unifiedCache/entities/AccessTokenEntity.spec.ts new file mode 100644 index 0000000000..14bb19b43c --- /dev/null +++ b/lib/msal-common/test/unifiedCache/entities/AccessTokenEntity.spec.ts @@ -0,0 +1,18 @@ +import { expect } from "chai"; +import { AccessTokenEntity } from "../../../src/unifiedCache/entities/AccessTokenEntity"; +import { mockCache } from "./cacheConstants"; + +describe("AccessTokenEntity.ts Unit Tests", () => { + + it("Verify an AccessTokenEntity entity", () => { + const at = new AccessTokenEntity(); + expect(at instanceof AccessTokenEntity); + }); + + it("Create an AccessTokenCacheEntity entity", () => { + let at = mockCache.createMockATOne(); + expect(at.generateAccessTokenEntityKey()).to.eql( + "uid.utid-login.microsoftonline.com-accesstoken-mock_client_id-microsoft-scope1 scope2 scope3" + ); + }); +}); diff --git a/lib/msal-common/test/unifiedCache/entities/AccountEntity.spec.ts b/lib/msal-common/test/unifiedCache/entities/AccountEntity.spec.ts new file mode 100644 index 0000000000..87a5c97a09 --- /dev/null +++ b/lib/msal-common/test/unifiedCache/entities/AccountEntity.spec.ts @@ -0,0 +1,89 @@ +import { expect } from "chai"; +import { AccountEntity } from "../../../src/unifiedCache/entities/AccountEntity"; +import { mockAccountEntity } from "./cacheConstants"; +import { IdToken } from "../../../src/account/IdToken"; +import { AuthorityFactory } from "../../../src/authority/AuthorityFactory"; +import { Constants } from "../../../src/utils/Constants"; +import { NetworkRequestOptions, INetworkModule } from "../../../src/network/INetworkModule"; +import { ICrypto, PkceCodes } from "../../../src/crypto/ICrypto"; +import { RANDOM_TEST_GUID, TEST_DATA_CLIENT_INFO, TEST_CONFIG, TEST_TOKENS } from "../../utils/StringConstants"; + +describe("AccountEntity.ts Unit Tests", () => { + + it("Verify an AccountEntity", () => { + const ac = new AccountEntity(); + expect(ac instanceof AccountEntity); + }); + + it("generate an AccountEntityKey", () => { + let ac = new AccountEntity(); + Object.assign(ac, mockAccountEntity); + expect(ac.generateAccountEntityKey()).to.eql( + "uid.utid-login.microsoftonline.com-microsoft" + ); + }); + + xit("create an Account", () => { + let cryptoInterface: ICrypto = { + createNewGuid(): string { + return RANDOM_TEST_GUID; + }, + base64Decode(input: string): string { + switch (input) { + case TEST_DATA_CLIENT_INFO.TEST_CACHE_RAW_CLIENT_INFO: + return TEST_DATA_CLIENT_INFO.TEST_CACHE_DECODED_CLIENT_INFO; + default: + return input; + } + }, + base64Encode(input: string): string { + switch (input) { + case "uid": + return "dWlk"; + case "utid": + return "dXRpZA=="; + default: + return input; + } + }, + async generatePkceCodes(): Promise { + return { + challenge: TEST_CONFIG.TEST_CHALLENGE, + verifier: TEST_CONFIG.TEST_VERIFIER, + }; + }, + }; + + const networkInterface: INetworkModule = { + sendGetRequestAsync( + url: string, + options?: NetworkRequestOptions + ): T { + return null; + }, + sendPostRequestAsync( + url: string, + options?: NetworkRequestOptions + ): T { + return null; + } + }; + const authority = AuthorityFactory.createInstance( + Constants.DEFAULT_AUTHORITY, + networkInterface + ); + + const acc = AccountEntity.createAccount( + "uid.utid", + authority, + idToken, + null, + cryptoInterface + ); + + expect(acc.generateAccountEntityKey()).to.eql( + "uid.utid-login.microsoftonline.com-microsoft" + ); + }); + +}); diff --git a/lib/msal-common/test/unifiedCache/entities/AppMetadataEntity.spec.ts b/lib/msal-common/test/unifiedCache/entities/AppMetadataEntity.spec.ts new file mode 100644 index 0000000000..127127364d --- /dev/null +++ b/lib/msal-common/test/unifiedCache/entities/AppMetadataEntity.spec.ts @@ -0,0 +1,18 @@ +import { expect } from "chai"; +import { AppMetadataEntity } from "../../../src/unifiedCache/entities/AppMetadataEntity"; +import { mockAppMetaDataEntity } from "./cacheConstants"; + +describe("AppMetadataEntity.ts Unit Tests", () => { + it("Verify an AppMetadataEntity", () => { + const appM = new AppMetadataEntity(); + expect(appM instanceof AppMetadataEntity); + }); + + it("Create an AppMetadataEntity", () => { + let appM = new AppMetadataEntity(); + Object.assign(appM, mockAppMetaDataEntity); + expect(appM.generateAppMetaDataEntityKey()).to.eql( + "appmetadata-login.microsoftonline.com-mock_client_id" + ); + }); +}); diff --git a/lib/msal-common/test/unifiedCache/entities/IdTokenEntity.spec.ts b/lib/msal-common/test/unifiedCache/entities/IdTokenEntity.spec.ts new file mode 100644 index 0000000000..922f0ebc9c --- /dev/null +++ b/lib/msal-common/test/unifiedCache/entities/IdTokenEntity.spec.ts @@ -0,0 +1,18 @@ +import { expect } from "chai"; +import { IdTokenEntity } from "../../../src/unifiedCache/entities/IdTokenEntity"; +import { mockIdTokenEntity } from "./cacheConstants"; + +describe("IdTokenEntity.ts Unit Tests", () => { + it("Verify an IdTokenEntity", () => { + const idT = new IdTokenEntity(); + expect(idT instanceof IdTokenEntity); + }); + + it("Create an IdTokenEntity", () => { + let idT = new IdTokenEntity(); + Object.assign(idT, mockIdTokenEntity); + expect(idT.generateIdTokenEntityKey()).to.eql( + "uid.utid-login.microsoftonline.com-idtoken-mock_client_id-microsoft-" + ); + }); +}); diff --git a/lib/msal-common/test/unifiedCache/entities/RefreshTokenEntity.spec.ts b/lib/msal-common/test/unifiedCache/entities/RefreshTokenEntity.spec.ts new file mode 100644 index 0000000000..f8a1d99a32 --- /dev/null +++ b/lib/msal-common/test/unifiedCache/entities/RefreshTokenEntity.spec.ts @@ -0,0 +1,26 @@ +import { expect } from "chai"; +import { RefreshTokenEntity } from "../../../src/unifiedCache/entities/RefreshTokenEntity"; +import { mockRefreshTokenEntity, mockRefreshTokenEntityWithFamilyId } from "./cacheConstants"; + +describe("RefreshTokenEntity.ts Unit Tests", () => { + it("Verify a RefreshTokenEntity", () => { + const rt = new RefreshTokenEntity(); + expect(rt instanceof RefreshTokenEntity); + }); + + it("Create a RefreshTokenEntity", () => { + let rt = new RefreshTokenEntity(); + Object.assign(rt, mockRefreshTokenEntity); + expect(rt.generateRefreshTokenEntityKey()).to.eql( + "uid.utid-login.microsoftonline.com-refreshtoken-mock_client_id--" + ); + }); + + it("Create a RefreshTokenEntity with familyId", () => { + let rt = new RefreshTokenEntity(); + Object.assign(rt, mockRefreshTokenEntityWithFamilyId); + expect(rt.generateRefreshTokenEntityKey()).to.eql( + "uid.utid-login.microsoftonline.com-refreshtoken-1--" + ); + }); +}); diff --git a/lib/msal-common/test/unifiedCache/entities/cacheConstants.ts b/lib/msal-common/test/unifiedCache/entities/cacheConstants.ts new file mode 100644 index 0000000000..2a2348f314 --- /dev/null +++ b/lib/msal-common/test/unifiedCache/entities/cacheConstants.ts @@ -0,0 +1,145 @@ +import { AccessTokenEntity } from "../../../src/unifiedCache/entities/AccessTokenEntity"; +import { IdTokenEntity } from "../../../src/unifiedCache/entities/IdTokenEntity"; +import { RefreshTokenEntity } from "../../../src/unifiedCache/entities/RefreshTokenEntity"; +import { AccountEntity } from "../../../src/unifiedCache/entities/AccountEntity"; +import { AppMetadataEntity } from "../../../src/unifiedCache/entities/AppMetadataEntity"; + +// mock tokens +export const mockAccessTokenEntity_1 = { + homeAccountId: "uid.utid", + environment: "login.microsoftonline.com", + credentialType: "AccessToken", + clientId: "mock_client_id", + secret: "an access token", + realm: "microsoft", + target: "scope1 scope2 scope3", + cachedAt: "1000", + expiresOn: "4600", + extendedExpiresOn: "4600", +}; + +export const mockAccessTokenEntity_2 = { + homeAccountId: "uid.utid", + environment: "login.microsoftonline.com", + credentialType: "AccessToken", + clientId: "mock_client_id", + secret: "an access token", + realm: "microsoft", + target: "scope4 scope5", + cachedAt: "1000", + expiresOn: "4600", + extendedExpiresOn: "4600" +}; + +export const mockIdTokenEntity = { + homeAccountId: "uid.utid", + environment: "login.microsoftonline.com", + credentialType: "IdToken", + clientId: "mock_client_id", + secret: "header.eyJvaWQiOiAib2JqZWN0MTIzNCIsICJwcmVmZXJyZWRfdXNlcm5hbWUiOiAiSm9obiBEb2UiLCAic3ViIjogInN1YiJ9.signature", + realm: "microsoft" +}; + +export const mockRefreshTokenEntity = { + homeAccountId: "uid.utid", + environment: "login.microsoftonline.com", + credentialType: "RefreshToken", + clientId: "mock_client_id", + secret: "a refresh token" +}; + +export const mockRefreshTokenEntityWithFamilyId = { + homeAccountId: "uid.utid", + environment: "login.microsoftonline.com", + credentialType: "RefreshToken", + clientId: "mock_client_id", + secret: "a refresh token", + familyId: "1" +}; + + +export const mockAccountEntity = { + homeAccountId: "uid.utid", + environment: "login.microsoftonline.com", + realm: "microsoft", + localAccountId: "object1234", + username: "John Doe", + authorityType: "MSSTS", + clientInfo: "eyJ1aWQiOiJ1aWQiLCAidXRpZCI6InV0aWQifQ==", +}; + +export const mockAppMetaDataEntity = { + clientId: "mock_client_id", + environment: "login.microsoftonline.com", + familyId: "1" +} + +// generate mockCache +export class mockCache { + + static createMockATOne(): AccessTokenEntity { + const at = new AccessTokenEntity(); + Object.assign(at, mockAccessTokenEntity_1); + + return at; + } + + static createMockATTwo(): AccessTokenEntity { + const at = new AccessTokenEntity(); + Object.assign(at, mockAccessTokenEntity_2); + + return at; + } + + static createMockIdT(): IdTokenEntity { + let idt = new IdTokenEntity(); + Object.assign(idt, mockIdTokenEntity); + + return idt; + } + + static createMockRT(): RefreshTokenEntity { + let rt = new RefreshTokenEntity(); + Object.assign(rt, mockRefreshTokenEntity); + + return rt; + } + + static createMockRTWithFamilyId(): RefreshTokenEntity { + let rt = new RefreshTokenEntity(); + Object.assign(rt, mockRefreshTokenEntityWithFamilyId); + + return rt; + } + + static createMockAcc(): AccountEntity { + let acc = new AccountEntity(); + Object.assign(acc, mockAccountEntity); + + return acc; + } + + static createMockAmdt(): AppMetadataEntity { + let amdt = new AppMetadataEntity(); + Object.assign(amdt, mockAppMetaDataEntity); + + return amdt; + } +} + +export const MockCache = { + atOne: mockCache.createMockATOne(), + atOneKey: mockCache.createMockATOne().generateAccessTokenEntityKey(), + atTwo: mockCache.createMockATTwo(), + atTwoKey: mockCache.createMockATTwo().generateAccessTokenEntityKey(), + idT: mockCache.createMockIdT(), + idTKey: mockCache.createMockIdT().generateIdTokenEntityKey(), + rt: mockCache.createMockRT(), + rtKey: mockCache.createMockRT().generateRefreshTokenEntityKey(), + rtF: mockCache.createMockRTWithFamilyId(), + rtFKey: mockCache.createMockRTWithFamilyId().generateRefreshTokenEntityKey(), + acc: mockCache.createMockAcc(), + accKey: mockCache.createMockAcc().generateAccountEntityKey(), + amdt: mockCache.createMockAmdt(), + amdtKey: mockCache.createMockAmdt().generateAppMetaDataEntityKey() +} diff --git a/lib/msal-common/test/unifiedCache/serialize/Account.json b/lib/msal-common/test/unifiedCache/serialize/Account.json new file mode 100644 index 0000000000..be3b4b4e81 --- /dev/null +++ b/lib/msal-common/test/unifiedCache/serialize/Account.json @@ -0,0 +1,13 @@ +{ + "Account": { + "uid.utid-login.microsoftonline.com-microsoft": { + "username": "John Doe", + "local_account_id": "object1234", + "realm": "microsoft", + "environment": "login.microsoftonline.com", + "home_account_id": "uid.utid", + "authority_type": "MSSTS", + "client_info": "eyJ1aWQiOiJ1aWQiLCAidXRpZCI6InV0aWQifQ==" + } + } +} diff --git a/lib/msal-common/test/unifiedCache/serialize/Deserializer.spec.ts b/lib/msal-common/test/unifiedCache/serialize/Deserializer.spec.ts new file mode 100644 index 0000000000..38f7780416 --- /dev/null +++ b/lib/msal-common/test/unifiedCache/serialize/Deserializer.spec.ts @@ -0,0 +1,73 @@ +import { expect } from "chai"; +import { MockCache } from "../entities/cacheConstants"; +import { Deserializer } from "../../../src/unifiedCache/serialize/Deserializer"; +import { InMemoryCache, JsonCache } from "../../../src/unifiedCache/utils/CacheTypes"; + +const cacheJson = require("./cache.json"); + +describe("Deserializer test cases", () => { + + const cache = JSON.stringify(cacheJson); + const jsonCache: JsonCache = Deserializer.deserializeJSONBlob(cache); + + it("deserializeJSONBlob", () => { + const mockAccount = { + "uid.utid-login.microsoftonline.com-microsoft": { + username: "John Doe", + local_account_id: "object1234", + realm: "microsoft", + environment: "login.microsoftonline.com", + home_account_id: "uid.utid", + authority_type: "MSSTS", + client_info: "eyJ1aWQiOiJ1aWQiLCAidXRpZCI6InV0aWQifQ==", + }, + }; + const acc = Deserializer.deserializeJSONBlob(cache); + expect(acc.Account).to.eql(mockAccount); + }); + + it("deSerializeAccounts", () => { + // serialize the mock Account and Test equivalency with the cache.json provided + const accCache = Deserializer.deserializeAccounts(jsonCache.Account); + expect(accCache[MockCache.accKey]).to.eql(MockCache.acc); + }); + + it("deSerializeIdTokens", () => { + // serialize the mock IdToken and Test equivalency with the cache.json provided + const idTCache = Deserializer.deserializeIdTokens(jsonCache.IdToken); + expect(idTCache[MockCache.idTKey]).to.eql(MockCache.idT); + }); + + + it("deSerializeAccessTokens", () => { + // serialize the mock AccessToken and Test equivalency with the cache.json provided + const atCache = Deserializer.deserializeAccessTokens(jsonCache.AccessToken); + expect(atCache[MockCache.atOneKey]).to.eql(MockCache.atOne); + }); + + it("deSerializeRefreshTokens", () => { + // serialize the mock RefreshToken and Test equivalency with the cache.json provided + const rtCache = Deserializer.deserializeRefreshTokens(jsonCache.RefreshToken); + expect(rtCache[MockCache.rtKey]).to.eql(MockCache.rt); + }); + + it("deserializeAppMetadata", () => { + // serialize the mock AppMetadata and Test equivalency with the cache.json provided + const amdtCache = Deserializer.deserializeAppMetadata(jsonCache.AppMetadata); + expect(amdtCache[MockCache.amdtKey]).to.eql(MockCache.amdt); + }); + + it("deserializeAll", () => { + + // deserialize the cache from memory and Test equivalency with the generated mock cache + const inMemoryCache: InMemoryCache = Deserializer.deserializeAllCache(jsonCache); + + expect(inMemoryCache.accounts[MockCache.accKey]).to.eql(MockCache.acc); + expect(inMemoryCache.idTokens[MockCache.idTKey]).to.eql(MockCache.idT); + expect(inMemoryCache.accessTokens[MockCache.atOneKey]).to.eql(MockCache.atOne); + expect(inMemoryCache.accessTokens[MockCache.atTwoKey]).to.eql(MockCache.atTwo); + expect(inMemoryCache.refreshTokens[MockCache.rtKey]).to.eql(MockCache.rt); + expect(inMemoryCache.refreshTokens[MockCache.rtFKey]).to.eql(MockCache.rtF); + expect(inMemoryCache.appMetadata[MockCache.amdtKey]).to.eql(MockCache.amdt); + }); +}); diff --git a/lib/msal-common/test/unifiedCache/serialize/JsonKeys.spec.ts b/lib/msal-common/test/unifiedCache/serialize/JsonKeys.spec.ts new file mode 100644 index 0000000000..8a057c0011 --- /dev/null +++ b/lib/msal-common/test/unifiedCache/serialize/JsonKeys.spec.ts @@ -0,0 +1,37 @@ +import { expect } from "chai"; +import { + AccessTokenCacheMaps, + IdTokenCacheMaps, + RefreshTokenCacheMaps, + AccountCacheMaps, + AppMetadataCacheMaps +} from "../../../src/unifiedCache/serialize/JsonKeys"; + + +describe("JsonKets.ts Unit Tests", () => { + + it("Verify an AccessTokenCacheMaps", () => { + expect(AccessTokenCacheMaps.toCacheMap["homeAccountId"]).to.equal("home_account_id"); + expect(AccessTokenCacheMaps.fromCacheMap["home_account_id"]).to.equal("homeAccountId"); + }); + + it("Verify an IdTokenCacheMaps", () => { + expect(IdTokenCacheMaps.toCacheMap["clientId"]).to.equal("client_id"); + expect(IdTokenCacheMaps.fromCacheMap["client_id"]).to.equal("clientId"); + }); + + it("Verify an RefreshTokenCacheMaps", () => { + expect(RefreshTokenCacheMaps.toCacheMap["credentialType"]).to.equal("credential_type"); + expect(RefreshTokenCacheMaps.fromCacheMap["credential_type"]).to.equal("credentialType"); + }); + + it("Verify an AccountCacheMaps", () => { + expect(AccountCacheMaps.toCacheMap["clientInfo"]).to.equal("client_info"); + expect(AccountCacheMaps.fromCacheMap["client_info"]).to.equal("clientInfo"); + }); + + it("Verify an AppMetadataCacheMaps", () => { + expect(AppMetadataCacheMaps.toCacheMap["familyId"]).to.equal("family_id"); + expect(AppMetadataCacheMaps.fromCacheMap["family_id"]).to.equal("familyId"); + }); +}); diff --git a/lib/msal-common/test/unifiedCache/serialize/Serializer.spec.ts b/lib/msal-common/test/unifiedCache/serialize/Serializer.spec.ts new file mode 100644 index 0000000000..6685f16896 --- /dev/null +++ b/lib/msal-common/test/unifiedCache/serialize/Serializer.spec.ts @@ -0,0 +1,78 @@ +import { expect } from "chai"; +import { MockCache } from "../entities/cacheConstants"; +import { Serializer } from "../../../src/unifiedCache/serialize/Serializer"; +import { Deserializer } from "../../../src/unifiedCache/serialize/Deserializer"; +import { JsonCache, InMemoryCache } from "../../../src/unifiedCache/utils/CacheTypes"; + +const cachedJson = require("./cache.json"); + +describe("Serializer test cases", () => { + const cache = JSON.stringify(cachedJson); + const jsonCache: JsonCache = Deserializer.deserializeJSONBlob(cache); + + it("serializeJSONBlob", () => { + const json = Serializer.serializeJSONBlob(cachedJson); + expect(JSON.parse(json)).to.eql(cachedJson); + }); + + it("serializeAccountCacheEntity", () => { + // create mock Account + const acc = { [MockCache.accKey]: MockCache.acc }; + + // serialize the mock Account and Test equivalency with the cache.json provided + const serializedAcc = Serializer.serializeAccounts(acc); + expect(serializedAcc[MockCache.accKey]).to.eql(jsonCache.Account[MockCache.accKey]); + }); + + it("serializeIdTokenCacheEntity", () => { + // create mock IdToken + const idt = { [MockCache.idTKey]: MockCache.idT }; + + // serialize the mock IdToken and Test equivalency with the cache.json provided + const serializedIdT = Serializer.serializeIdTokens(idt); + expect(serializedIdT[MockCache.idTKey]).to.eql(jsonCache.IdToken[MockCache.idTKey]); + }); + + it("serializeAccessTokenEntity", () => { + // create mock AccessToken + const at = { [MockCache.atOneKey]: MockCache.atOne }; + + // serialize the mock AccessToken and Test equivalency with the cache.json provided + const serializedAt = Serializer.serializeAccessTokens(at); + expect(serializedAt[MockCache.atOneKey]).to.eql(jsonCache.AccessToken[MockCache.atOneKey]); + }); + + it("serializeRefreshTokenCacheEntity", () => { + // create mock Refresh Token + const rt = { [MockCache.rtKey]: MockCache.rt }; + + // serialize the mock RefreshToken and Test equivalency with the cache.json provided + const serializedRT = Serializer.serializeRefreshTokens(rt); + expect(serializedRT[MockCache.rtKey]).to.eql(jsonCache.RefreshToken[MockCache.rtKey]); + }); + + it("serializeAppMetadataCacheEntity", () => { + // create mock AppMetadata + const amdt = { [MockCache.amdtKey]: MockCache.amdt }; + + // serialize the mock AppMetadata and Test equivalency with the cache.json provided + const serializedAmdt = Serializer.serializeAppMetadata(amdt); + expect(serializedAmdt[MockCache.amdtKey]).to.eql(jsonCache.AppMetadata[MockCache.amdtKey]); + }); + + it("serializeAll", () => { + + // deserialize the cache from memory and Test equivalency with the generated mock cache + const inMemoryCache: InMemoryCache = Deserializer.deserializeAllCache(jsonCache); + const jCache: JsonCache = Serializer.serializeAllCache(inMemoryCache); + + expect(jCache.Account[MockCache.accKey]).to.eql(jsonCache.Account[MockCache.accKey]); + expect(jCache.IdToken[MockCache.idTKey]).to.eql(jsonCache.IdToken[MockCache.idTKey]); + expect(jCache.AccessToken[MockCache.atOneKey]).to.eql(jsonCache.AccessToken[MockCache.atOneKey]); + expect(jCache.AccessToken[MockCache.atTwoKey]).to.eql(jsonCache.AccessToken[MockCache.atTwoKey]); + expect(jCache.RefreshToken[MockCache.rtKey]).to.eql(jsonCache.RefreshToken[MockCache.rtKey]); + expect(jCache.RefreshToken[MockCache.rtFKey]).to.eql(jsonCache.RefreshToken[MockCache.rtFKey]); + expect(jCache.AppMetadata[MockCache.amdtKey]).to.eql(jsonCache.AppMetadata[MockCache.amdtKey]); + }); + +}); diff --git a/lib/msal-common/test/unifiedCache/serialize/cache.json b/lib/msal-common/test/unifiedCache/serialize/cache.json new file mode 100644 index 0000000000..3aca7ad39b --- /dev/null +++ b/lib/msal-common/test/unifiedCache/serialize/cache.json @@ -0,0 +1,73 @@ +{ + "Account": { + "uid.utid-login.microsoftonline.com-microsoft": { + "username": "John Doe", + "local_account_id": "object1234", + "realm": "microsoft", + "environment": "login.microsoftonline.com", + "home_account_id": "uid.utid", + "authority_type": "MSSTS", + "client_info": "eyJ1aWQiOiJ1aWQiLCAidXRpZCI6InV0aWQifQ==" + } + }, + "RefreshToken": { + "uid.utid-login.microsoftonline.com-refreshtoken-mock_client_id--": { + "environment": "login.microsoftonline.com", + "credential_type": "RefreshToken", + "secret": "a refresh token", + "client_id": "mock_client_id", + "home_account_id": "uid.utid" + }, + "uid.utid-login.microsoftonline.com-refreshtoken-1--": { + "environment": "login.microsoftonline.com", + "credential_type": "RefreshToken", + "secret": "a refresh token", + "client_id": "mock_client_id", + "home_account_id": "uid.utid", + "familyId": "1" + } + }, + "AccessToken": { + "uid.utid-login.microsoftonline.com-accesstoken-mock_client_id-microsoft-scope1 scope2 scope3": { + "environment": "login.microsoftonline.com", + "credential_type": "AccessToken", + "secret": "an access token", + "realm": "microsoft", + "target": "scope1 scope2 scope3", + "client_id": "mock_client_id", + "cached_at": "1000", + "home_account_id": "uid.utid", + "extended_expires_on": "4600", + "expires_on": "4600" + }, + "uid.utid-login.microsoftonline.com-accesstoken-mock_client_id-microsoft-scope4 scope5": { + "environment": "login.microsoftonline.com", + "credential_type": "AccessToken", + "secret": "an access token", + "realm": "microsoft", + "target": "scope4 scope5", + "client_id": "mock_client_id", + "cached_at": "1000", + "home_account_id": "uid.utid", + "extended_expires_on": "4600", + "expires_on": "4600" + } + }, + "IdToken": { + "uid.utid-login.microsoftonline.com-idtoken-mock_client_id-microsoft-": { + "realm": "microsoft", + "environment": "login.microsoftonline.com", + "credential_type": "IdToken", + "secret": "header.eyJvaWQiOiAib2JqZWN0MTIzNCIsICJwcmVmZXJyZWRfdXNlcm5hbWUiOiAiSm9obiBEb2UiLCAic3ViIjogInN1YiJ9.signature", + "client_id": "mock_client_id", + "home_account_id": "uid.utid" + } + }, + "AppMetadata": { + "appmetadata-login.microsoftonline.com-mock_client_id": { + "environment": "login.microsoftonline.com", + "family_id": "1", + "client_id": "mock_client_id" + } + } +} diff --git a/lib/msal-common/test/utils/StringConstants.ts b/lib/msal-common/test/utils/StringConstants.ts index f1f3ec917f..dcce2da28f 100644 --- a/lib/msal-common/test/utils/StringConstants.ts +++ b/lib/msal-common/test/utils/StringConstants.ts @@ -17,6 +17,7 @@ export const TEST_TOKENS = { SAMPLE_JWT_PAYLOAD: "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ", SAMPLE_JWT_SIG: "SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", SAMPLE_MALFORMED_JWT: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ", + CACHE_IDTOKEN: "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Imk2bEdrM0ZaenhSY1ViMkMzbkVRN3N5SEpsWSIsImtpZCI6Imk2bEdrM0ZaenhSY1ViMkMzbkVRN3N5SEpsWSJ9.eyJvaWQiOiAib2JqZWN0MTIzNCIsICJwcmVmZXJyZWRfdXNlcm5hbWUiOiAiSm9obiBEb2UiLCAic3ViIjogInN1YiJ9.D3H6pMUtQnoJAGq6AHd" }; // Test Expiration Vals @@ -34,7 +35,9 @@ export const TEST_DATA_CLIENT_INFO = { TEST_INVALID_JSON_CLIENT_INFO: `{"uid":"123-test-uid""utid":"456-test-utid"}`, TEST_RAW_CLIENT_INFO: "eyJ1aWQiOiIxMjMtdGVzdC11aWQiLCJ1dGlkIjoiNDU2LXRlc3QtdXRpZCJ9", TEST_CLIENT_INFO_B64ENCODED: "eyJ1aWQiOiIxMjM0NSIsInV0aWQiOiI2Nzg5MCJ9", - TEST_HOME_ACCOUNT_ID: "MTIzLXRlc3QtdWlk.NDU2LXRlc3QtdXRpZA==" + TEST_HOME_ACCOUNT_ID: "MTIzLXRlc3QtdWlk.NDU2LXRlc3QtdXRpZA==", + TEST_CACHE_RAW_CLIENT_INFO: "eyJ1aWQiOiJ1aWQiLCAidXRpZCI6InV0aWQifQ==", + TEST_CACHE_DECODED_CLIENT_INFO:`{"uid":"uid", "utid":"utid"}` }; // Test Hashes @@ -194,7 +197,7 @@ export const AUTHENTICATION_RESULT = { "ext_expires_in": 3599, "access_token": "thisIs.an.accessT0ken", "refresh_token": "thisIsARefreshT0ken", - "id_token": "thisIsAIdT0ken" + "id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjFMVE16YWtpaGlSbGFfOHoyQkVKVlhlV01xbyJ9.eyJ2ZXIiOiIyLjAiLCJpc3MiOiJodHRwczovL2xvZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vOTE4ODA0MGQtNmM2Ny00YzViLWIxMTItMzZhMzA0YjY2ZGFkL3YyLjAiLCJzdWIiOiJBQUFBQUFBQUFBQUFBQUFBQUFBQUFJa3pxRlZyU2FTYUZIeTc4MmJidGFRIiwiYXVkIjoiNmNiMDQwMTgtYTNmNS00NmE3LWI5OTUtOTQwYzc4ZjVhZWYzIiwiZXhwIjoxNTM2MzYxNDExLCJpYXQiOjE1MzYyNzQ3MTEsIm5iZiI6MTUzNjI3NDcxMSwibmFtZSI6IkFiZSBMaW5jb2xuIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiQWJlTGlAbWljcm9zb2Z0LmNvbSIsIm9pZCI6IjAwMDAwMDAwLTAwMDAtMDAwMC02NmYzLTMzMzJlY2E3ZWE4MSIsInRpZCI6IjMzMzgwNDBkLTZjNjctNGM1Yi1iMTEyLTM2YTMwNGI2NmRhZCIsIm5vbmNlIjoiMTIzNTIzIiwiYWlvIjoiRGYyVVZYTDFpeCFsTUNXTVNPSkJjRmF0emNHZnZGR2hqS3Y4cTVnMHg3MzJkUjVNQjVCaXN2R1FPN1lXQnlqZDhpUURMcSFlR2JJRGFreXA1bW5PcmNkcUhlWVNubHRlcFFtUnA2QUlaOGpZIn0=.1AFWW-Ck5nROwSlltm7GzZvDwUkqvhSQpm55TQsmVo9Y59cLhRXpvB8n-55HCr9Z6G_31_UbeUkoz612I2j_Sm9FFShSDDjoaLQr54CreGIJvjtmS3EkK9a7SJBbcpL1MpUtlfygow39tFjY7EVNW9plWUvRrTgVk7lYLprvfzw-CIqw3gHC-T7IK_m_xkr08INERBtaecwhTeN4chPC4W3jdmw_lIxzC48YoQ0dB1L9-ImX98Egypfrlbm0IBL5spFzL6JDZIRRJOu8vecJvj1mq-IUhGt0MacxX8jdxYLP-KUu2d9MbNKpCKJuZ7p8gwTL5B7NlUdh_dmSviPWrw" } }; diff --git a/lib/msal-common/tsconfig.json b/lib/msal-common/tsconfig.json index 9d1a20d55f..5e25ba44f6 100644 --- a/lib/msal-common/tsconfig.json +++ b/lib/msal-common/tsconfig.json @@ -16,7 +16,7 @@ "resolveJsonModule": true }, "include": [ - "src/**/*" + "src/**/*", "src/unifiedCache/serialize/.ts" ], "exclude": [ "node_modules" diff --git a/lib/msal-core/package-lock.json b/lib/msal-core/package-lock.json index 0eeb600bd1..dcbd7d98b2 100644 --- a/lib/msal-core/package-lock.json +++ b/lib/msal-core/package-lock.json @@ -5282,8 +5282,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -5304,14 +5303,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5326,20 +5323,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -5456,8 +5450,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -5469,7 +5462,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -5484,7 +5476,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -5497,7 +5488,6 @@ "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -5516,7 +5506,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" }, @@ -5606,8 +5595,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -5619,7 +5607,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -5671,7 +5658,7 @@ }, "dependencies": { "minimist": { - "version": "1.2.5", + "version": "1.2.0", "bundled": true, "dev": true, "optional": true @@ -5705,8 +5692,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -5742,7 +5728,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -5762,7 +5747,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5806,14 +5790,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, diff --git a/lib/msal-node/cache.json b/lib/msal-node/cache.json new file mode 100644 index 0000000000..d3fdd20f5b --- /dev/null +++ b/lib/msal-node/cache.json @@ -0,0 +1,73 @@ +{ + "Account": { + "uid.utid-login.microsoftonline.com-microsoft": { + "username": "John Doe", + "local_account_id": "object1234", + "realm": "microsoft", + "environment": "login.microsoftonline.com", + "home_account_id": "uid.utid", + "authority_type": "MSSTS", + "client_info": "base64encodedjson" + } + }, + "RefreshToken": { + "uid.utid-login.microsoftonline.com-refreshtoken-mock_client_id--": { + "environment": "login.microsoftonline.com", + "credential_type": "RefreshToken", + "secret": "a refresh token", + "client_id": "mock_client_id", + "home_account_id": "uid.utid" + }, + "uid.utid-login.microsoftonline.com-refreshtoken-1--": { + "environment": "login.microsoftonline.com", + "credential_type": "RefreshToken", + "secret": "a refresh token", + "client_id": "mock_client_id", + "home_account_id": "uid.utid", + "familyId": "1" + } + }, + "AccessToken": { + "uid.utid-login.microsoftonline.com-accesstoken-mock_client_id-microsoft-scope1 scope2 scope3": { + "environment": "login.microsoftonline.com", + "credential_type": "AccessToken", + "secret": "an access token", + "realm": "microsoft", + "target": "scope1 scope2 scope3", + "client_id": "mock_client_id", + "cached_at": "1000", + "home_account_id": "uid.utid", + "extended_expires_on": "4600", + "expires_on": "4600" + }, + "uid.utid-login.microsoftonline.com-accesstoken-mock_client_id-microsoft-scope4 scope5": { + "environment": "login.microsoftonline.com", + "credential_type": "AccessToken", + "secret": "an access token", + "realm": "microsoft", + "target": "scope4 scope5", + "client_id": "mock_client_id", + "cached_at": "1000", + "home_account_id": "uid.utid", + "extended_expires_on": "4600", + "expires_on": "4600" + } + }, + "IdToken": { + "uid.utid-login.microsoftonline.com-idtoken-mock_client_id-microsoft-": { + "realm": "microsoft", + "environment": "login.microsoftonline.com", + "credential_type": "IdToken", + "secret": "header.eyJvaWQiOiAib2JqZWN0MTIzNCIsICJwcmVmZXJyZWRfdXNlcm5hbWUiOiAiSm9obiBEb2UiLCAic3ViIjogInN1YiJ9.signature", + "client_id": "mock_client_id", + "home_account_id": "uid.utid" + } + }, + "AppMetadata": { + "appmetadata-login.microsoftonline.com-mock_client_id": { + "environment": "login.microsoftonline.com", + "family_id": "1", + "client_id": "mock_client_id" + } + } +} diff --git a/lib/msal-node/package.json b/lib/msal-node/package.json index 6e9d75205c..4b093baf1a 100644 --- a/lib/msal-node/package.json +++ b/lib/msal-node/package.json @@ -62,6 +62,7 @@ "devDependencies": { "@types/debug": "^4.1.5", "@types/jest": "^25.1.2", + "@types/node": "^13.13.4", "@types/uuid": "^7.0.0", "husky": "^4.2.3", "tsdx": "^0.13.2", diff --git a/lib/msal-node/src/cache/CacheContext.ts b/lib/msal-node/src/cache/CacheContext.ts new file mode 100644 index 0000000000..a563f34eca --- /dev/null +++ b/lib/msal-node/src/cache/CacheContext.ts @@ -0,0 +1,56 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { JsonCache, Deserializer } from '@azure/msal-common'; +import { Storage } from '../cache/Storage'; + +/** + * class managing sync between the persistent cache blob in the disk and the in memory cache of the node + */ +export class CacheContext { + private defaultSerializedCache: JsonCache = { + Account: {}, + IdToken: {}, + AccessToken: {}, + RefreshToken: {}, + AppMetadata: {}, + }; + + /** + * Update the library cache + * @param storage + */ + setCurrentCache(storage: Storage, cacheObject: JsonCache) { + const cacheWithOverlayedDefaults = this.overlayDefaults(cacheObject); + storage.setCache( + Deserializer.deserializeAllCache(cacheWithOverlayedDefaults) + ); + } + + overlayDefaults(passedInCache: JsonCache): JsonCache { + return { + Account: { + ...this.defaultSerializedCache.Account, + ...passedInCache.Account, + }, + IdToken: { + ...this.defaultSerializedCache.IdToken, + ...passedInCache.IdToken, + }, + AccessToken: { + ...this.defaultSerializedCache.AccessToken, + ...passedInCache.AccessToken, + }, + RefreshToken: { + ...this.defaultSerializedCache.RefreshToken, + ...passedInCache.RefreshToken, + }, + AppMetadata: { + ...this.defaultSerializedCache.AppMetadata, + ...passedInCache.AppMetadata, + }, + }; + } +} diff --git a/lib/msal-node/src/cache/Storage.ts b/lib/msal-node/src/cache/Storage.ts index 641290261d..a7de13d6a8 100644 --- a/lib/msal-node/src/cache/Storage.ts +++ b/lib/msal-node/src/cache/Storage.ts @@ -2,92 +2,47 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ -import { - ICacheStorage, - Constants, - PersistentCacheKeys, - TemporaryCacheKeys, -} from '@azure/msal-common'; +import { ICacheStorage, InMemoryCache } from '@azure/msal-common'; import { CacheOptions } from '../config/Configuration'; -import { CACHE } from './../utils/Constants'; - -const fs = require('fs'); - -// Cookie life calculation (hours * minutes * seconds * ms) -const COOKIE_LIFE_MULTIPLIER = 24 * 60 * 60 * 1000; /** - * This class implements the cache storage interface for MSAL through browser local or session storage. - * Cookies are only used if storeAuthStateInCookie is true, and are only used for - * parameters such as state and nonce, generally. + * This class implements Storage for node, reading cache from user specified storage location or an extension library */ export class Storage implements ICacheStorage { // Cache configuration, either set by user or default values. private cacheConfig: CacheOptions; - // storage object - private fileStorage: Storage; - // Client id of application. Used in cache keys to partition cache correctly in the case of multiple instances of MSAL. - private clientId: string; + private inMemoryCache: InMemoryCache; - constructor(clientId: string, cacheConfig: CacheOptions) { + constructor(cacheConfig: CacheOptions) { this.cacheConfig = cacheConfig; - this.fileStorage = this.initializeFileStorage( - this.cacheConfig.cacheLocation! - ); - this.clientId = clientId; - } - - private initializeFileStorage(cacheLocation: string) { - if (cacheLocation === CACHE.FILE_CACHE) { - fs.open('NodeCache.txt', 'w', (err: Error) => { - if (err) throw err; - }); - - return fs; - } + if (this.cacheConfig.cacheLocation! === 'fileCache') + this.inMemoryCache = this.cacheConfig.cacheInMemory!; } /** - * Prepend msal. to each key; Skip for any JSON object as Key (defined schemas do not need the key appended: AccessToken Keys or the upcoming schema) - * @param key - * @param addInstanceId + * gets the current in memory cache for the client */ - private generateCacheKey(key: string): string { - try { - // Defined schemas do not need the key migrated - this.validateObjectKey(key); - return key; - } catch (e) { - if ( - key.startsWith(`${Constants.CACHE_PREFIX}`) || - key.startsWith(PersistentCacheKeys.ADAL_ID_TOKEN) - ) { - return key; - } - return `${Constants.CACHE_PREFIX}.${this.clientId}.${key}`; - } + getCache(): InMemoryCache { + return this.inMemoryCache; } /** - * Parses key as JSON object, JSON.parse() will throw an error. - * @param key + * sets the current in memory cache for the client + * @param inMemoryCache */ - private validateObjectKey(key: string): void { - JSON.parse(key); + setCache(inMemoryCache: InMemoryCache) { + this.inMemoryCache = inMemoryCache; } /** * Sets the cache item with the key and value given. - * Stores in cookie if storeAuthStateInCookie is set to true. - * This can cause cookie overflow if used incorrectly. * @param key * @param value + * TODO: implement after the lookup implementation */ setItem(key: string, value: string): void { - const msalKey = this.generateCacheKey(key); - this.fileStorage.setItem(msalKey, value); - if (this.cacheConfig.storeAuthStateInCookie) { - this.setItemCookie(msalKey, value); + if (key && value) { + return; } } @@ -95,137 +50,43 @@ export class Storage implements ICacheStorage { * Gets cache item with given key. * Will retrieve frm cookies if storeAuthStateInCookie is set to true. * @param key + * TODO: implement after the lookup implementation */ getItem(key: string): string { - const msalKey = this.generateCacheKey(key); - const itemCookie = this.getItemCookie(msalKey); - if (this.cacheConfig.storeAuthStateInCookie && itemCookie) { - return itemCookie; - } - return this.fileStorage.getItem(msalKey); + return key ? 'random' : 'truly random'; } /** * Removes the cache item with the given key. * Will also clear the cookie item if storeAuthStateInCookie is set to true. * @param key + * TODO: implement after the lookup implementation */ removeItem(key: string): void { - const msalKey = this.generateCacheKey(key); - this.fileStorage.removeItem(msalKey); - if (this.cacheConfig.storeAuthStateInCookie) { - this.clearItemCookie(msalKey); - } + if (!key) return; } /** * Checks whether key is in cache. * @param key + * TODO: implement after the lookup implementation */ containsKey(key: string): boolean { - const msalKey = this.generateCacheKey(key); - return ( - this.fileStorage.hasOwnProperty(msalKey) || - this.fileStorage.hasOwnProperty(key) - ); + return key ? true : false; } /** * Gets all keys in window. + * TODO: implement after the lookup implementation */ getKeys(): string[] { - return Object.keys(this.fileStorage); + return []; } /** * Clears all cache entries created by MSAL (except tokens). */ clear(): void { - let key: string; - for (key in this.fileStorage) { - // Check if key contains msal prefix; For now, we are clearing all the cache items created by MSAL.js - if ( - this.fileStorage.hasOwnProperty(key) && - key.indexOf(Constants.CACHE_PREFIX) !== -1 && - key.indexOf(this.clientId) !== -1 - ) { - this.removeItem(key); - } - } - } - - /** - * Add value to cookies - * @param cookieName - * @param cookieValue - * @param expires - */ - setItemCookie( - cookieName: string, - cookieValue: string, - expires?: number - ): void { - let cookieStr = `${cookieName}=${cookieValue};path=/;`; - if (expires) { - const expireTime = this.getCookieExpirationTime(expires); - cookieStr += `expires=${expireTime};`; - } - - document.cookie = cookieStr; - } - - /** - * Get one item by key from cookies - * @param cookieName - */ - getItemCookie(cookieName: string): string { - const name = `${cookieName}=`; - const cookieList = document.cookie.split(';'); - for (let i = 0; i < cookieList.length; i++) { - let cookie = cookieList[i]; - while (cookie.charAt(0) === ' ') { - cookie = cookie.substring(1); - } - if (cookie.indexOf(name) === 0) { - return cookie.substring(name.length, cookie.length); - } - } - return ''; - } - - /** - * Clear an item in the cookies by key - * @param cookieName - */ - clearItemCookie(cookieName: string): void { - this.setItemCookie(cookieName, '', -1); - } - - /** - * Clear all msal cookies - */ - clearMsalCookie(state?: string): void { - const nonceKey = state - ? `${TemporaryCacheKeys.NONCE_IDTOKEN}|${state}` - : TemporaryCacheKeys.NONCE_IDTOKEN; - this.clearItemCookie(this.generateCacheKey(nonceKey)); - this.clearItemCookie( - this.generateCacheKey(TemporaryCacheKeys.REQUEST_STATE) - ); - this.clearItemCookie( - this.generateCacheKey(TemporaryCacheKeys.ORIGIN_URI) - ); - } - - /** - * Get cookie expiration time - * @param cookieLifeDays - */ - getCookieExpirationTime(cookieLifeDays: number): string { - const today = new Date(); - const expr = new Date( - today.getTime() + cookieLifeDays * COOKIE_LIFE_MULTIPLIER - ); - return expr.toUTCString(); + return; } } diff --git a/lib/msal-node/src/client/ClientApplication.ts b/lib/msal-node/src/client/ClientApplication.ts index 822b92e1c5..a60db753c2 100644 --- a/lib/msal-node/src/client/ClientApplication.ts +++ b/lib/msal-node/src/client/ClientApplication.ts @@ -10,35 +10,40 @@ import { ClientConfiguration, RefreshTokenClient, RefreshTokenRequest, + AuthenticationResult, Authority, AuthorityFactory, ClientAuthError, Constants, - B2cAuthority + B2cAuthority, + JsonCache, + Serializer, } from '@azure/msal-common'; import { Configuration, buildAppConfiguration } from '../config/Configuration'; import { CryptoProvider } from '../crypto/CryptoProvider'; import { Storage } from '../cache/Storage'; import { version } from '../../package.json'; -import { Constants as NodeConstants } from "./../utils/Constants"; +import { Constants as NodeConstants } from './../utils/Constants'; +import { CacheContext } from '../cache/CacheContext'; export abstract class ClientApplication { - private config: Configuration; private _authority: Authority; private readonly cryptoProvider: CryptoProvider; - private readonly storage: Storage; + private storage: Storage; + private cacheContext: CacheContext; /** * @constructor * Constructor for the ClientApplication - */ + */ protected constructor(configuration: Configuration) { this.config = buildAppConfiguration(configuration); this.cryptoProvider = new CryptoProvider(); - this.storage = new Storage(this.config.auth.clientId, this.config.cache!); + this.storage = new Storage(this.config.cache!); B2cAuthority.setKnownAuthorities(this.config.auth.knownAuthorities!); + this.cacheContext = new CacheContext(); } /** @@ -51,10 +56,15 @@ export abstract class ClientApplication { * acquireToken(AuthorizationCodeRequest) * @param request */ - async getAuthCodeUrl(request: AuthorizationCodeUrlRequest): Promise { - - const authClientConfig = await this.buildOauthClientConfiguration(request.authority); - const authorizationCodeClient = new AuthorizationCodeClient(authClientConfig); + async getAuthCodeUrl( + request: AuthorizationCodeUrlRequest + ): Promise { + const authClientConfig = await this.buildOauthClientConfiguration( + request.authority + ); + const authorizationCodeClient = new AuthorizationCodeClient( + authClientConfig + ); return authorizationCodeClient.getAuthCodeUrl(request); } @@ -68,10 +78,15 @@ export abstract class ClientApplication { * * @param request */ - async acquireTokenByCode(request: AuthorizationCodeRequest): Promise { - - const authClientConfig = await this.buildOauthClientConfiguration(request.authority); - const authorizationCodeClient = new AuthorizationCodeClient(authClientConfig); + async acquireTokenByCode( + request: AuthorizationCodeRequest + ): Promise { + const authClientConfig = await this.buildOauthClientConfiguration( + request.authority + ); + const authorizationCodeClient = new AuthorizationCodeClient( + authClientConfig + ); return authorizationCodeClient.acquireToken(request); } @@ -83,24 +98,33 @@ export abstract class ClientApplication { * handle the caching and refreshing of tokens automatically. * @param request */ - async acquireTokenByRefreshToken(request: RefreshTokenRequest): Promise { - - const refreshTokenClientConfig = await this.buildOauthClientConfiguration(request.authority); - const refreshTokenClient = new RefreshTokenClient(refreshTokenClientConfig); + async acquireTokenByRefreshToken( + request: RefreshTokenRequest + ): Promise { + const refreshTokenClientConfig = await this.buildOauthClientConfiguration( + request.authority + ); + const refreshTokenClient = new RefreshTokenClient( + refreshTokenClientConfig + ); return refreshTokenClient.acquireToken(request); } - protected async buildOauthClientConfiguration(authority?: string): Promise { + protected async buildOauthClientConfiguration( + authority?: string + ): Promise { // using null assertion operator as we ensure that all config values have default values in buildConfiguration() return { authOptions: { clientId: this.config.auth.clientId, authority: await this.createAuthority(authority), - knownAuthorities: this.config.auth.knownAuthorities + knownAuthorities: this.config.auth.knownAuthorities, }, loggerOptions: { - loggerCallback: this.config.system!.loggerOptions!.loggerCallback, - piiLoggingEnabled: this.config.system!.loggerOptions!.piiLoggingEnabled, + loggerCallback: this.config.system!.loggerOptions! + .loggerCallback, + piiLoggingEnabled: this.config.system!.loggerOptions! + .piiLoggingEnabled, }, cryptoInterface: this.cryptoProvider, networkInterface: this.config.system!.networkClient, @@ -108,8 +132,8 @@ export abstract class ClientApplication { libraryInfo: { sku: NodeConstants.MSAL_SKU, version: version, - cpu: process.arch || "", - os: process.platform || "" + cpu: process.arch || '', + os: process.platform || '', }, }; } @@ -119,12 +143,17 @@ export abstract class ClientApplication { * object. If no authority set in application object, then default to common authority. * @param authorityString */ - private async createAuthority(authorityString?: string): Promise { + private async createAuthority( + authorityString?: string + ): Promise { const authority: Authority = authorityString - ? AuthorityFactory.createInstance(authorityString, this.config.system!.networkClient!) + ? AuthorityFactory.createInstance( + authorityString, + this.config.system!.networkClient! + ) : this.authority; - if(authority.discoveryComplete()){ + if (authority.discoveryComplete()) { return authority; } @@ -148,4 +177,19 @@ export abstract class ClientApplication { return this._authority; } + + /** + * Initialize cache from a user provided Json file + * @param cacheObject + */ + initializeCache(cacheObject: JsonCache) { + this.cacheContext.setCurrentCache(this.storage, cacheObject); + } + + /** + * read the cache as a Json convertible object from memory + */ + readCache(): JsonCache { + return Serializer.serializeAllCache(this.storage.getCache()); + } } diff --git a/lib/msal-node/src/client/PublicClientApplication.ts b/lib/msal-node/src/client/PublicClientApplication.ts index bc5b55c623..875f6a97fe 100644 --- a/lib/msal-node/src/client/PublicClientApplication.ts +++ b/lib/msal-node/src/client/PublicClientApplication.ts @@ -12,7 +12,6 @@ import { ClientApplication } from './ClientApplication'; * are not trusted to safely store application secrets, and therefore can only request tokens in the name of an user. */ export class PublicClientApplication extends ClientApplication { - /** * @constructor * @@ -48,8 +47,12 @@ export class PublicClientApplication extends ClientApplication { * until the end-user completes input of credentials. * @param request */ - public async acquireTokenByDeviceCode(request: DeviceCodeRequest): Promise{ - const deviceCodeConfig = await this.buildOauthClientConfiguration(request.authority); + public async acquireTokenByDeviceCode( + request: DeviceCodeRequest + ): Promise { + const deviceCodeConfig = await this.buildOauthClientConfiguration( + request.authority + ); const deviceCodeClient = new DeviceCodeClient(deviceCodeConfig); return deviceCodeClient.acquireToken(request); } diff --git a/lib/msal-node/src/config/Configuration.ts b/lib/msal-node/src/config/Configuration.ts index b9ad5dc842..1461d862d1 100644 --- a/lib/msal-node/src/config/Configuration.ts +++ b/lib/msal-node/src/config/Configuration.ts @@ -6,10 +6,11 @@ import { LoggerOptions, INetworkModule, LogLevel, + InMemoryCache, } from '@azure/msal-common'; import { NetworkUtils } from '../utils/NetworkUtils'; import { CACHE } from '../utils/Constants'; -import debug from "debug"; +import debug from 'debug'; /** * - clientId - Client id of the application. @@ -20,7 +21,7 @@ export type NodeAuthOptions = { clientId: string; authority?: string; knownAuthorities?: Array; -} +}; /** * Use this to configure the below cache configuration options: @@ -32,6 +33,7 @@ export type NodeAuthOptions = { export type CacheOptions = { cacheLocation?: string; storeAuthStateInCookie?: boolean; + cacheInMemory?: InMemoryCache; }; /** @@ -61,17 +63,28 @@ export type Configuration = { const DEFAULT_AUTH_OPTIONS: NodeAuthOptions = { clientId: '', authority: '', - knownAuthorities: [] + knownAuthorities: [], }; const DEFAULT_CACHE_OPTIONS: CacheOptions = { cacheLocation: CACHE.FILE_CACHE, storeAuthStateInCookie: false, + cacheInMemory: { + accounts: {}, + idTokens: {}, + accessTokens: {}, + refreshTokens: {}, + appMetadata: {}, + }, }; const DEFAULT_LOGGER_OPTIONS: LoggerOptions = { - loggerCallback: (level: LogLevel, message: string, containsPii: boolean) => { - debug(`msal:${LogLevel[level]}${containsPii ? "-Pii": ""}`)(message); + loggerCallback: ( + level: LogLevel, + message: string, + containsPii: boolean + ) => { + debug(`msal:${LogLevel[level]}${containsPii ? '-Pii' : ''}`)(message); }, piiLoggingEnabled: false, logLevel: LogLevel.Info, diff --git a/lib/msal-node/src/utils/Constants.ts b/lib/msal-node/src/utils/Constants.ts index 63256b493a..3f04c00b96 100644 --- a/lib/msal-node/src/utils/Constants.ts +++ b/lib/msal-node/src/utils/Constants.ts @@ -35,7 +35,7 @@ export const CharSet = { * Cache Constants */ export const CACHE = { - FILE_CACHE: 'file_cache', + FILE_CACHE: 'fileCache', EXTENSION_LIB: 'extenstion_library', }; @@ -43,5 +43,5 @@ export const CACHE = { * Constants for headers */ export const Constants = { - MSAL_SKU: "msal.js.node", + MSAL_SKU: 'msal.js.node', }; diff --git a/lib/msal-node/test/client/PublicClientApplication.spec.ts b/lib/msal-node/test/client/PublicClientApplication.spec.ts index aa85ce6128..5fbd0a6dfa 100644 --- a/lib/msal-node/test/client/PublicClientApplication.spec.ts +++ b/lib/msal-node/test/client/PublicClientApplication.spec.ts @@ -1,7 +1,7 @@ import { PublicClientApplication } from './../../src/client/PublicClientApplication'; import { AuthorizationCodeRequest, Configuration } from './../../src/index'; -import { TEST_CONSTANTS } from "../utils/TestConstants"; -import { mocked } from "ts-jest/utils"; +import { TEST_CONSTANTS } from '../utils/TestConstants'; +import { mocked } from 'ts-jest/utils'; import { Authority, AuthorityFactory, @@ -13,50 +13,51 @@ import { RefreshTokenClient, RefreshTokenRequest, ClientConfiguration, -} from "@azure/msal-common"; +} from '@azure/msal-common'; -jest.mock("@azure/msal-common"); +jest.mock('@azure/msal-common'); describe('PublicClientApplication', () => { - const authority: Authority = { resolveEndpointsAsync: () => { - return new Promise((resolve) => { + return new Promise(resolve => { resolve(); }); }, discoveryComplete: () => { return true; - } + }, } as Authority; let appConfig: Configuration = { auth: { clientId: TEST_CONSTANTS.CLIENT_ID, - authority: TEST_CONSTANTS.AUTHORITY - } + authority: TEST_CONSTANTS.AUTHORITY, + }, }; const expectedConfig: ClientConfiguration = { authOptions: { clientId: TEST_CONSTANTS.CLIENT_ID, authority: authority, - knownAuthorities: [] - } + knownAuthorities: [], + }, }; + // const expectedOauthClientConfig: ClientConfiguration = { + // authOptions: appConfig.auth, + // }; + beforeEach(() => { jest.clearAllMocks(); }); test('exports a class', () => { - const authApp = new PublicClientApplication(appConfig); expect(authApp).toBeInstanceOf(PublicClientApplication); }); test('acquireTokenByDeviceCode', async () => { - const request: DeviceCodeRequest = { deviceCodeCallback: response => { console.log(response); @@ -69,11 +70,12 @@ describe('PublicClientApplication', () => { const authApp = new PublicClientApplication(appConfig); await authApp.acquireTokenByDeviceCode(request); expect(DeviceCodeClient).toHaveBeenCalledTimes(1); - expect(DeviceCodeClient).toHaveBeenCalledWith(expect.objectContaining(expectedConfig)); + expect(DeviceCodeClient).toHaveBeenCalledWith( + expect.objectContaining(expectedConfig) + ); }); test('acquireTokenByAuthorizationCode', async () => { - const request: AuthorizationCodeRequest = { scopes: TEST_CONSTANTS.DEFAULT_GRAPH_SCOPE, redirectUri: TEST_CONSTANTS.REDIRECT_URI, @@ -85,15 +87,15 @@ describe('PublicClientApplication', () => { const authApp = new PublicClientApplication(appConfig); await authApp.acquireTokenByCode(request); expect(AuthorizationCodeClient).toHaveBeenCalledTimes(1); - expect(AuthorizationCodeClient).toHaveBeenCalledWith(expect.objectContaining(expectedConfig)); - + expect(AuthorizationCodeClient).toHaveBeenCalledWith( + expect.objectContaining(expectedConfig) + ); }); test('acquireTokenByRefreshToken', async () => { - const request: RefreshTokenRequest = { scopes: TEST_CONSTANTS.DEFAULT_GRAPH_SCOPE, - refreshToken: TEST_CONSTANTS.REFRESH_TOKEN + refreshToken: TEST_CONSTANTS.REFRESH_TOKEN, }; mocked(AuthorityFactory.createInstance).mockReturnValueOnce(authority); @@ -101,11 +103,12 @@ describe('PublicClientApplication', () => { const authApp = new PublicClientApplication(appConfig); await authApp.acquireTokenByRefreshToken(request); expect(RefreshTokenClient).toHaveBeenCalledTimes(1); - expect(RefreshTokenClient).toHaveBeenCalledWith(expect.objectContaining(expectedConfig)); + expect(RefreshTokenClient).toHaveBeenCalledWith( + expect.objectContaining(expectedConfig) + ); }); test('create AuthorizationCode URL', async () => { - const request: AuthorizationCodeUrlRequest = { scopes: TEST_CONSTANTS.DEFAULT_GRAPH_SCOPE, redirectUri: TEST_CONSTANTS.REDIRECT_URI, @@ -116,47 +119,57 @@ describe('PublicClientApplication', () => { const authApp = new PublicClientApplication(appConfig); await authApp.getAuthCodeUrl(request); expect(AuthorizationCodeClient).toHaveBeenCalledTimes(1); - expect(AuthorizationCodeClient).toHaveBeenCalledWith(expect.objectContaining(expectedConfig)); + expect(AuthorizationCodeClient).toHaveBeenCalledWith( + expect.objectContaining(expectedConfig) + ); }); test('acquireToken default authority', async () => { - // No authority set in app configuration or request, should default to common authority - const config: Configuration = { + const config: Configuration = { auth: { clientId: TEST_CONSTANTS.CLIENT_ID, - } + }, }; const request: RefreshTokenRequest = { scopes: TEST_CONSTANTS.DEFAULT_GRAPH_SCOPE, - refreshToken: TEST_CONSTANTS.REFRESH_TOKEN + refreshToken: TEST_CONSTANTS.REFRESH_TOKEN, }; mocked(AuthorityFactory.createInstance).mockReturnValueOnce(authority); const authApp = new PublicClientApplication(config); await authApp.acquireTokenByRefreshToken(request); - expect(AuthorityFactory.createInstance).toHaveBeenCalledWith(Constants.DEFAULT_AUTHORITY, {}); + expect(AuthorityFactory.createInstance).toHaveBeenCalledWith( + Constants.DEFAULT_AUTHORITY, + {} + ); expect(RefreshTokenClient).toHaveBeenCalledTimes(1); - expect(RefreshTokenClient).toHaveBeenCalledWith(expect.objectContaining(expectedConfig)); + expect(RefreshTokenClient).toHaveBeenCalledWith( + expect.objectContaining(expectedConfig) + ); }); test('authority overridden by acquire token request parameters', async () => { - // Authority set on client app, but should be overridden by authority passed in request const request: RefreshTokenRequest = { scopes: TEST_CONSTANTS.DEFAULT_GRAPH_SCOPE, refreshToken: TEST_CONSTANTS.REFRESH_TOKEN, - authority: TEST_CONSTANTS.ALTERNATE_AUTHORITY + authority: TEST_CONSTANTS.ALTERNATE_AUTHORITY, }; mocked(AuthorityFactory.createInstance).mockReturnValueOnce(authority); const authApp = new PublicClientApplication(appConfig); await authApp.acquireTokenByRefreshToken(request); - expect(AuthorityFactory.createInstance).toHaveBeenCalledWith(TEST_CONSTANTS.ALTERNATE_AUTHORITY, {}); + expect(AuthorityFactory.createInstance).toHaveBeenCalledWith( + TEST_CONSTANTS.ALTERNATE_AUTHORITY, + {} + ); expect(RefreshTokenClient).toHaveBeenCalledTimes(1); - expect(RefreshTokenClient).toHaveBeenCalledWith(expect.objectContaining(expectedConfig)); + expect(RefreshTokenClient).toHaveBeenCalledWith( + expect.objectContaining(expectedConfig) + ); }); }); diff --git a/lib/msal-node/test/config/ClientConfiguration.spec.ts b/lib/msal-node/test/config/ClientConfiguration.spec.ts index 2757c4be24..a604dd95d4 100644 --- a/lib/msal-node/test/config/ClientConfiguration.spec.ts +++ b/lib/msal-node/test/config/ClientConfiguration.spec.ts @@ -1,16 +1,18 @@ -import {buildAppConfiguration, Configuration} from "../../src/config/Configuration"; -import {CACHE} from "../../src/utils/Constants"; -import {HttpClient} from "../../src/network/HttpClient"; -import {TEST_CONSTANTS} from "../utils/TestConstants"; -import {LogLevel, NetworkRequestOptions} from "@azure/msal-common"; +import { + buildAppConfiguration, + Configuration, +} from '../../src/config/Configuration'; +import { CACHE } from '../../src/utils/Constants'; +import { HttpClient } from '../../src/network/HttpClient'; +import { TEST_CONSTANTS } from '../utils/TestConstants'; +import { LogLevel, NetworkRequestOptions } from '@azure/msal-common'; describe('ClientConfiguration tests', () => { test('builds configuration and assigns default functions', () => { - const config: Configuration = buildAppConfiguration({ auth: { - clientId: TEST_CONSTANTS.CLIENT_ID - } + clientId: TEST_CONSTANTS.CLIENT_ID, + }, }); // network options @@ -63,7 +65,7 @@ describe('ClientConfiguration tests', () => { // expect(console.warn).toHaveBeenCalledTimes(1); // auth options - expect(config.auth!.authority).toEqual(""); + expect(config.auth!.authority).toEqual(''); expect(config.auth!.clientId).toEqual(TEST_CONSTANTS.CLIENT_ID); // cache options diff --git a/lib/msal-node/test/network/HttpClient.spec.ts b/lib/msal-node/test/network/HttpClient.spec.ts index 8c7803b398..09ddd8889b 100644 --- a/lib/msal-node/test/network/HttpClient.spec.ts +++ b/lib/msal-node/test/network/HttpClient.spec.ts @@ -1,6 +1,6 @@ import { HttpClient } from '../../src/network/HttpClient'; import axios, { AxiosResponse } from 'axios'; -import { mocked } from "ts-jest/utils"; +import { mocked } from 'ts-jest/utils'; jest.mock('axios'); diff --git a/lib/msal-node/test/utils/TestConstants.ts b/lib/msal-node/test/utils/TestConstants.ts index 70b448e6d9..7ad9181661 100644 --- a/lib/msal-node/test/utils/TestConstants.ts +++ b/lib/msal-node/test/utils/TestConstants.ts @@ -3,15 +3,17 @@ */ export const TEST_CONSTANTS = { - CLIENT_ID: "b41a6fbb-c728-4e03-aa59-d25b0fd383b6", - AUTHORITY: "https://login.microsoftonline.com/TenantId", - ALTERNATE_AUTHORITY: "https://login.microsoftonline.com/alternate", - REDIRECT_URI: "http://localhost:8080", - DEFAULT_GRAPH_SCOPE: ["user.read"], - AUTHORIZATION_CODE: "0.ASgAqPq4kJXMDkamGO53C-4XWVm3ypmrKgtCkdhePY1PBjsoAJg.AQABAAIAAAAm-06blBE1TpVMil8KPQ41DOje1jDj1oK3KxTXGKg89VjLYJi71gx_npOoxVfC7X49MqOX7IltTJOilUId-IAHndHXlfWzoSGq3GUmwAOLMisftceBRtq3YBsvHX7giiuSZXJgpgu03uf3V2h5Z3GJNpnSXT1f7iVFuRvGh1-jqjWxKs2un8AS5rhti1ym1zxkeicKT43va5jQeHVUlTQo69llnwQJ3iKmKLDVq_Q25Au4EQjYaeEx6TP5IZSqPPm7x0bynmjE8cqR5r4ySP4wH8fjnxlLySrUEZObk2VgREB1AdH6-xKIa04EnJEj9dUgTwiFvQumkuHHetFOgH7ep_9diFOdAOQLUK8C9N4Prlj0JiOcgn6l0xYd5Q9691Ylw8UfifLwq_B7f30mMLN64_XgoBY9K9CR1L4EC1kPPwIhVv3m6xmbhXZ3efx-A-bbV2SYcO4D4ZlnQztHzie_GUlredtsdEMAOE3-jaMJs7i2yYMuIEEtRcHIjV_WscVooCDdKmVncHOObWhNUSdULAejBr3pFs0v3QO_xZ269eLu5Z0qHzCZ_EPg2aL-ERz-rpgdclQ_H_KnEtMsC4F1RgAnDjVmSRKJZZdnNLfKSX_Wd40t_nuo4kjN2cSt8QzzeL533zIZ4CxthOsC4HH2RcUZDIgHdLDLT2ukg-Osc6J9URpZP-IUpdjXg_uwbkHEjrXDMBMo2pmCqaWbMJKo5Lr7CrystifnDITXzZmmOah8HV83Xyb6EP8Gno6JRuaG80j8BKDWyb1Yof4rnLI1kZ59n_t2d0LnRBXz50PdWCWX6vtkg-kAV-bGJQr45XDSKBSv0Q_fVsdLMk24NacUZcF5ujUtqv__Bv-wATzCHWlbUDGHC8nHEi84PcYAjSsgAA", - REFRESH_TOKEN: "thisIsARefreshT0ken", - AUTH_CODE_URL: "https://login.microsoftonline.com/TenantId/oauth2.0/v2.0/authorize?client_id=b41a6fbb-c728-4e03-aa59-d25b0fd383b6&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%2F8080%2F&response_mode=query&scope=user.read%20openid%20profile%20offline_access", - CACHE_LOCATION: "Test", + CLIENT_ID: 'b41a6fbb-c728-4e03-aa59-d25b0fd383b6', + AUTHORITY: 'https://login.microsoftonline.com/TenantId', + ALTERNATE_AUTHORITY: 'https://login.microsoftonline.com/alternate', + REDIRECT_URI: 'http://localhost:8080', + DEFAULT_GRAPH_SCOPE: ['user.read'], + AUTHORIZATION_CODE: + '0.ASgAqPq4kJXMDkamGO53C-4XWVm3ypmrKgtCkdhePY1PBjsoAJg.AQABAAIAAAAm-06blBE1TpVMil8KPQ41DOje1jDj1oK3KxTXGKg89VjLYJi71gx_npOoxVfC7X49MqOX7IltTJOilUId-IAHndHXlfWzoSGq3GUmwAOLMisftceBRtq3YBsvHX7giiuSZXJgpgu03uf3V2h5Z3GJNpnSXT1f7iVFuRvGh1-jqjWxKs2un8AS5rhti1ym1zxkeicKT43va5jQeHVUlTQo69llnwQJ3iKmKLDVq_Q25Au4EQjYaeEx6TP5IZSqPPm7x0bynmjE8cqR5r4ySP4wH8fjnxlLySrUEZObk2VgREB1AdH6-xKIa04EnJEj9dUgTwiFvQumkuHHetFOgH7ep_9diFOdAOQLUK8C9N4Prlj0JiOcgn6l0xYd5Q9691Ylw8UfifLwq_B7f30mMLN64_XgoBY9K9CR1L4EC1kPPwIhVv3m6xmbhXZ3efx-A-bbV2SYcO4D4ZlnQztHzie_GUlredtsdEMAOE3-jaMJs7i2yYMuIEEtRcHIjV_WscVooCDdKmVncHOObWhNUSdULAejBr3pFs0v3QO_xZ269eLu5Z0qHzCZ_EPg2aL-ERz-rpgdclQ_H_KnEtMsC4F1RgAnDjVmSRKJZZdnNLfKSX_Wd40t_nuo4kjN2cSt8QzzeL533zIZ4CxthOsC4HH2RcUZDIgHdLDLT2ukg-Osc6J9URpZP-IUpdjXg_uwbkHEjrXDMBMo2pmCqaWbMJKo5Lr7CrystifnDITXzZmmOah8HV83Xyb6EP8Gno6JRuaG80j8BKDWyb1Yof4rnLI1kZ59n_t2d0LnRBXz50PdWCWX6vtkg-kAV-bGJQr45XDSKBSv0Q_fVsdLMk24NacUZcF5ujUtqv__Bv-wATzCHWlbUDGHC8nHEi84PcYAjSsgAA', + REFRESH_TOKEN: 'thisIsARefreshT0ken', + AUTH_CODE_URL: + 'https://login.microsoftonline.com/TenantId/oauth2.0/v2.0/authorize?client_id=b41a6fbb-c728-4e03-aa59-d25b0fd383b6&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%2F8080%2F&response_mode=query&scope=user.read%20openid%20profile%20offline_access', + CACHE_LOCATION: 'Test', }; export const AUTHENTICATION_RESULT = { diff --git a/samples/VanillaJSTestApp2.0/app/default/auth.js b/samples/VanillaJSTestApp2.0/app/default/auth.js index c855c9562d..297d2a5329 100644 --- a/samples/VanillaJSTestApp2.0/app/default/auth.js +++ b/samples/VanillaJSTestApp2.0/app/default/auth.js @@ -12,7 +12,7 @@ let signInType; // Create the main myMSALObj instance // configuration parameters are located at authConfig.js -const myMSALObj = new msal.PublicClientApplication(msalConfig); +const myMSALObj = new msal.PublicClientApplication(msalConfig); // Register Callbacks for Redirect flow myMSALObj.handleRedirectCallback(authRedirectCallBack); diff --git a/samples/msal-node-auth-code/data/cache.json b/samples/msal-node-auth-code/data/cache.json new file mode 100644 index 0000000000..d3fdd20f5b --- /dev/null +++ b/samples/msal-node-auth-code/data/cache.json @@ -0,0 +1,73 @@ +{ + "Account": { + "uid.utid-login.microsoftonline.com-microsoft": { + "username": "John Doe", + "local_account_id": "object1234", + "realm": "microsoft", + "environment": "login.microsoftonline.com", + "home_account_id": "uid.utid", + "authority_type": "MSSTS", + "client_info": "base64encodedjson" + } + }, + "RefreshToken": { + "uid.utid-login.microsoftonline.com-refreshtoken-mock_client_id--": { + "environment": "login.microsoftonline.com", + "credential_type": "RefreshToken", + "secret": "a refresh token", + "client_id": "mock_client_id", + "home_account_id": "uid.utid" + }, + "uid.utid-login.microsoftonline.com-refreshtoken-1--": { + "environment": "login.microsoftonline.com", + "credential_type": "RefreshToken", + "secret": "a refresh token", + "client_id": "mock_client_id", + "home_account_id": "uid.utid", + "familyId": "1" + } + }, + "AccessToken": { + "uid.utid-login.microsoftonline.com-accesstoken-mock_client_id-microsoft-scope1 scope2 scope3": { + "environment": "login.microsoftonline.com", + "credential_type": "AccessToken", + "secret": "an access token", + "realm": "microsoft", + "target": "scope1 scope2 scope3", + "client_id": "mock_client_id", + "cached_at": "1000", + "home_account_id": "uid.utid", + "extended_expires_on": "4600", + "expires_on": "4600" + }, + "uid.utid-login.microsoftonline.com-accesstoken-mock_client_id-microsoft-scope4 scope5": { + "environment": "login.microsoftonline.com", + "credential_type": "AccessToken", + "secret": "an access token", + "realm": "microsoft", + "target": "scope4 scope5", + "client_id": "mock_client_id", + "cached_at": "1000", + "home_account_id": "uid.utid", + "extended_expires_on": "4600", + "expires_on": "4600" + } + }, + "IdToken": { + "uid.utid-login.microsoftonline.com-idtoken-mock_client_id-microsoft-": { + "realm": "microsoft", + "environment": "login.microsoftonline.com", + "credential_type": "IdToken", + "secret": "header.eyJvaWQiOiAib2JqZWN0MTIzNCIsICJwcmVmZXJyZWRfdXNlcm5hbWUiOiAiSm9obiBEb2UiLCAic3ViIjogInN1YiJ9.signature", + "client_id": "mock_client_id", + "home_account_id": "uid.utid" + } + }, + "AppMetadata": { + "appmetadata-login.microsoftonline.com-mock_client_id": { + "environment": "login.microsoftonline.com", + "family_id": "1", + "client_id": "mock_client_id" + } + } +} diff --git a/samples/msal-node-auth-code/index.js b/samples/msal-node-auth-code/index.js index 93b2c9bc96..93c44fc212 100644 --- a/samples/msal-node-auth-code/index.js +++ b/samples/msal-node-auth-code/index.js @@ -4,6 +4,8 @@ */ const express = require("express"); const msal = require('@azure/msal-node'); +const myLocalCache = require("./data/cache"); +const fs = require("fs"); const SERVER_PORT = process.env.PORT || 3000; @@ -17,10 +19,11 @@ const publicClientConfig = { }, cache: { cacheLocation: "fileCache", // This configures where your cache will be stored - storeAuthStateInCookie: false // Set this to "true" if you are having issues on IE11 or Edge - } + storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge + }, }; const pca = new msal.PublicClientApplication(publicClientConfig); +pca.initializeCache(myLocalCache); // Create Express App and Routes const app = express(); @@ -49,12 +52,14 @@ app.get('/redirect', (req, res) => { }; pca.acquireTokenByCode(tokenRequest).then((response) => { - console.log(JSON.stringify(response)); - res.status(200).json(response); + console.log("\nResponse: \n:", response); + res.send(200); + // uncomment this to show writing of cache, dont commit real tokens. + // fs.writeFileSync("./data/cache.json", JSON.stringify(pca.readCache()), null, 4); }).catch((error) => { - console.log(JSON.stringify(error.response)); - res.status(500).send(JSON.stringify(error.response)); - }) + console.log(error); + res.status(500).send(error); + }); }); app.listen(SERVER_PORT, () => console.log(`Msal Node Auth Code Sample app listening on port ${SERVER_PORT}!`)) diff --git a/samples/msal-node-auth-code/package.json b/samples/msal-node-auth-code/package.json index 53f1c4aea9..07ec38695e 100644 --- a/samples/msal-node-auth-code/package.json +++ b/samples/msal-node-auth-code/package.json @@ -8,7 +8,7 @@ "build:package": "cd ../../lib/msal-common && npm run build && cd ../msal-node && npm run build", "start:build": "npm run build:package && npm start" }, - "author": "sameerag", + "author": "Microsoft", "license": "MIT", "dependencies": { "@azure/msal-node": "^0.1.0",