Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backport hydrateCache() changes to 2.x #7055

Merged
merged 11 commits into from
May 30, 2024
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"files.eol": "\n",
"github-pr.targetBranch": "dev",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
"source.fixAll.eslint": "explicit"
tnorling marked this conversation as resolved.
Show resolved Hide resolved
},
"files.associations": {
"**/build/*.yml": "azure-pipelines",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Add `hydrateCache()` API support",
"packageName": "@azure/msal-browser",
"email": "[email protected]",
tnorling marked this conversation as resolved.
Show resolved Hide resolved
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Add `hydrateCache()` API support",
"packageName": "@azure/msal-common",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "none",
"comment": "Tests support for `hydrateCache()` API",
"packageName": "@azure/msal-node",
"email": "[email protected]",
"dependentChangeType": "none"
}
11 changes: 10 additions & 1 deletion lib/msal-browser/src/app/IPublicClientApplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ export interface IPublicClientApplication {
initializeWrapperLibrary(sku: WrapperSKU, version: string): void;
setNavigationClient(navigationClient: INavigationClient): void;
getConfiguration(): BrowserConfiguration;
hydrateCache(
result: AuthenticationResult,
request: SilentRequest
): Promise<void>;
}

export const stubbedPublicClientApplication: IPublicClientApplication = {
Expand Down Expand Up @@ -140,5 +144,10 @@ export const stubbedPublicClientApplication: IPublicClientApplication = {
},
getConfiguration: () => {
throw BrowserConfigurationAuthError.createStubPcaInstanceCalledError();
}
},
hydrateCache: () => {
return Promise.reject(
BrowserConfigurationAuthError.createStubPcaInstanceCalledError()
);
},
};
35 changes: 34 additions & 1 deletion lib/msal-browser/src/app/PublicClientApplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License.
*/

import { AccountInfo, AuthenticationResult, Constants, RequestThumbprint, AuthError, PerformanceEvents, ServerError, InteractionRequiredAuthError, InProgressPerformanceEvent, InteractionRequiredAuthErrorMessage } from "@azure/msal-common";
import { AccountInfo, AuthenticationResult, Constants, RequestThumbprint, AuthError, PerformanceEvents, ServerError, InteractionRequiredAuthError, InProgressPerformanceEvent, InteractionRequiredAuthErrorMessage, AccountEntity } from "@azure/msal-common";
import { Configuration } from "../config/Configuration";
import { DEFAULT_REQUEST, InteractionType, ApiId, CacheLookupPolicy, BrowserConstants } from "../utils/BrowserConstants";
import { IPublicClientApplication } from "./IPublicClientApplication";
Expand Down Expand Up @@ -282,4 +282,37 @@ export class PublicClientApplication extends ClientApplication implements IPubli
document.removeEventListener("visibilitychange",this.trackPageVisibility);
});
}

/**
* Hydrates cache with the tokens and account in the AuthenticationResult object
* @param result
* @param request - The request object that was used to obtain the AuthenticationResult
* @returns
*/
async hydrateCache(
result: AuthenticationResult,
request: SilentRequest
): Promise<void> {
this.logger.verbose("hydrateCache called");

if(result.account) {
tnorling marked this conversation as resolved.
Show resolved Hide resolved
// Account gets saved to browser storage regardless of native or not
const accountEntity = AccountEntity.createFromAccountInfo(
result.account,
result.cloudGraphHostName,
result.msGraphHost
);
this.browserStorage.setAccount(accountEntity);

if (result.fromNativeBroker) {
this.logger.verbose(
"Response was from native broker, storing in-memory"
);
// Tokens from native broker are stored in-memory
return this.nativeInternalStorage.hydrateCache(result, request);
} else {
return this.browserStorage.hydrateCache(result, request);
}
}
}
}
50 changes: 49 additions & 1 deletion lib/msal-browser/src/cache/BrowserCacheManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License.
*/

import { Constants, PersistentCacheKeys, StringUtils, CommonAuthorizationCodeRequest, ICrypto, AccountEntity, IdTokenEntity, AccessTokenEntity, RefreshTokenEntity, AppMetadataEntity, CacheManager, ServerTelemetryEntity, ThrottlingEntity, ProtocolUtils, Logger, AuthorityMetadataEntity, DEFAULT_CRYPTO_IMPLEMENTATION, AccountInfo, ActiveAccountFilters, CcsCredential, CcsCredentialType, IdToken, ValidCredentialType, ClientAuthError, TokenKeys, CredentialType } from "@azure/msal-common";
import { Constants, PersistentCacheKeys, StringUtils, CommonAuthorizationCodeRequest, ICrypto, AccountEntity, IdTokenEntity, AccessTokenEntity, RefreshTokenEntity, AppMetadataEntity, CacheManager, ServerTelemetryEntity, ThrottlingEntity, ProtocolUtils, Logger, AuthorityMetadataEntity, DEFAULT_CRYPTO_IMPLEMENTATION, AccountInfo, ActiveAccountFilters, CcsCredential, CcsCredentialType, IdToken, ValidCredentialType, ClientAuthError, TokenKeys, CredentialType, AuthenticationResult, AuthenticationScheme, CacheRecord } from "@azure/msal-common";
import { CacheOptions } from "../config/Configuration";
import { BrowserAuthError } from "../error/BrowserAuthError";
import { BrowserCacheLocation, InteractionType, TemporaryCacheKeys, InMemoryCacheKeys, StaticCacheKeys } from "../utils/BrowserConstants";
Expand All @@ -12,6 +12,7 @@ import { MemoryStorage } from "./MemoryStorage";
import { IWindowStorage } from "./IWindowStorage";
import { BrowserProtocolUtils } from "../utils/BrowserProtocolUtils";
import { NativeTokenRequest } from "../broker/nativeBroker/NativeRequest";
import { SilentRequest } from "../request/SilentRequest";

/**
* This class implements the cache storage interface for MSAL through browser local or session storage.
Expand Down Expand Up @@ -1410,6 +1411,53 @@ export class BrowserCacheManager extends CacheManager {
setRedirectRequestContext(value: string): void {
this.setTemporaryCache(TemporaryCacheKeys.REDIRECT_CONTEXT, value, true);
}

/**
* Builds credential entities from AuthenticationResult object and saves the resulting credentials to the cache
* @param result
* @param request
*/
async hydrateCache(
result: AuthenticationResult,
request: SilentRequest
): Promise<void> {
const idTokenEntity = IdTokenEntity.createIdTokenEntity(
result.account?.homeAccountId || "" ,
result.account?.environment || "",
result.idToken,
this.clientId,
result.tenantId
);

let claimsHash;
if (request.claims) {
claimsHash = await this.cryptoImpl.hashString(request.claims);
}
const accessTokenEntity = AccessTokenEntity.createAccessTokenEntity(
result.account?.homeAccountId || "",
result.account?.environment || "",
result.accessToken,
this.clientId,
result.tenantId,
result.scopes.join(" "),
result.expiresOn?.getTime() || 0,
result.extExpiresOn?.getTime() || 0,
this.cryptoImpl,
undefined, // refreshOn
result.tokenType as AuthenticationScheme,
undefined, // userAssertionHash
request.sshKid,
request.claims,
claimsHash
);

const cacheRecord = new CacheRecord(
undefined,
idTokenEntity,
accessTokenEntity
);
return this.saveCacheRecord(cacheRecord);
}
}

export const DEFAULT_BROWSER_CACHE_MANAGER = (clientId: string, logger: Logger): BrowserCacheManager => {
Expand Down
39 changes: 22 additions & 17 deletions lib/msal-browser/src/cache/TokenCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License.
*/

import { AccessTokenEntity, ICrypto, IdTokenEntity, Logger, ScopeSet, Authority, AuthorityOptions, ExternalTokenResponse, AccountEntity, AuthToken, RefreshTokenEntity , AuthorityType, CacheRecord, AuthenticationResult, Constants } from "@azure/msal-common";
import { AccessTokenEntity, ICrypto, IdTokenEntity, Logger, ScopeSet, Authority, AuthorityOptions, ExternalTokenResponse, AccountEntity, AuthToken, RefreshTokenEntity, CacheRecord, AuthenticationResult, Constants } from "@azure/msal-common";
import { BrowserConfiguration } from "../config/Configuration";
import { SilentRequest } from "../request/SilentRequest";
import { BrowserCacheManager } from "./BrowserCacheManager";
Expand Down Expand Up @@ -57,11 +57,12 @@ export class TokenCache implements ITokenCache {

const idToken = new AuthToken(response.id_token, this.cryptoObj);

let cacheRecord: CacheRecord | undefined;
let cacheRecord: CacheRecord;
let authority: Authority | undefined;
let cacheRecordAccount: AccountEntity;

if (request.account) {
const cacheRecordAccount = this.loadAccount(idToken, request.account.environment, undefined, undefined, request.account.homeAccountId);
cacheRecordAccount = AccountEntity.createFromAccountInfo(request.account);
cacheRecord = new CacheRecord(
cacheRecordAccount,
this.loadIdToken(idToken, cacheRecordAccount.homeAccountId, request.account.environment, request.account.tenantId),
Expand All @@ -83,7 +84,11 @@ export class TokenCache implements ITokenCache {
// "clientInfo" from options takes precedence over "clientInfo" in response
if (options.clientInfo) {
this.logger.trace("TokenCache - homeAccountId from options");
const cacheRecordAccount = this.loadAccount(idToken, authority.hostnameAndPort, options.clientInfo, authority.authorityType);
cacheRecordAccount = this.loadAccount(
idToken,
authority,
options.clientInfo
);
cacheRecord = new CacheRecord(
cacheRecordAccount,
this.loadIdToken(idToken, cacheRecordAccount.homeAccountId, authority.hostnameAndPort, authority.tenant),
Expand All @@ -92,7 +97,7 @@ export class TokenCache implements ITokenCache {
);
} else if (response.client_info) {
this.logger.trace("TokenCache - homeAccountId from response");
const cacheRecordAccount = this.loadAccount(idToken, authority.hostnameAndPort, response.client_info, authority.authorityType);
cacheRecordAccount = this.loadAccount(idToken, authority, response.client_info);
cacheRecord = new CacheRecord(
cacheRecordAccount,
this.loadIdToken(idToken, cacheRecordAccount.homeAccountId, authority.hostnameAndPort, authority.tenant),
Expand All @@ -106,7 +111,7 @@ export class TokenCache implements ITokenCache {
throw BrowserAuthError.createUnableToLoadTokenError("Please provide a request with an account or a request with authority.");
}

return this.generateAuthenticationResult(request, idToken, cacheRecord, authority);
return this.generateAuthenticationResult(request, idToken, cacheRecord, cacheRecordAccount, authority);
}

/**
Expand All @@ -118,22 +123,20 @@ export class TokenCache implements ITokenCache {
* @param requestHomeAccountId
* @returns `AccountEntity`
*/
private loadAccount(idToken: AuthToken, environment: string, clientInfo?: string, authorityType?: AuthorityType, requestHomeAccountId?: string): AccountEntity {
private loadAccount(idToken: AuthToken, authority: Authority, clientInfo?: string, requestHomeAccountId?: string): AccountEntity {

let homeAccountId;
if (requestHomeAccountId) {
homeAccountId = requestHomeAccountId;
} else if (authorityType !== undefined && clientInfo) {
homeAccountId = AccountEntity.generateHomeAccountId(clientInfo, authorityType, this.logger, this.cryptoObj, idToken);
} else if (authority.authorityType !== undefined && clientInfo) {
homeAccountId = AccountEntity.generateHomeAccountId(clientInfo, authority.authorityType, this.logger, this.cryptoObj, idToken.claims);
}

if (!homeAccountId) {
throw BrowserAuthError.createUnableToLoadTokenError("Unexpected missing homeAccountId");
}

const accountEntity = clientInfo ?
AccountEntity.createAccount(clientInfo, homeAccountId, idToken, undefined, undefined, undefined, environment) :
AccountEntity.createGenericAccount(homeAccountId, idToken, undefined, undefined, undefined, environment);
const accountEntity = AccountEntity.createAccount({homeAccountId, idTokenClaims: idToken.claims, clientInfo, environment: authority.hostnameAndPort}, authority);

if (this.isBrowserEnvironment) {
this.logger.verbose("TokenCache - loading account");
Expand Down Expand Up @@ -236,21 +239,23 @@ export class TokenCache implements ITokenCache {
* @param request
* @param idTokenObj
* @param cacheRecord
* @param accountEntity
* @param authority
* @returns `AuthenticationResult`
*/
private generateAuthenticationResult(
request: SilentRequest,
idTokenObj: AuthToken,
cacheRecord?: CacheRecord,
cacheRecord: CacheRecord,
accountEntity: AccountEntity,
authority?: Authority,
): AuthenticationResult {
let accessToken: string = Constants.EMPTY_STRING;
let responseScopes: Array<string> = [];
let expiresOn: Date | null = null;
let extExpiresOn: Date | undefined;

if (cacheRecord?.accessToken) {
if (cacheRecord.accessToken) {
accessToken = cacheRecord.accessToken.secret;
responseScopes = ScopeSet.fromString(cacheRecord.accessToken.target).asArray();
expiresOn = new Date(Number(cacheRecord.accessToken.expiresOn) * 1000);
Expand All @@ -265,7 +270,7 @@ export class TokenCache implements ITokenCache {
uniqueId: uid,
tenantId: tid,
scopes: responseScopes,
account: cacheRecord?.account ? cacheRecord.account.getAccountInfo() : null,
account: accountEntity ? accountEntity.getAccountInfo() : null,
idToken: idTokenObj ? idTokenObj.rawToken : Constants.EMPTY_STRING,
idTokenClaims: idTokenObj ? idTokenObj.claims : {},
accessToken: accessToken,
Expand All @@ -277,8 +282,8 @@ export class TokenCache implements ITokenCache {
familyId: Constants.EMPTY_STRING,
tokenType: cacheRecord?.accessToken?.tokenType || Constants.EMPTY_STRING,
state: Constants.EMPTY_STRING,
cloudGraphHostName: cacheRecord?.account?.cloudGraphHostName || Constants.EMPTY_STRING,
msGraphHost: cacheRecord?.account?.msGraphHost || Constants.EMPTY_STRING,
cloudGraphHostName: accountEntity.cloudGraphHostName || Constants.EMPTY_STRING,
msGraphHost: accountEntity.msGraphHost || Constants.EMPTY_STRING,
code: undefined,
fromNativeBroker: false
};
Expand Down
Loading
Loading