Skip to content

Commit 3f96e7f

Browse files
authored
Redirect flow temporary cache refactor (#7648)
This PR makes several changes to temporary cache used in the redirect flow to prepare for the addition of the EAR protocol and has the added benefit of consolidating response handling across flows (more work to be done here but that's a tomorrow problem) - Cache the full request + verifier rather than prebuilding the AuthCodeRequest before redirecting. - Stop caching nonce, state, correlationId and authority separately as they will be included in the full request above ^ - Stop caching CCSCredential as everything needed to build it is in the full request above ^ - Move temporary storage cleanup (including interaction_in_progress) to more centralized locations
1 parent 7d13c0d commit 3f96e7f

18 files changed

+708
-2366
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "minor",
3+
"comment": "refactor redirect request caching",
4+
"packageName": "@azure/msal-browser",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}

Diff for: lib/msal-browser/apiReview/msal-browser.api.md

-10
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,6 @@ declare namespace BrowserAuthErrorCodes {
210210
silentPromptValueError,
211211
noTokenRequestCacheError,
212212
unableToParseTokenRequestCacheError,
213-
noCachedAuthorityError,
214213
authRequestNotSetError,
215214
invalidCacheType,
216215
nonBrowserEnvironment,
@@ -335,10 +334,6 @@ export const BrowserAuthErrorMessage: {
335334
code: string;
336335
desc: string;
337336
};
338-
noCachedAuthorityError: {
339-
code: string;
340-
desc: string;
341-
};
342337
authRequestNotSet: {
343338
code: string;
344339
desc: string;
@@ -1285,11 +1280,6 @@ export { NetworkResponse }
12851280
// @public (undocumented)
12861281
const noAccountError = "no_account_error";
12871282

1288-
// Warning: (ae-missing-release-tag) "noCachedAuthorityError" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
1289-
//
1290-
// @public (undocumented)
1291-
const noCachedAuthorityError = "no_cached_authority_error";
1292-
12931283
// Warning: (ae-missing-release-tag) "nonBrowserEnvironment" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
12941284
//
12951285
// @public (undocumented)

Diff for: lib/msal-browser/src/cache/BrowserCacheManager.ts

+30-205
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
Constants,
88
PersistentCacheKeys,
99
StringUtils,
10-
CommonAuthorizationCodeRequest,
1110
ICrypto,
1211
AccountEntity,
1312
IdTokenEntity,
@@ -17,14 +16,11 @@ import {
1716
CacheManager,
1817
ServerTelemetryEntity,
1918
ThrottlingEntity,
20-
ProtocolUtils,
2119
Logger,
2220
AuthorityMetadataEntity,
2321
DEFAULT_CRYPTO_IMPLEMENTATION,
2422
AccountInfo,
2523
ActiveAccountFilters,
26-
CcsCredential,
27-
CcsCredentialType,
2824
TokenKeys,
2925
CredentialType,
3026
CacheRecord,
@@ -39,6 +35,7 @@ import {
3935
CacheError,
4036
invokeAsync,
4137
TimeUtils,
38+
CommonAuthorizationUrlRequest,
4239
} from "@azure/msal-common/browser";
4340
import { CacheOptions } from "../config/Configuration.js";
4441
import {
@@ -47,7 +44,6 @@ import {
4744
} from "../error/BrowserAuthError.js";
4845
import {
4946
BrowserCacheLocation,
50-
InteractionType,
5147
TemporaryCacheKeys,
5248
InMemoryCacheKeys,
5349
StaticCacheKeys,
@@ -56,7 +52,6 @@ import { LocalStorage } from "./LocalStorage.js";
5652
import { SessionStorage } from "./SessionStorage.js";
5753
import { MemoryStorage } from "./MemoryStorage.js";
5854
import { IWindowStorage } from "./IWindowStorage.js";
59-
import { extractBrowserRequestState } from "../utils/BrowserProtocolUtils.js";
6055
import { NativeTokenRequest } from "../broker/nativeBroker/NativeRequest.js";
6156
import { AuthenticationResult } from "../response/AuthenticationResult.js";
6257
import { SilentRequest } from "../request/SilentRequest.js";
@@ -1074,221 +1069,58 @@ export class BrowserCacheManager extends CacheManager {
10741069
return JSON.stringify(key);
10751070
}
10761071

1077-
/**
1078-
* Create authorityKey to cache authority
1079-
* @param state
1080-
*/
1081-
generateAuthorityKey(stateString: string): string {
1082-
const {
1083-
libraryState: { id: stateId },
1084-
} = ProtocolUtils.parseRequestState(this.cryptoImpl, stateString);
1085-
1086-
return this.generateCacheKey(
1087-
`${TemporaryCacheKeys.AUTHORITY}.${stateId}`
1088-
);
1089-
}
1090-
1091-
/**
1092-
* Create Nonce key to cache nonce
1093-
* @param state
1094-
*/
1095-
generateNonceKey(stateString: string): string {
1096-
const {
1097-
libraryState: { id: stateId },
1098-
} = ProtocolUtils.parseRequestState(this.cryptoImpl, stateString);
1099-
1100-
return this.generateCacheKey(
1101-
`${TemporaryCacheKeys.NONCE_IDTOKEN}.${stateId}`
1102-
);
1103-
}
1104-
1105-
/**
1106-
* Creates full cache key for the request state
1107-
* @param stateString State string for the request
1108-
*/
1109-
generateStateKey(stateString: string): string {
1110-
// Use the library state id to key temp storage for uniqueness for multiple concurrent requests
1111-
const {
1112-
libraryState: { id: stateId },
1113-
} = ProtocolUtils.parseRequestState(this.cryptoImpl, stateString);
1114-
return this.generateCacheKey(
1115-
`${TemporaryCacheKeys.REQUEST_STATE}.${stateId}`
1116-
);
1117-
}
1118-
1119-
/**
1120-
* Gets the cached authority based on the cached state. Returns empty if no cached state found.
1121-
*/
1122-
getCachedAuthority(cachedState: string): string | null {
1123-
const stateCacheKey = this.generateStateKey(cachedState);
1124-
const state = this.getTemporaryCache(stateCacheKey);
1125-
if (!state) {
1126-
return null;
1127-
}
1128-
1129-
const authorityCacheKey = this.generateAuthorityKey(state);
1130-
return this.getTemporaryCache(authorityCacheKey);
1131-
}
1132-
1133-
/**
1134-
* Updates account, authority, and state in cache
1135-
* @param serverAuthenticationRequest
1136-
* @param account
1137-
*/
1138-
updateCacheEntries(
1139-
state: string,
1140-
nonce: string,
1141-
authorityInstance: string,
1142-
loginHint: string,
1143-
account: AccountInfo | null
1144-
): void {
1145-
this.logger.trace("BrowserCacheManager.updateCacheEntries called");
1146-
// Cache the request state
1147-
const stateCacheKey = this.generateStateKey(state);
1148-
this.setTemporaryCache(stateCacheKey, state, false);
1149-
1150-
// Cache the nonce
1151-
const nonceCacheKey = this.generateNonceKey(state);
1152-
this.setTemporaryCache(nonceCacheKey, nonce, false);
1153-
1154-
// Cache authorityKey
1155-
const authorityCacheKey = this.generateAuthorityKey(state);
1156-
this.setTemporaryCache(authorityCacheKey, authorityInstance, false);
1157-
1158-
if (account) {
1159-
const ccsCredential: CcsCredential = {
1160-
credential: account.homeAccountId,
1161-
type: CcsCredentialType.HOME_ACCOUNT_ID,
1162-
};
1163-
this.setTemporaryCache(
1164-
TemporaryCacheKeys.CCS_CREDENTIAL,
1165-
JSON.stringify(ccsCredential),
1166-
true
1167-
);
1168-
} else if (loginHint) {
1169-
const ccsCredential: CcsCredential = {
1170-
credential: loginHint,
1171-
type: CcsCredentialType.UPN,
1172-
};
1173-
this.setTemporaryCache(
1174-
TemporaryCacheKeys.CCS_CREDENTIAL,
1175-
JSON.stringify(ccsCredential),
1176-
true
1177-
);
1178-
}
1179-
}
1180-
11811072
/**
11821073
* Reset all temporary cache items
11831074
* @param state
11841075
*/
1185-
resetRequestCache(state: string): void {
1076+
resetRequestCache(): void {
11861077
this.logger.trace("BrowserCacheManager.resetRequestCache called");
1187-
// check state and remove associated cache items
1188-
if (state) {
1189-
this.temporaryCacheStorage.getKeys().forEach((key) => {
1190-
if (key.indexOf(state) !== -1) {
1191-
this.removeTemporaryItem(key);
1192-
}
1193-
});
11941078

1195-
// delete generic interactive request parameters
1196-
this.removeTemporaryItem(this.generateStateKey(state));
1197-
this.removeTemporaryItem(this.generateNonceKey(state));
1198-
this.removeTemporaryItem(this.generateAuthorityKey(state));
1199-
}
12001079
this.removeTemporaryItem(
12011080
this.generateCacheKey(TemporaryCacheKeys.REQUEST_PARAMS)
12021081
);
12031082
this.removeTemporaryItem(
1204-
this.generateCacheKey(TemporaryCacheKeys.ORIGIN_URI)
1205-
);
1206-
this.removeTemporaryItem(
1207-
this.generateCacheKey(TemporaryCacheKeys.URL_HASH)
1083+
this.generateCacheKey(TemporaryCacheKeys.VERIFIER)
12081084
);
12091085
this.removeTemporaryItem(
1210-
this.generateCacheKey(TemporaryCacheKeys.CORRELATION_ID)
1086+
this.generateCacheKey(TemporaryCacheKeys.ORIGIN_URI)
12111087
);
12121088
this.removeTemporaryItem(
1213-
this.generateCacheKey(TemporaryCacheKeys.CCS_CREDENTIAL)
1089+
this.generateCacheKey(TemporaryCacheKeys.URL_HASH)
12141090
);
12151091
this.removeTemporaryItem(
12161092
this.generateCacheKey(TemporaryCacheKeys.NATIVE_REQUEST)
12171093
);
12181094
this.setInteractionInProgress(false);
12191095
}
12201096

1221-
/**
1222-
* Removes temporary cache for the provided state
1223-
* @param stateString
1224-
*/
1225-
cleanRequestByState(stateString: string): void {
1226-
this.logger.trace("BrowserCacheManager.cleanRequestByState called");
1227-
// Interaction is completed - remove interaction status.
1228-
if (stateString) {
1229-
const stateKey = this.generateStateKey(stateString);
1230-
const cachedState = this.temporaryCacheStorage.getItem(stateKey);
1231-
this.logger.infoPii(
1232-
`BrowserCacheManager.cleanRequestByState: Removing temporary cache items for state: ${cachedState}`
1233-
);
1234-
this.resetRequestCache(cachedState || Constants.EMPTY_STRING);
1235-
}
1236-
}
1237-
1238-
/**
1239-
* Looks in temporary cache for any state values with the provided interactionType and removes all temporary cache items for that state
1240-
* Used in scenarios where temp cache needs to be cleaned but state is not known, such as clicking browser back button.
1241-
* @param interactionType
1242-
*/
1243-
cleanRequestByInteractionType(interactionType: InteractionType): void {
1244-
this.logger.trace(
1245-
"BrowserCacheManager.cleanRequestByInteractionType called"
1246-
);
1247-
// Loop through all keys to find state key
1248-
this.temporaryCacheStorage.getKeys().forEach((key) => {
1249-
// If this key is not the state key, move on
1250-
if (key.indexOf(TemporaryCacheKeys.REQUEST_STATE) === -1) {
1251-
return;
1252-
}
1253-
1254-
// Retrieve state value, return if not a valid value
1255-
const stateValue = this.temporaryCacheStorage.getItem(key);
1256-
if (!stateValue) {
1257-
return;
1258-
}
1259-
// Extract state and ensure it matches given InteractionType, then clean request cache
1260-
const parsedState = extractBrowserRequestState(
1261-
this.cryptoImpl,
1262-
stateValue
1263-
);
1264-
if (
1265-
parsedState &&
1266-
parsedState.interactionType === interactionType
1267-
) {
1268-
this.logger.infoPii(
1269-
`BrowserCacheManager.cleanRequestByInteractionType: Removing temporary cache items for state: ${stateValue}`
1270-
);
1271-
this.resetRequestCache(stateValue);
1272-
}
1273-
});
1274-
this.setInteractionInProgress(false);
1275-
}
1276-
1277-
cacheCodeRequest(authCodeRequest: CommonAuthorizationCodeRequest): void {
1278-
this.logger.trace("BrowserCacheManager.cacheCodeRequest called");
1097+
cacheAuthorizeRequest(
1098+
authCodeRequest: CommonAuthorizationUrlRequest,
1099+
codeVerifier?: string
1100+
): void {
1101+
this.logger.trace("BrowserCacheManager.cacheAuthorizeRequest called");
12791102

12801103
const encodedValue = base64Encode(JSON.stringify(authCodeRequest));
12811104
this.setTemporaryCache(
12821105
TemporaryCacheKeys.REQUEST_PARAMS,
12831106
encodedValue,
12841107
true
12851108
);
1109+
1110+
if (codeVerifier) {
1111+
const encodedVerifier = base64Encode(codeVerifier);
1112+
this.setTemporaryCache(
1113+
TemporaryCacheKeys.VERIFIER,
1114+
encodedVerifier,
1115+
true
1116+
);
1117+
}
12861118
}
12871119

12881120
/**
12891121
* Gets the token exchange parameters from the cache. Throws an error if nothing is found.
12901122
*/
1291-
getCachedRequest(state: string): CommonAuthorizationCodeRequest {
1123+
getCachedRequest(): [CommonAuthorizationUrlRequest, string] {
12921124
this.logger.trace("BrowserCacheManager.getCachedRequest called");
12931125
// Get token request from cache and parse as TokenExchangeParameters.
12941126
const encodedTokenRequest = this.getTemporaryCache(
@@ -1300,10 +1132,18 @@ export class BrowserCacheManager extends CacheManager {
13001132
BrowserAuthErrorCodes.noTokenRequestCacheError
13011133
);
13021134
}
1135+
const encodedVerifier = this.getTemporaryCache(
1136+
TemporaryCacheKeys.VERIFIER,
1137+
true
1138+
);
13031139

1304-
let parsedRequest: CommonAuthorizationCodeRequest;
1140+
let parsedRequest: CommonAuthorizationUrlRequest;
1141+
let verifier = "";
13051142
try {
13061143
parsedRequest = JSON.parse(base64Decode(encodedTokenRequest));
1144+
if (encodedVerifier) {
1145+
verifier = base64Decode(encodedVerifier);
1146+
}
13071147
} catch (e) {
13081148
this.logger.errorPii(`Attempted to parse: ${encodedTokenRequest}`);
13091149
this.logger.error(
@@ -1313,23 +1153,8 @@ export class BrowserCacheManager extends CacheManager {
13131153
BrowserAuthErrorCodes.unableToParseTokenRequestCacheError
13141154
);
13151155
}
1316-
this.removeTemporaryItem(
1317-
this.generateCacheKey(TemporaryCacheKeys.REQUEST_PARAMS)
1318-
);
13191156

1320-
// Get cached authority and use if no authority is cached with request.
1321-
if (!parsedRequest.authority) {
1322-
const authorityCacheKey: string = this.generateAuthorityKey(state);
1323-
const cachedAuthority = this.getTemporaryCache(authorityCacheKey);
1324-
if (!cachedAuthority) {
1325-
throw createBrowserAuthError(
1326-
BrowserAuthErrorCodes.noCachedAuthorityError
1327-
);
1328-
}
1329-
parsedRequest.authority = cachedAuthority;
1330-
}
1331-
1332-
return parsedRequest;
1157+
return [parsedRequest, verifier];
13331158
}
13341159

13351160
/**

0 commit comments

Comments
 (0)