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..348562f44f 100644 --- a/lib/msal-common/src/cache/ICacheStorage.ts +++ b/lib/msal-common/src/cache/ICacheStorage.ts @@ -7,31 +7,49 @@ * 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 + */ + getSerializedCache(): Promise; + + /** + * Function to write serialized Cache to disk + * @param cache + */ + setSerializedCache(cache: string): 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 c7c50d3f2b..f8d447089d 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,13 +47,29 @@ export class AuthorizationCodeClient extends BaseClient { * API to acquire a token in exchange of 'authorization_code` acquired by the user in the first leg of the authorization_code_grant * @param request */ - async acquireToken(request: AuthorizationCodeRequest): Promise { + async acquireToken(request: AuthorizationCodeRequest): Promise { this.logger.info("in acquireToken call"); + const authority: Authority = await this.createAuthority(request && request.authority); const response = await this.executeTokenRequest(authority, 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, + authority + ); + + // set the final cache and return the auth response + this.setCache(); + return tokenResponse; } /** diff --git a/lib/msal-common/src/client/BaseClient.ts b/lib/msal-common/src/client/BaseClient.ts index 7dfb685a7a..702069caec 100644 --- a/lib/msal-common/src/client/BaseClient.ts +++ b/lib/msal-common/src/client/BaseClient.ts @@ -11,10 +11,12 @@ import { Account } from "../account/Account"; import { Authority } from "../authority/Authority"; import { Logger } from "../logger/Logger"; import { AuthorityFactory } from "../authority/AuthorityFactory"; -import {AADServerParamKeys, Constants, HeaderNames} from "../utils/Constants"; -import {ClientAuthError} from "../error/ClientAuthError"; -import {NetworkResponse} from "../network/NetworkManager"; -import {ServerAuthorizationTokenResponse} from "../server/ServerAuthorizationTokenResponse"; +import { AADServerParamKeys, Constants, HeaderNames } from "../utils/Constants"; +import { ClientAuthError } from "../error/ClientAuthError"; +import { NetworkResponse } from "../network/NetworkManager"; +import { ServerAuthorizationTokenResponse } from "../server/ServerAuthorizationTokenResponse"; +import { UnifiedCacheManager } from "../unifiedCache/UnifiedCacheManager"; +import { Serializer } from "../unifiedCache/serialize/Serializer"; /** * Base application class which will construct requests to send to and handle responses from the Microsoft STS using the authorization code flow. @@ -37,7 +39,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; @@ -59,7 +64,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; @@ -131,4 +139,13 @@ export abstract class BaseClient { headers: headers, }); } + + /** + * Set the cache post acquireToken call + */ + protected setCache(): void { + const inMemCache = this.unifiedCacheManager.getCacheInMemory(); + const cache = this.unifiedCacheManager.generateJsonCache(inMemCache); + this.cacheStorage.setSerializedCache(Serializer.serializeJSONBlob(cache)); + } } diff --git a/lib/msal-common/src/client/SPAClient.ts b/lib/msal-common/src/client/SPAClient.ts index 8eaf35ddaa..3a6052a00c 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"; @@ -101,7 +101,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); @@ -123,7 +123,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; } } @@ -171,7 +171,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; } @@ -233,7 +233,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; @@ -243,7 +243,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; } @@ -261,7 +261,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. @@ -298,7 +298,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(); @@ -315,7 +315,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 || ""); } /** @@ -329,7 +329,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; } @@ -348,7 +348,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()); } @@ -388,7 +388,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 @@ -400,9 +400,9 @@ export class SPAClient extends BaseClient { /** * Creates refreshToken request and sends to given token endpoint. - * @param refreshTokenRequest - * @param tokenEndpoint - * @param refreshToken + * @param refreshTokenRequest + * @param tokenEndpoint + * @param refreshToken */ private async renewToken(refreshTokenRequest: TokenRenewParameters, tokenEndpoint: string, refreshToken: string): Promise { // Initialize request parameters. diff --git a/lib/msal-common/src/config/ClientConfiguration.ts b/lib/msal-common/src/config/ClientConfiguration.ts index 3f8661efb4..4027bf32d3 100644 --- a/lib/msal-common/src/config/ClientConfiguration.ts +++ b/lib/msal-common/src/config/ClientConfiguration.ts @@ -134,6 +134,14 @@ const DEFAULT_STORAGE_IMPLEMENTATION: ICacheStorage = { setItem: () => { const notImplErr = "Storage interface - setItem() has not been implemented for the cacheStorage interface."; throw AuthError.createUnexpectedError(notImplErr); + }, + getSerializedCache: (): Promise => { + const notImplErr = "Storage interface - getSerializedCache() has not been implemented for the cacheStorage interface."; + throw AuthError.createUnexpectedError(notImplErr); + }, + setSerializedCache: () => { + const notImplErr = "Storage interface - setSerializedCache() 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 b19defbfef..ad7cf1cf59 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 { AuthOptions, SystemOptions, LoggerOptions, TelemetryOptions } from "./config/ClientConfiguration"; @@ -13,6 +13,9 @@ export { Authority } from "./authority/Authority"; 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"; // Network Interface export { INetworkModule, NetworkRequestOptions } from "./network/INetworkModule"; export { NetworkResponse } from "./network/NetworkManager"; @@ -43,3 +46,4 @@ export { ClientConfigurationError, ClientConfigurationErrorMessage } from "./err // Constants and Utils export { Constants, PromptValue, TemporaryCacheKeys, PersistentCacheKeys } 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 d62c409bc4..a0174cea34 100644 --- a/lib/msal-common/src/request/AuthorizationCodeUrlRequest.ts +++ b/lib/msal-common/src/request/AuthorizationCodeUrlRequest.ts @@ -67,6 +67,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 d315ff9882..678b6937cb 100644 --- a/lib/msal-common/src/response/ResponseHandler.ts +++ b/lib/msal-common/src/response/ResponseHandler.ts @@ -2,121 +2,65 @@ * 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"; +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"; /** * Class that handles response parsing. */ 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(); } // Check for error if (serverResponseHash.error || serverResponseHash.error_description) { - if (InteractionRequiredAuthError.isInteractionRequiredError(serverResponseHash.error, serverResponseHash.error_description)) { - throw new InteractionRequiredAuthError(serverResponseHash.error, serverResponseHash.error_description); - } - - throw new ServerError(serverResponseHash.error, serverResponseHash.error_description); + throw new ServerError( + serverResponseHash.error, + serverResponseHash.error_description + ); } if (serverResponseHash.client_info) { @@ -128,169 +72,162 @@ 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) { 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); - } + 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 - // 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 || ""); + const authenticationResult = this.processTokenResponse(serverTokenResponse, authority); - // 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) - }; - } + const environment = authority.canonicalAuthorityUrlComponents.HostNameAndPort; + this.addCredentialsToCache(authenticationResult, environment, serverTokenResponse.refresh_token); - /** - * 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; - } + return authenticationResult; } /** - * Returns a constructed token response based on given string. Also manages the cache updates and cleanups. - * @param serverTokenResponse - * @param authorityString - * @param resource - * @param state + * 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) */ - 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 + ) { + 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..ebcf292480 --- /dev/null +++ b/lib/msal-common/src/response/SPAResponseHandler.ts @@ -0,0 +1,296 @@ +/* + * 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; + + 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 + */ + private validateServerAuthorizationCodeResponse(serverResponseHash: ServerAuthorizationCodeResponse, cachedState: string, cryptoObj: ICrypto): void { + if (serverResponseHash.state !== cachedState) { + throw ClientAuthError.createStateMismatchError(); + } + + // Check for error + if (serverResponseHash.error || serverResponseHash.error_description) { + if (InteractionRequiredAuthError.isInteractionRequiredError(serverResponseHash.error, serverResponseHash.error_description)) { + throw new InteractionRequiredAuthError(serverResponseHash.error, serverResponseHash.error_description); + } + + throw new ServerError(serverResponseHash.error, serverResponseHash.error_description); + } + + 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) { + 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); + } + } + + /** + * 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/ServerAuthorizationTokenResponse.ts b/lib/msal-common/src/server/ServerAuthorizationTokenResponse.ts index 769d871fe6..60aebaa2e6 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..3d401212e3 --- /dev/null +++ b/lib/msal-common/src/unifiedCache/UnifiedCacheManager.ts @@ -0,0 +1,112 @@ +/* + * 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.readSerializedCache(); + } + + async readSerializedCache(): Promise { + const serializedCache = await this.cacheStorage.getSerializedCache(); + this.inMemoryCache = this.generateInMemoryCache(serializedCache); + } + + /** + * 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 index 86b39296c7..586316bf19 100644 --- a/lib/msal-common/src/unifiedCache/entities/AccessTokenEntity.ts +++ b/lib/msal-common/src/unifiedCache/entities/AccessTokenEntity.ts @@ -5,6 +5,7 @@ import { Credential } from "./Credential"; import { Separators } from "../../utils/Constants"; +import { AuthenticationResult } from "../../response/AuthenticationResult"; /** * ACCESS_TOKEN Credential Type @@ -34,4 +35,45 @@ export class AccessTokenEntity extends Credential { 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 index 0623b2d01b..0515fb3b76 100644 --- a/lib/msal-common/src/unifiedCache/entities/AccountEntity.ts +++ b/lib/msal-common/src/unifiedCache/entities/AccountEntity.ts @@ -46,7 +46,7 @@ export class AccountEntity { * @param policy */ static createAccount(clientInfo: string, authority: Authority, idToken: IdToken, policy: string, crypto: ICrypto): AccountEntity { - let account: AccountEntity; + const account: AccountEntity = new AccountEntity(); account.authorityType = CacheAccountType.MSSTS_ACCOUNT_TYPE; account.clientInfo = clientInfo; @@ -80,7 +80,7 @@ export class AccountEntity { * @param idToken */ static createADFSAccount(authority: Authority, idToken: IdToken): AccountEntity { - let account: AccountEntity; + const account: AccountEntity = new AccountEntity(); account.authorityType = CacheAccountType.ADFS_ACCOUNT_TYPE; account.homeAccountId = idToken.claims.sub; diff --git a/lib/msal-common/src/unifiedCache/entities/IdTokenEntity.ts b/lib/msal-common/src/unifiedCache/entities/IdTokenEntity.ts index 999929016d..fd674ac5d5 100644 --- a/lib/msal-common/src/unifiedCache/entities/IdTokenEntity.ts +++ b/lib/msal-common/src/unifiedCache/entities/IdTokenEntity.ts @@ -5,6 +5,7 @@ import { Credential } from "./Credential"; import { Separators } from "../../utils/Constants"; +import { AuthenticationResult } from "../../response/AuthenticationResult"; /** * ID_TOKEN Cache @@ -27,4 +28,29 @@ export class IdTokenEntity extends Credential { 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 index 0e9bb89c4f..ce81f4d98b 100644 --- a/lib/msal-common/src/unifiedCache/entities/RefreshTokenEntity.ts +++ b/lib/msal-common/src/unifiedCache/entities/RefreshTokenEntity.ts @@ -5,6 +5,7 @@ import { Credential } from "./Credential"; import { Separators } from "../../utils/Constants"; +import { AuthenticationResult } from "../../response/AuthenticationResult"; /** * REFRESH_TOKEN Cache @@ -31,4 +32,32 @@ export class RefreshTokenEntity extends Credential { 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 index 79c44af468..b26fb67182 100644 --- a/lib/msal-common/src/unifiedCache/serialize/Deserializer.ts +++ b/lib/msal-common/src/unifiedCache/serialize/Deserializer.ts @@ -9,26 +9,13 @@ 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 { AccountCacheMaps, IdTokenCacheMaps, AccessTokenCacheMaps, RefreshTokenCacheMaps, AppMetadataCacheMaps } from "../serialize/JsonKeys"; +import { AccountCache, IdTokenCache, AccessTokenCache, RefreshTokenCache, AppMetadataCache, InMemoryCache, JsonCache } from "../utils/CacheTypes"; import { StringDict } from "../../utils/MsalTypes"; // TODO: Can we write this with Generics? export class Deserializer { + /** * Parse the JSON blob in memory and deserialize the content * @param cachedJson diff --git a/lib/msal-common/src/unifiedCache/serialize/Serializer.ts b/lib/msal-common/src/unifiedCache/serialize/Serializer.ts index fe5702532a..a4b5215402 100644 --- a/lib/msal-common/src/unifiedCache/serialize/Serializer.ts +++ b/lib/msal-common/src/unifiedCache/serialize/Serializer.ts @@ -6,8 +6,10 @@ 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 @@ -20,7 +22,7 @@ export class Serializer { * Serialize Accounts * @param accCache */ - static serializeAccounts(accCache: AccountCache) { + static serializeAccounts(accCache: AccountCache): StringDict { const accounts = {}; Object.keys(accCache).map(function (key) { const mappedAcc = CacheHelper.renameKeys( @@ -37,7 +39,7 @@ export class Serializer { * Serialize IdTokens * @param idTCache */ - static serializeIdTokens(idTCache: IdTokenCache) { + static serializeIdTokens(idTCache: IdTokenCache): StringDict{ const idTokens = {}; Object.keys(idTCache).map(function (key) { const mappedIdT = CacheHelper.renameKeys( @@ -54,7 +56,7 @@ export class Serializer { * Serializes AccessTokens * @param atCache */ - static serializeAccessTokens(atCache: AccessTokenCache) { + static serializeAccessTokens(atCache: AccessTokenCache): StringDict { // access tokens const accessTokens = {}; Object.keys(atCache).map(function (key) { @@ -72,7 +74,7 @@ export class Serializer { * Serialize refreshTokens * @param rtCache */ - static serializeRefreshTokens(rtCache: RefreshTokenCache) { + static serializeRefreshTokens(rtCache: RefreshTokenCache): StringDict{ const refreshTokens = {}; Object.keys(rtCache).map(function (key) { const mappedRT = CacheHelper.renameKeys( @@ -89,7 +91,7 @@ export class Serializer { * Serialize amdtCache * @param amdtCache */ - static serializeAppMetadata(amdtCache: AppMetadataCache) { + static serializeAppMetadata(amdtCache: AppMetadataCache): StringDict { const appMetadata = {}; Object.keys(amdtCache).map(function (key) { const mappedAmdt = CacheHelper.renameKeys( diff --git a/lib/msal-common/src/unifiedCache/utils/CacheHelper.ts b/lib/msal-common/src/unifiedCache/utils/CacheHelper.ts index 0c548eba25..2cd6f06764 100644 --- a/lib/msal-common/src/unifiedCache/utils/CacheHelper.ts +++ b/lib/msal-common/src/unifiedCache/utils/CacheHelper.ts @@ -3,15 +3,13 @@ * Licensed under the MIT License. */ -import { StringDict } from "../../utils/MsalTypes"; - export class CacheHelper { /** * Helper to convert serialized data to object * @param obj * @param json */ - static toObject(obj: T, json: StringDict): T { + static toObject(obj: T, json: object): T { for (const propertyName in json) { obj[propertyName] = json[propertyName]; } @@ -22,7 +20,7 @@ export class CacheHelper { * helper function to swap keys and objects * @param cacheMap */ - static swap(cacheMap: object) { + static swap(cacheMap: object): object { const ret = {}; for (const key in cacheMap) { ret[cacheMap[key]] = key; @@ -35,7 +33,7 @@ export class CacheHelper { * @param objAT * @param keysMap */ - static renameKeys(objAT: Object, keysMap: Object) { + static renameKeys(objAT: Object, keysMap: Object): object { const keyValues = Object.keys(objAT).map((key) => { if (objAT[key]) { const newKey = keysMap[key] || key; diff --git a/lib/msal-common/src/utils/Constants.ts b/lib/msal-common/src/utils/Constants.ts index 7df1fff205..e520e16571 100644 --- a/lib/msal-common/src/utils/Constants.ts +++ b/lib/msal-common/src/utils/Constants.ts @@ -256,5 +256,19 @@ export enum CacheEntity { 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 c06e140fef..a918763b7a 100644 --- a/lib/msal-common/src/utils/MsalTypes.ts +++ b/lib/msal-common/src/utils/MsalTypes.ts @@ -7,4 +7,3 @@ * Key-Value type to support queryParams, extraQueryParams and claims */ export type StringDict = { [key: string]: string }; - diff --git a/lib/msal-common/test/client/BaseClient.spec.ts b/lib/msal-common/test/client/BaseClient.spec.ts index dbf3bd3f80..4ad5621359 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/response/ResponseHandler.spec.ts b/lib/msal-common/test/response/SPAResponseHandler.spec.ts similarity index 90% rename from lib/msal-common/test/response/ResponseHandler.spec.ts rename to lib/msal-common/test/response/SPAResponseHandler.spec.ts index a88274ed4a..eb2ab313cf 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 } fro 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; }); @@ -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,22 +279,22 @@ 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); }); }); 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, @@ -309,7 +309,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, @@ -325,7 +325,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, @@ -337,7 +337,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", @@ -372,7 +372,6 @@ describe("ResponseHandler.ts Class Unit Tests", () => { return input; } }; - const testAccount2: Account = { ...testAccount, accountIdentifier: RANDOM_TEST_GUID, @@ -429,9 +428,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)); @@ -466,7 +465,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..0e28aa23d2 --- /dev/null +++ b/lib/msal-common/test/unifiedCache/UnifiedCacheManager.spec.ts @@ -0,0 +1,41 @@ +import { expect } from "chai"; +import { UnifiedCacheManager } from "../../src/unifiedCache/UnifiedCacheManager"; +import { CacheContent } from "../../src/unifiedCache/serialize/CacheInterface"; +import { mockCache } from "./entities/cacheConstants"; +import { CacheEntity } from "../../src/utils/Constants"; + +const cachedJson = require("./serialize/cache.json"); + +describe("UnifiedCacheManager test cases", () => { + + let unifiedCacheManager = new UnifiedCacheManager(cachedJson); + + it("initCache", () => { + + // create mock AccessToken + const atOne = mockCache.createMockATOne(); + const atOneKey = atOne.generateAccessTokenEntityKey(); + const atTwo = mockCache.createMockATTwo(); + const atTwoKey = atTwo.generateAccessTokenEntityKey(); + + expect(Object.keys(unifiedCacheManager.inMemoryCache.accessTokens).length).to.equal(2); + expect(unifiedCacheManager.inMemoryCache.accessTokens[atOneKey]).to.eql(atOne); + expect(unifiedCacheManager.inMemoryCache.accessTokens[atTwoKey]).to.eql(atTwo); + }); + + it("getAccount", () => { + + // 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.undefined; + }); + +}); diff --git a/lib/msal-common/test/unifiedCache/serialize/CacheInterface.spec.ts b/lib/msal-common/test/unifiedCache/serialize/CacheInterface.spec.ts new file mode 100644 index 0000000000..5fc109b45b --- /dev/null +++ b/lib/msal-common/test/unifiedCache/serialize/CacheInterface.spec.ts @@ -0,0 +1,100 @@ +import { expect } from "chai"; +import { CacheInterface } from "../../../src/unifiedCache/serialize/CacheInterface"; +import { mockCache } from "../entities/cacheConstants"; + +const cachedJson = require("./cache.json"); +const accountJson = require("./Account.json"); + +describe("CacheInterface test cases", () => { + + const jsonContent = CacheInterface.deserializeJSONBlob(cachedJson); + + it("serializeJSONBlob", () => { + const json = CacheInterface.serializeJSONBlob(cachedJson); + expect(JSON.parse(json)).to.eql(cachedJson); + }); + + 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: "base64encodedjson" + } + }; + const acc = CacheInterface.deserializeJSONBlob(accountJson); + expect(acc.accounts).to.eql(mockAccount); + }); + + it("retrieve empty JSON blob", () => { + const emptyCacheJson = {}; + const emptyJsonContent = CacheInterface.deserializeJSONBlob(emptyCacheJson); + + expect(emptyJsonContent.accounts).to.eql({}); + expect(emptyJsonContent.accessTokens).to.eql({}); + expect(emptyJsonContent.idTokens).to.eql({}); + expect(emptyJsonContent.refreshTokens).to.eql({}); + expect(emptyJsonContent.appMetadata).to.eql({}); + }); + + it("generateAccessTokenCache", () => { + // create mock AccessToken + const atOne = mockCache.createMockATOne(); + const atOneKey = atOne.generateAccessTokenEntityKey(); + const atTwo = mockCache.createMockATTwo(); + const atTwoKey = atTwo.generateAccessTokenEntityKey(); + + // deserialize the AccessToken from memory and Test equivalency with the generated mock AccessToken + const accessTokens = CacheInterface.generateAccessTokenCache(jsonContent.accessTokens); + expect(accessTokens[atOneKey]).to.eql(atOne); + expect(accessTokens[atTwoKey]).to.eql(atTwo); + }); + + it("generateIdTokenCache", () => { + // create mock IdToken + const idt = mockCache.createMockIdT(); + const idTKey = idt.generateIdTokenEntityKey(); + + // deserialize the IdToken from memory and Test equivalency with the generated mock IdToken + const idTokens = CacheInterface.generateIdTokenCache(jsonContent.idTokens); + expect(idTokens[idTKey]).to.eql(idt); + }); + + it("generateRefreshTokenCache", () => { + // create mock Refresh Token + const rt = mockCache.createMockRT(); + const rtKey = rt.generateRefreshTokenEntityKey(); + + const rtF = mockCache.createMockRTWithFamilyId(); + const rtFKey = rtF.generateRefreshTokenEntityKey(); + + // deserialize the RefreshToken from memory and Test equivalency with the generated mock Refresh Token + const refreshTokens = CacheInterface.generateRefreshTokenCache(jsonContent.refreshTokens); + expect(refreshTokens[rtKey]).to.eql(rt); + expect(refreshTokens[rtFKey]).to.eql(rtF); + }); + + it("generateAccountCache", () => { + // create mock Account + const acc = mockCache.createMockAcc(); + const accKey = acc.generateAccountEntityKey(); + + // deserialize the Account from memory and Test equivalency with the generated mock Account + const accounts = CacheInterface.generateAccountCache(jsonContent.accounts); + expect(accounts[accKey]).to.eql(acc); + }); + + it("generateAppMetadataCache", () => { + // create mock AppMetadata + const amdt = mockCache.createMockAmdt(); + const amdtKey = amdt.generateAppMetaDataEntityKey(); + + // deserialize the AppMetadata from memory and Test equivalency with the generated mock AppMetadata + const appMetadata = CacheInterface.generateAppMetadataCache(jsonContent.appMetadata); + expect(appMetadata[amdtKey]).to.eql(amdt); + }); +}); diff --git a/lib/msal-common/test/utils/StringConstants.ts b/lib/msal-common/test/utils/StringConstants.ts index 6160a45b62..26bc34d787 100644 --- a/lib/msal-common/test/utils/StringConstants.ts +++ b/lib/msal-common/test/utils/StringConstants.ts @@ -197,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 09a3a3d7b4..d3fd70f917 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,22 +5476,18 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true + "version": "0.0.8", + "bundled": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -5518,9 +5506,16 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + } } }, "ms": { @@ -5599,8 +5594,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -5612,7 +5606,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -5664,7 +5657,7 @@ }, "dependencies": { "minimist": { - "version": "1.2.5", + "version": "1.2.0", "bundled": true, "dev": true, "optional": true @@ -5698,8 +5691,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -5735,7 +5727,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", @@ -5755,7 +5746,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5799,14 +5789,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 } } }, @@ -9028,12 +9016,6 @@ "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=", "dev": true }, - "serialize-javascript": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz", - "integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==", - "dev": true - }, "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -10295,6 +10277,12 @@ "ajv-keywords": "^3.1.0" } }, + "serialize-javascript": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.9.1.tgz", + "integrity": "sha512-0Vb/54WJ6k5v8sSWN09S0ora+Hnr+cX40r9F170nT+mSkaxltoE/7R3OrIdBSUv1OoiobH1QoWQbCnAO+e8J1A==", + "dev": true + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 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-lock.json b/lib/msal-node/package-lock.json index 7e78a9b5f4..bb052d105b 100644 --- a/lib/msal-node/package-lock.json +++ b/lib/msal-node/package-lock.json @@ -4655,8 +4655,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -4677,14 +4676,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" @@ -4699,20 +4696,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", @@ -4829,8 +4823,7 @@ "inherits": { "version": "2.0.4", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -4842,7 +4835,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4857,7 +4849,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -4865,14 +4856,12 @@ "minimist": { "version": "1.2.5", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.9.0", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -4891,7 +4880,6 @@ "version": "0.5.3", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "^1.2.5" } @@ -4953,8 +4941,7 @@ "npm-normalize-package-bin": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "npm-packlist": { "version": "1.4.8", @@ -4982,8 +4969,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -4995,7 +4981,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -5073,8 +5058,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -5110,7 +5094,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", @@ -5130,7 +5113,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5174,14 +5156,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, diff --git a/lib/msal-node/package.json b/lib/msal-node/package.json index 202e051390..37594eca5b 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/CacheManager.ts b/lib/msal-node/src/cache/CacheManager.ts new file mode 100644 index 0000000000..ecab815228 --- /dev/null +++ b/lib/msal-node/src/cache/CacheManager.ts @@ -0,0 +1,25 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { promises as fs } from 'fs'; + +export class CacheManager { + + /** + * Read contents of the cache blob to in memoryCache + * @param cachePath + */ + async readFromFile(cacheLocation: string): Promise { + return await fs.readFile(cacheLocation, 'utf8'); + } + + /** + * Create the JSON file + * @param jsonContent + */ + async writeToFile(cachePath: string, cache: string) { + await fs.writeFile(cachePath, cache, 'utf8'); + } +} diff --git a/lib/msal-node/src/cache/Storage.ts b/lib/msal-node/src/cache/Storage.ts index 641290261d..257bb659d5 100644 --- a/lib/msal-node/src/cache/Storage.ts +++ b/lib/msal-node/src/cache/Storage.ts @@ -2,92 +2,57 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ -import { - ICacheStorage, - Constants, - PersistentCacheKeys, - TemporaryCacheKeys, -} from '@azure/msal-common'; +import { ICacheStorage } from '@azure/msal-common'; +import { CacheManager } from './CacheManager'; 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 cachePath: string; + private cacheManager: CacheManager; - constructor(clientId: string, cacheConfig: CacheOptions) { + constructor(cacheConfig: CacheOptions) { this.cacheConfig = cacheConfig; - this.fileStorage = this.initializeFileStorage( - this.cacheConfig.cacheLocation! - ); - this.clientId = clientId; + this.cachePath = this.cacheConfig.cacheLocation!; + this.cacheManager = new CacheManager(); } - private initializeFileStorage(cacheLocation: string) { - if (cacheLocation === CACHE.FILE_CACHE) { - fs.open('NodeCache.txt', 'w', (err: Error) => { - if (err) throw err; - }); - - return fs; - } + /** + * retrieve the file Path to read the cache from + */ + getCachePath(): string { + return this.cachePath; } /** - * 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 + * read JSON formatted cache from disk */ - 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}`; - } + async getSerializedCache(): Promise { + const serializedCache = await this.cacheManager.readFromFile(this.cachePath); + return serializedCache; } /** - * Parses key as JSON object, JSON.parse() will throw an error. - * @param key + * write the JSON formatted cache to disk + * @param jsonCache */ - private validateObjectKey(key: string): void { - JSON.parse(key); + async setSerializedCache(cache: string): Promise { + this.cacheManager.writeToFile(this.cachePath, cache); } /** * 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 +60,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 748a1006c0..4b16c37993 100644 --- a/lib/msal-node/src/client/ClientApplication.ts +++ b/lib/msal-node/src/client/ClientApplication.ts @@ -9,13 +9,14 @@ import { AuthorizationCodeRequest, ClientConfiguration, RefreshTokenClient, - RefreshTokenRequest + RefreshTokenRequest, + AuthenticationResult } 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 } from "./../utils/Constants"; +import { Constants } from './../utils/Constants'; export abstract class ClientApplication { @@ -76,7 +77,7 @@ export abstract class ClientApplication { */ async acquireTokenByCode( request: AuthorizationCodeRequest - ): Promise { + ): Promise { const authorizationCodeClient = new AuthorizationCodeClient( this.buildOauthClientConfiguration() ); @@ -108,7 +109,7 @@ export abstract class ClientApplication { }, cryptoInterface: new CryptoProvider(), networkInterface: this.config.system!.networkClient, - storageInterface: new Storage(this.config.auth!.clientId, this.config.cache!), + storageInterface: new Storage(this.config.cache!), libraryInfo: { sku: Constants.MSAL_SKU, version: version, diff --git a/lib/msal-node/src/utils/Constants.ts b/lib/msal-node/src/utils/Constants.ts index 63256b493a..5c9607a8cd 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: '', EXTENSION_LIB: 'extenstion_library', }; diff --git a/samples/VanillaJSTestApp2.0/app/default/auth.js b/samples/VanillaJSTestApp2.0/app/default/auth.js index 9180f452d4..ec2ddff278 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/index.js b/samples/msal-node-auth-code/index.js index c81fa16e25..79befd183c 100644 --- a/samples/msal-node-auth-code/index.js +++ b/samples/msal-node-auth-code/index.js @@ -20,9 +20,9 @@ const publicClientConfig = { redirectUri: "http://localhost:3000/redirect", }, 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 - } + cacheLocation: "/Users/sameeragajjarapu/Documents/cache.json", // This configures where your cache will be stored + storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge + }, }; const pca = new msal.PublicClientApplication(publicClientConfig); @@ -56,10 +56,10 @@ function acquireToken(req, res){ }; pca.acquireTokenByCode(tokenRequest).then((response) => { - console.log(response); + console.log("\nResponse: \n:", response); + // console.log(pca.getCache()); res.send(200); }).catch((error) => { res.send(500); - console.log(JSON.stringify(error.response)); }) } diff --git a/samples/msal-node-auth-code/package.json b/samples/msal-node-auth-code/package.json index 31337670e4..0e64fd2eec 100644 --- a/samples/msal-node-auth-code/package.json +++ b/samples/msal-node-auth-code/package.json @@ -6,7 +6,7 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, - "author": "sameerag", + "author": "Microsoft", "license": "MIT", "dependencies": { "@azure/msal-node": "^0.1.0",