diff --git a/lib/msal-core/src/ScopeSet.ts b/lib/msal-core/src/ScopeSet.ts index 1043eb702c..76c2cc6462 100644 --- a/lib/msal-core/src/ScopeSet.ts +++ b/lib/msal-core/src/ScopeSet.ts @@ -4,7 +4,7 @@ */ import { ClientConfigurationError } from "./error/ClientConfigurationError"; -import { AuthenticationParameters } from "./AuthenticationParameters"; +import { Constants } from "./utils/Constants"; export class ScopeSet { @@ -117,7 +117,7 @@ export class ScopeSet { */ static getScopeFromState(state: string): string { if (state) { - const splitIndex = state.indexOf("|"); + const splitIndex = state.indexOf(Constants.resourceDelimiter); if (splitIndex > -1 && splitIndex + 1 < state.length) { return state.substring(splitIndex + 1); } diff --git a/lib/msal-core/src/UserAgentApplication.ts b/lib/msal-core/src/UserAgentApplication.ts index 24b0eb8b17..62b3fe35ce 100644 --- a/lib/msal-core/src/UserAgentApplication.ts +++ b/lib/msal-core/src/UserAgentApplication.ts @@ -94,6 +94,7 @@ export interface CacheResult { */ export type ResponseStateInfo = { state: string; + timestamp: number, stateMatch: boolean; requestType: string; }; @@ -1022,7 +1023,7 @@ export class UserAgentApplication { /** * @hidden - * This method must be called for processing the response received from the STS if using popups or iframes. It extracts the hash, processes the token or error + * This method must be called for processing the response received from the STS if using popups or iframes. It extracts the hash, processes the token or error * information and saves it in the cache. It then resolves the promises with the result. * @param {string} [hash=window.location.hash] - Hash fragment of Url. */ @@ -1042,13 +1043,13 @@ export class UserAgentApplication { /** * @hidden - * This method must be called for processing the response received from the STS when using redirect flows. It extracts the hash, processes the token or error + * This method must be called for processing the response received from the STS when using redirect flows. It extracts the hash, processes the token or error * information and saves it in the cache. The result can then be accessed by user registered callbacks. * @param {string} [hash=window.location.hash] - Hash fragment of Url. */ private handleRedirectAuthenticationResponse(hash: string): void { this.logger.info("Returned from redirect url"); - + // clear hash from window window.location.hash = ""; @@ -1087,10 +1088,13 @@ export class UserAgentApplication { if (!parameters) { throw AuthError.createUnexpectedError("Hash was not parsed correctly."); } - if (parameters.hasOwnProperty("state")) { + if (parameters.hasOwnProperty(ServerHashParamKeys.STATE)) { + const parsedState = RequestUtils.parseLibraryState(parameters.state); + stateResponse = { requestType: Constants.unknown, state: parameters.state, + timestamp: parsedState.ts, stateMatch: false }; } else { @@ -1102,13 +1106,13 @@ export class UserAgentApplication { */ // loginRedirect - if (stateResponse.state === this.cacheStorage.getItem(`${TemporaryCacheKeys.STATE_LOGIN}${Constants.resourceDelimiter}${stateResponse.state}`, this.inCookie) || stateResponse.state === this.silentAuthenticationState) { // loginRedirect + if (stateResponse.state === this.cacheStorage.getItem(`${TemporaryCacheKeys.STATE_LOGIN}${Constants.resourceDelimiter}${stateResponse.state}`, this.inCookie) || stateResponse.state === this.silentAuthenticationState) { stateResponse.requestType = Constants.login; stateResponse.stateMatch = true; return stateResponse; } // acquireTokenRedirect - else if (stateResponse.state === this.cacheStorage.getItem(`${TemporaryCacheKeys.STATE_ACQ_TOKEN}${Constants.resourceDelimiter}${stateResponse.state}`, this.inCookie)) { // acquireTokenRedirect + else if (stateResponse.state === this.cacheStorage.getItem(`${TemporaryCacheKeys.STATE_ACQ_TOKEN}${Constants.resourceDelimiter}${stateResponse.state}`, this.inCookie)) { stateResponse.requestType = Constants.renewToken; stateResponse.stateMatch = true; return stateResponse; @@ -1374,7 +1378,8 @@ export class UserAgentApplication { // Generate and cache accessTokenKey and accessTokenValue const expiresIn = TimeUtils.parseExpiresIn(parameters[ServerHashParamKeys.EXPIRES_IN]); - expiration = TimeUtils.now() + expiresIn; + const parsedState = RequestUtils.parseLibraryState(parameters[ServerHashParamKeys.STATE]); + expiration = parsedState.ts + expiresIn; const accessTokenKey = new AccessTokenKey(authority, this.clientId, scope, clientObj.uid, clientObj.utid); const accessTokenValue = new AccessTokenValue(parameters[ServerHashParamKeys.ACCESS_TOKEN], idTokenObj.rawIdToken, expiration.toString(), clientInfo); @@ -1686,7 +1691,7 @@ export class UserAgentApplication { */ getAccountState (state: string) { if (state) { - const splitIndex = state.indexOf("|"); + const splitIndex = state.indexOf(Constants.resourceDelimiter); if (splitIndex > -1 && splitIndex + 1 < state.length) { return state.substring(splitIndex + 1); } diff --git a/lib/msal-core/src/utils/Constants.ts b/lib/msal-core/src/utils/Constants.ts index 34894828a3..8f7c9af119 100644 --- a/lib/msal-core/src/utils/Constants.ts +++ b/lib/msal-core/src/utils/Constants.ts @@ -59,6 +59,7 @@ export class Constants { */ export enum ServerHashParamKeys { SCOPE = "scope", + STATE = "state", ERROR = "error", ERROR_DESCRIPTION = "error_description", ACCESS_TOKEN = "access_token", diff --git a/lib/msal-core/src/utils/RequestUtils.ts b/lib/msal-core/src/utils/RequestUtils.ts index 0369603690..e7f6e1136e 100644 --- a/lib/msal-core/src/utils/RequestUtils.ts +++ b/lib/msal-core/src/utils/RequestUtils.ts @@ -10,6 +10,13 @@ import { ScopeSet } from "../ScopeSet"; import { StringDict } from "../MsalTypes"; import { StringUtils } from "../utils/StringUtils"; import { CryptoUtils } from "../utils/CryptoUtils"; +import { TimeUtils } from "./TimeUtils"; +import { ClientAuthError } from "../error/ClientAuthError"; + +export type LibraryStateObject = { + id: string, + ts: number +}; /** * @hidden @@ -129,11 +136,54 @@ export class RequestUtils { * @ignore * * generate unique state per request - * @param request + * @param userState User-provided state value + * @returns State string include library state and user state + */ + static validateAndGenerateState(userState: string): string { + return !StringUtils.isEmpty(userState) ? `${RequestUtils.generateLibraryState()}${Constants.resourceDelimiter}${userState}` : RequestUtils.generateLibraryState(); + } + + /** + * Generates the state value used by the library. + * + * @returns Base64 encoded string representing the state + */ + static generateLibraryState(): string { + const stateObject: LibraryStateObject = { + id: CryptoUtils.createNewGuid(), + ts: TimeUtils.now() + }; + + const stateString = JSON.stringify(stateObject); + + return CryptoUtils.base64Encode(stateString); + } + + /** + * Decodes the state value into a StateObject + * + * @param state State value returned in the request + * @returns Parsed values from the encoded state value */ - static validateAndGenerateState(state: string): string { - // append GUID to user set state or set one for the user if null - return !StringUtils.isEmpty(state) ? CryptoUtils.createNewGuid() + "|" + state : CryptoUtils.createNewGuid(); + static parseLibraryState(state: string): LibraryStateObject { + const libraryState = state.split(Constants.resourceDelimiter)[0]; + + if (CryptoUtils.isGuid(libraryState)) { + return { + id: libraryState, + ts: TimeUtils.now() + }; + } + + try { + const stateString = CryptoUtils.base64Decode(libraryState); + + const stateObject = JSON.parse(stateString); + + return stateObject; + } catch (e) { + throw ClientAuthError.createInvalidStateError(state, null); + } } /** diff --git a/lib/msal-core/src/utils/TimeUtils.ts b/lib/msal-core/src/utils/TimeUtils.ts index 3960a5ccff..92c8d499ba 100644 --- a/lib/msal-core/src/utils/TimeUtils.ts +++ b/lib/msal-core/src/utils/TimeUtils.ts @@ -21,7 +21,7 @@ export class TimeUtils { } /** - * return the current time in Unix time. Date.getTime() returns in milliseconds. + * Return the current time in Unix time (seconds). Date.getTime() returns in milliseconds. */ static now(): number { return Math.round(new Date().getTime() / 1000.0); diff --git a/lib/msal-core/test/TestConstants.ts b/lib/msal-core/test/TestConstants.ts index a1e61a6d20..59066d9199 100644 --- a/lib/msal-core/test/TestConstants.ts +++ b/lib/msal-core/test/TestConstants.ts @@ -38,18 +38,20 @@ export const TEST_TOKEN_LIFETIMES = { TEST_ACCESS_TOKEN_EXP: 1537234948 }; + + // Test Hashes -export const TEST_HASHES = { - TEST_SUCCESS_ID_TOKEN_HASH: `#id_token=${TEST_TOKENS.IDTOKEN_V2}&client_info=${TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO}&state=RANDOM-GUID-HERE|`, - TEST_SUCCESS_ACCESS_TOKEN_HASH: `#access_token=${TEST_TOKENS.ACCESSTOKEN}&id_token=${TEST_TOKENS.IDTOKEN_V2}&scope=test&expiresIn=${TEST_TOKEN_LIFETIMES.DEFAULT_EXPIRES_IN}&client_info=${TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO}&state=RANDOM-GUID-HERE|`, - TEST_ERROR_HASH: "#error=error_code&error_description=msal+error+description&state=RANDOM-GUID-HERE|", - TEST_INTERACTION_REQ_ERROR_HASH1: "#error=interaction_required&error_description=msal+error+description&state=RANDOM-GUID-HERE|", - TEST_INTERACTION_REQ_ERROR_HASH2: "#error=interaction_required&error_description=msal+error+description+interaction_required&state=RANDOM-GUID-HERE|", - TEST_LOGIN_REQ_ERROR_HASH1: "#error=login_required&error_description=msal+error+description&state=RANDOM-GUID-HERE|", - TEST_LOGIN_REQ_ERROR_HASH2: "#error=login_required&error_description=msal+error+description+login_required&state=RANDOM-GUID-HERE|", - TEST_CONSENT_REQ_ERROR_HASH1: "#error=consent_required&error_description=msal+error+description&state=RANDOM-GUID-HERE|", - TEST_CONSENT_REQ_ERROR_HASH2: "#error=consent_required&error_description=msal+error+description+consent_required&state=RANDOM-GUID-HERE|" -}; +export const testHashesForState = state => ({ + TEST_SUCCESS_ID_TOKEN_HASH: `#id_token=${TEST_TOKENS.IDTOKEN_V2}&client_info=${TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO}&state=${state}|`, + TEST_SUCCESS_ACCESS_TOKEN_HASH: `#access_token=${TEST_TOKENS.ACCESSTOKEN}&id_token=${TEST_TOKENS.IDTOKEN_V2}&scope=test&expiresIn=${TEST_TOKEN_LIFETIMES.DEFAULT_EXPIRES_IN}&client_info=${TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO}&state=${state}|`, + TEST_ERROR_HASH: `#error=error_code&error_description=msal+error+description&state=${state}|`, + TEST_INTERACTION_REQ_ERROR_HASH1: `#error=interaction_required&error_description=msal+error+description&state=${state}|`, + TEST_INTERACTION_REQ_ERROR_HASH2: `#error=interaction_required&error_description=msal+error+description+interaction_required&state=${state}|`, + TEST_LOGIN_REQ_ERROR_HASH1: `#error=login_required&error_description=msal+error+description&state=${state}|`, + TEST_LOGIN_REQ_ERROR_HASH2: `#error=login_required&error_description=msal+error+description+login_required&state=${state}|`, + TEST_CONSENT_REQ_ERROR_HASH1: `#error=consent_required&error_description=msal+error+description&state=${state}|`, + TEST_CONSENT_REQ_ERROR_HASH2: `#error=consent_required&error_description=msal+error+description+consent_required&state=${state}|` +}); // Test MSAL config params export const TEST_CONFIG = { diff --git a/lib/msal-core/test/UserAgentApplication.spec.ts b/lib/msal-core/test/UserAgentApplication.spec.ts index a461482f41..c67542bfee 100644 --- a/lib/msal-core/test/UserAgentApplication.spec.ts +++ b/lib/msal-core/test/UserAgentApplication.spec.ts @@ -28,9 +28,10 @@ import { ClientAuthErrorMessage } from "../src/error/ClientAuthError"; import { ClientConfigurationErrorMessage } from "../src/error/ClientConfigurationError"; import { InteractionRequiredAuthErrorMessage } from "../src/error/InteractionRequiredAuthError"; import { ServerRequestParameters } from "../src/ServerRequestParameters"; -import { TEST_URIS, TEST_DATA_CLIENT_INFO, TEST_HASHES, TEST_TOKENS, TEST_CONFIG, TEST_TOKEN_LIFETIMES } from "./TestConstants"; +import { TEST_URIS, TEST_DATA_CLIENT_INFO, testHashesForState, TEST_TOKENS, TEST_CONFIG, TEST_TOKEN_LIFETIMES } from "./TestConstants"; import { IdToken } from "../src/IdToken"; import { TimeUtils } from "../src/utils/TimeUtils"; +import { RequestUtils } from "../src/utils/RequestUtils"; type kv = { [key: string]: string; @@ -39,6 +40,9 @@ type kv = { describe("UserAgentApplication.ts Class", function () { // Test state params + sinon.stub(TimeUtils, "now").returns(TEST_TOKEN_LIFETIMES.BASELINE_DATE_CHECK); + const TEST_LIBRARY_STATE = RequestUtils.generateLibraryState(); + const TEST_USER_STATE_NUM = "1234"; const TEST_USER_STATE_URL = "https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-implicit-grant-flow/scope1"; @@ -52,7 +56,7 @@ describe("UserAgentApplication.ts Class", function () { const TEST_ERROR_DESC = "msal error description"; const TEST_ACCESS_DENIED = "access_denied"; - const TEST_SERVER_ERROR_SUBCODE_CANCEL = "#error=access_denied&error_subcode=cancel&state=RANDOM-GUID-HERE|"; + const TEST_SERVER_ERROR_SUBCODE_CANCEL = `#error=access_denied&error_subcode=cancel&state=${TEST_LIBRARY_STATE}|`; // Test SSO params const TEST_LOGIN_HINT = "test@test.com"; @@ -101,6 +105,7 @@ describe("UserAgentApplication.ts Class", function () { sinon.stub(msal.getAuthorityInstance(), "EndSessionEndpoint").value(validOpenIdConfigurationResponse.EndSessionEndpoint); sinon.stub(msal.getAuthorityInstance(), "SelfSignedJwtAudience").value(validOpenIdConfigurationResponse.Issuer); sinon.stub(WindowUtils, "isInIframe").returns(false); + sinon.stub(TimeUtils, "now").returns(TEST_TOKEN_LIFETIMES.BASELINE_DATE_CHECK); }; const setUtilUnifiedCacheQPStubs = function (params: kv) { @@ -704,8 +709,8 @@ describe("UserAgentApplication.ts Class", function () { }); it("Calls the error callback if two callbacks are sent", function () { - window.location.hash = TEST_HASHES.TEST_ERROR_HASH + TEST_USER_STATE_NUM; - cacheStorage.setItem(`${TemporaryCacheKeys.STATE_LOGIN}|RANDOM-GUID-HERE|${TEST_USER_STATE_NUM}`, "RANDOM-GUID-HERE|" + TEST_USER_STATE_NUM); + window.location.hash = testHashesForState(TEST_LIBRARY_STATE) + TEST_USER_STATE_NUM; + cacheStorage.setItem(`${TemporaryCacheKeys.STATE_LOGIN}|${TEST_LIBRARY_STATE}|${TEST_USER_STATE_NUM}`, `${TEST_LIBRARY_STATE}|${TEST_USER_STATE_NUM}`); msal = new UserAgentApplication(config); @@ -722,9 +727,9 @@ describe("UserAgentApplication.ts Class", function () { }); it("Calls the token callback if two callbacks are sent", function () { - cacheStorage.setItem(`${TemporaryCacheKeys.STATE_LOGIN}|RANDOM-GUID-HERE|${TEST_USER_STATE_NUM}`, "RANDOM-GUID-HERE|" + TEST_USER_STATE_NUM); - cacheStorage.setItem(`${TemporaryCacheKeys.NONCE_IDTOKEN}|RANDOM-GUID-HERE|${TEST_USER_STATE_NUM}`, TEST_NONCE); - window.location.hash = TEST_HASHES.TEST_SUCCESS_ID_TOKEN_HASH + TEST_USER_STATE_NUM; + cacheStorage.setItem(`${TemporaryCacheKeys.STATE_LOGIN}|${TEST_LIBRARY_STATE}|${TEST_USER_STATE_NUM}`, `${TEST_LIBRARY_STATE}|${TEST_USER_STATE_NUM}`); + cacheStorage.setItem(`${TemporaryCacheKeys.NONCE_IDTOKEN}|${TEST_LIBRARY_STATE}|${TEST_USER_STATE_NUM}`, TEST_NONCE); + window.location.hash = testHashesForState(TEST_LIBRARY_STATE).TEST_SUCCESS_ID_TOKEN_HASH + TEST_USER_STATE_NUM; msal = new UserAgentApplication(config); @@ -739,9 +744,9 @@ describe("UserAgentApplication.ts Class", function () { }); it("Calls the response callback if single callback is sent", function () { - cacheStorage.setItem(`${TemporaryCacheKeys.STATE_LOGIN}|RANDOM-GUID-HERE|${TEST_USER_STATE_NUM}`, "RANDOM-GUID-HERE|" + TEST_USER_STATE_NUM); - cacheStorage.setItem(`${TemporaryCacheKeys.NONCE_IDTOKEN}|RANDOM-GUID-HERE|${TEST_USER_STATE_NUM}`, TEST_NONCE) - window.location.hash = TEST_HASHES.TEST_SUCCESS_ID_TOKEN_HASH + TEST_USER_STATE_NUM; + cacheStorage.setItem(`${TemporaryCacheKeys.STATE_LOGIN}|${TEST_LIBRARY_STATE}|${TEST_USER_STATE_NUM}`, `${TEST_LIBRARY_STATE}|${TEST_USER_STATE_NUM}`); + cacheStorage.setItem(`${TemporaryCacheKeys.NONCE_IDTOKEN}|${TEST_LIBRARY_STATE}|${TEST_USER_STATE_NUM}`, TEST_NONCE); + window.location.hash = testHashesForState(TEST_LIBRARY_STATE).TEST_SUCCESS_ID_TOKEN_HASH + TEST_USER_STATE_NUM; msal = new UserAgentApplication(config); @@ -1077,7 +1082,7 @@ describe("UserAgentApplication.ts Class", function () { navigateToLoginRequestUrl: false } }; - + setAuthInstanceStubs(); setTestCacheItems(); }); @@ -1090,10 +1095,12 @@ describe("UserAgentApplication.ts Class", function () { }); it("tests saveTokenForHash in case of response", function() { - window.location.hash = TEST_HASHES.TEST_SUCCESS_ID_TOKEN_HASH + TEST_USER_STATE_NUM; - cacheStorage.setItem(`${TemporaryCacheKeys.STATE_LOGIN}|RANDOM-GUID-HERE|${TEST_USER_STATE_NUM}`, "RANDOM-GUID-HERE|" + TEST_USER_STATE_NUM); - cacheStorage.setItem(`${TemporaryCacheKeys.NONCE_IDTOKEN}|RANDOM-GUID-HERE|${TEST_USER_STATE_NUM}`, TEST_NONCE); - + const successHash = testHashesForState(TEST_LIBRARY_STATE).TEST_SUCCESS_ID_TOKEN_HASH + TEST_USER_STATE_NUM; + + window.location.hash = successHash; + cacheStorage.setItem(`${TemporaryCacheKeys.STATE_LOGIN}|${TEST_LIBRARY_STATE}|${TEST_USER_STATE_NUM}`, `${TEST_LIBRARY_STATE}|${TEST_USER_STATE_NUM}`); + cacheStorage.setItem(`${TemporaryCacheKeys.NONCE_IDTOKEN}|${TEST_LIBRARY_STATE}|${TEST_USER_STATE_NUM}`, TEST_NONCE); + msal = new UserAgentApplication(config); const checkRespFromServer = function(response: AuthResponse) { @@ -1107,9 +1114,9 @@ describe("UserAgentApplication.ts Class", function () { }); it("tests saveTokenForHash in case of error", function() { - window.location.hash = TEST_HASHES.TEST_ERROR_HASH + TEST_USER_STATE_NUM; - cacheStorage.setItem(`${TemporaryCacheKeys.STATE_LOGIN}|RANDOM-GUID-HERE|${TEST_USER_STATE_NUM}`, "RANDOM-GUID-HERE|" + TEST_USER_STATE_NUM); - + window.location.hash = testHashesForState(TEST_LIBRARY_STATE).TEST_ERROR_HASH + TEST_USER_STATE_NUM; + cacheStorage.setItem(`${TemporaryCacheKeys.STATE_LOGIN}|${TEST_LIBRARY_STATE}|${TEST_USER_STATE_NUM}`, `${TEST_LIBRARY_STATE}|${TEST_USER_STATE_NUM}`); + msal = new UserAgentApplication(config); const checkErrorFromServer = function(error: AuthError, response: AuthResponse) { expect(cacheStorage.getItem(TemporaryCacheKeys.URL_HASH)).to.be.null; @@ -1126,8 +1133,8 @@ describe("UserAgentApplication.ts Class", function () { // TEST_SERVER_ERROR_SUBCODE_CANCEL it("tests saveTokenForHash in case of non-consentable scopes / return to the application without consenting", function() { window.location.hash = TEST_SERVER_ERROR_SUBCODE_CANCEL + TEST_USER_STATE_NUM; - cacheStorage.setItem(`${TemporaryCacheKeys.STATE_LOGIN}|RANDOM-GUID-HERE|${TEST_USER_STATE_NUM}`, "RANDOM-GUID-HERE|" + TEST_USER_STATE_NUM); - + cacheStorage.setItem(`${TemporaryCacheKeys.STATE_LOGIN}|${TEST_LIBRARY_STATE}|${TEST_USER_STATE_NUM}`, `${TEST_LIBRARY_STATE}|${TEST_USER_STATE_NUM}`); + msal = new UserAgentApplication(config); const checkErrorFromServer = function(error: AuthError, response: AuthResponse) { expect(cacheStorage.getItem(TemporaryCacheKeys.URL_HASH)).to.be.null; @@ -1140,9 +1147,9 @@ describe("UserAgentApplication.ts Class", function () { }); it("tests if you get the state back in errorReceived callback, if state is a number", function () { - window.location.hash = TEST_HASHES.TEST_ERROR_HASH + TEST_USER_STATE_NUM; - cacheStorage.setItem(`${TemporaryCacheKeys.STATE_LOGIN}|RANDOM-GUID-HERE|${TEST_USER_STATE_NUM}`, "RANDOM-GUID-HERE|" + TEST_USER_STATE_NUM); - + window.location.hash = testHashesForState(TEST_LIBRARY_STATE).TEST_ERROR_HASH + TEST_USER_STATE_NUM; + cacheStorage.setItem(`${TemporaryCacheKeys.STATE_LOGIN}|${TEST_LIBRARY_STATE}|${TEST_USER_STATE_NUM}`, `${TEST_LIBRARY_STATE}|${TEST_USER_STATE_NUM}`); + msal = new UserAgentApplication(config); const checkErrorHasState = function(error: AuthError, response: AuthResponse) { expect(response.accountState).to.include(TEST_USER_STATE_NUM); @@ -1151,9 +1158,9 @@ describe("UserAgentApplication.ts Class", function () { }); it("tests if you get the state back in errorReceived callback, if state is a url", function () { - window.location.hash = TEST_HASHES.TEST_ERROR_HASH + TEST_USER_STATE_URL; - cacheStorage.setItem(`${TemporaryCacheKeys.STATE_LOGIN}|RANDOM-GUID-HERE|${TEST_USER_STATE_NUM}`, "RANDOM-GUID-HERE|" + TEST_USER_STATE_NUM); - + window.location.hash = testHashesForState(TEST_LIBRARY_STATE).TEST_ERROR_HASH + TEST_USER_STATE_URL; + cacheStorage.setItem(`${TemporaryCacheKeys.STATE_LOGIN}|${TEST_LIBRARY_STATE}|${TEST_USER_STATE_NUM}`, `${TEST_LIBRARY_STATE}|${TEST_USER_STATE_NUM}`); + msal = new UserAgentApplication(config); const checkErrorHasState = function(error: AuthError, response: AuthResponse) { @@ -1162,9 +1169,9 @@ describe("UserAgentApplication.ts Class", function () { msal.handleRedirectCallback(checkErrorHasState); }); - it("tests that isCallback correctly identifies url hash", function () { + it("tests that isCallback correctly identifies url hash", function () { msal = new UserAgentApplication(config); - + expect(msal.isCallback("not a callback")).to.be.false; expect(msal.isCallback("#error_description=someting_wrong")).to.be.true; expect(msal.isCallback("#/error_description=someting_wrong")).to.be.true; @@ -1173,15 +1180,16 @@ describe("UserAgentApplication.ts Class", function () { }); it("tests that expiresIn returns the correct date for access tokens", function () { - sinon.stub(TimeUtils, "now").returns(TEST_TOKEN_LIFETIMES.BASELINE_DATE_CHECK); - const acquireTokenAccountKey = AuthCache.generateAcquireTokenAccountKey(account.homeAccountIdentifier, "RANDOM-GUID-HERE|" + TEST_USER_STATE_NUM); + const acquireTokenAccountKey = AuthCache.generateAcquireTokenAccountKey(account.homeAccountIdentifier, `${TEST_LIBRARY_STATE}|${TEST_USER_STATE_NUM}`); cacheStorage.setItem(acquireTokenAccountKey, JSON.stringify(account)); - window.location.hash = TEST_HASHES.TEST_SUCCESS_ACCESS_TOKEN_HASH + TEST_USER_STATE_NUM; - cacheStorage.setItem(`${TemporaryCacheKeys.STATE_ACQ_TOKEN}|RANDOM-GUID-HERE|${TEST_USER_STATE_NUM}`, "RANDOM-GUID-HERE|" + TEST_USER_STATE_NUM); - cacheStorage.setItem(`${TemporaryCacheKeys.NONCE_IDTOKEN}|RANDOM-GUID-HERE|${TEST_USER_STATE_NUM}`, TEST_NONCE); - + + const successHash = testHashesForState(TEST_LIBRARY_STATE).TEST_SUCCESS_ACCESS_TOKEN_HASH + TEST_USER_STATE_NUM; + window.location.hash = successHash; + cacheStorage.setItem(`${TemporaryCacheKeys.STATE_ACQ_TOKEN}|${TEST_LIBRARY_STATE}|${TEST_USER_STATE_NUM}`, `${TEST_LIBRARY_STATE}|${TEST_USER_STATE_NUM}`); + cacheStorage.setItem(`${TemporaryCacheKeys.NONCE_IDTOKEN}|${TEST_LIBRARY_STATE}|${TEST_USER_STATE_NUM}`, TEST_NONCE); + msal = new UserAgentApplication(config); - + const checkRespFromServer = function(response: AuthResponse) { expect(response.uniqueId).to.be.eq(TEST_UNIQUE_ID); @@ -1195,12 +1203,14 @@ describe("UserAgentApplication.ts Class", function () { }); it("tests that expiresIn returns the correct date for id tokens", function () { - const acquireTokenAccountKey = AuthCache.generateAcquireTokenAccountKey(account.homeAccountIdentifier, "RANDOM-GUID-HERE|" + TEST_USER_STATE_NUM); + const acquireTokenAccountKey = AuthCache.generateAcquireTokenAccountKey(account.homeAccountIdentifier, `${TEST_LIBRARY_STATE}|${TEST_USER_STATE_NUM}`); cacheStorage.setItem(acquireTokenAccountKey, JSON.stringify(account)); - window.location.hash = TEST_HASHES.TEST_SUCCESS_ID_TOKEN_HASH + TEST_USER_STATE_NUM; - cacheStorage.setItem(`${TemporaryCacheKeys.STATE_LOGIN}|RANDOM-GUID-HERE|${TEST_USER_STATE_NUM}`, "RANDOM-GUID-HERE|" + TEST_USER_STATE_NUM); - cacheStorage.setItem(`${TemporaryCacheKeys.NONCE_IDTOKEN}|RANDOM-GUID-HERE|${TEST_USER_STATE_NUM}`, TEST_NONCE); - + + const successHash = testHashesForState(TEST_LIBRARY_STATE).TEST_SUCCESS_ID_TOKEN_HASH + TEST_USER_STATE_NUM; + window.location.hash = successHash; + cacheStorage.setItem(`${TemporaryCacheKeys.STATE_LOGIN}|${TEST_LIBRARY_STATE}|${TEST_USER_STATE_NUM}`, `${TEST_LIBRARY_STATE}|${TEST_USER_STATE_NUM}`); + cacheStorage.setItem(`${TemporaryCacheKeys.NONCE_IDTOKEN}|${TEST_LIBRARY_STATE}|${TEST_USER_STATE_NUM}`, TEST_NONCE); + msal = new UserAgentApplication(config); const checkRespFromServer = function(response: AuthResponse) { @@ -1226,7 +1236,7 @@ describe("UserAgentApplication.ts Class", function () { navigateToLoginRequestUrl: false } }; - + setAuthInstanceStubs(); setTestCacheItems(); }); @@ -1238,8 +1248,8 @@ describe("UserAgentApplication.ts Class", function () { }); it("tests saveTokenForHash in case of interaction_required error code", function() { - window.location.hash = TemporaryCacheKeys.URL_HASH, TEST_HASHES.TEST_INTERACTION_REQ_ERROR_HASH1 + TEST_USER_STATE_NUM; - cacheStorage.setItem(`${TemporaryCacheKeys.STATE_LOGIN}|RANDOM-GUID-HERE|${TEST_USER_STATE_NUM}`, "RANDOM-GUID-HERE|" + TEST_USER_STATE_NUM); + window.location.hash = testHashesForState(TEST_LIBRARY_STATE).TEST_INTERACTION_REQ_ERROR_HASH1 + TEST_USER_STATE_NUM; + cacheStorage.setItem(`${TemporaryCacheKeys.STATE_LOGIN}|${TEST_LIBRARY_STATE}|${TEST_USER_STATE_NUM}`, `${TEST_LIBRARY_STATE}|${TEST_USER_STATE_NUM}`); msal = new UserAgentApplication(config); @@ -1256,8 +1266,8 @@ describe("UserAgentApplication.ts Class", function () { }); it("tests saveTokenForHash in case of interaction_required error code and description", function() { - window.location.hash = TemporaryCacheKeys.URL_HASH, TEST_HASHES.TEST_INTERACTION_REQ_ERROR_HASH2 + TEST_USER_STATE_NUM; - cacheStorage.setItem(`${TemporaryCacheKeys.STATE_LOGIN}|RANDOM-GUID-HERE|${TEST_USER_STATE_NUM}`, "RANDOM-GUID-HERE|" + TEST_USER_STATE_NUM); + window.location.hash = testHashesForState(TEST_LIBRARY_STATE).TEST_INTERACTION_REQ_ERROR_HASH2 + TEST_USER_STATE_NUM; + cacheStorage.setItem(`${TemporaryCacheKeys.STATE_LOGIN}|${TEST_LIBRARY_STATE}|${TEST_USER_STATE_NUM}`, `${TEST_LIBRARY_STATE}|${TEST_USER_STATE_NUM}`); msal = new UserAgentApplication(config); @@ -1276,8 +1286,8 @@ describe("UserAgentApplication.ts Class", function () { }); it("tests saveTokenForHash in case of login_required error code", function() { - window.location.hash = TemporaryCacheKeys.URL_HASH, TEST_HASHES.TEST_LOGIN_REQ_ERROR_HASH1 + TEST_USER_STATE_NUM; - cacheStorage.setItem(`${TemporaryCacheKeys.STATE_LOGIN}|RANDOM-GUID-HERE|${TEST_USER_STATE_NUM}`, "RANDOM-GUID-HERE|" + TEST_USER_STATE_NUM); + window.location.hash = testHashesForState(TEST_LIBRARY_STATE).TEST_LOGIN_REQ_ERROR_HASH1 + TEST_USER_STATE_NUM; + cacheStorage.setItem(`${TemporaryCacheKeys.STATE_LOGIN}|${TEST_LIBRARY_STATE}|${TEST_USER_STATE_NUM}`, `${TEST_LIBRARY_STATE}|${TEST_USER_STATE_NUM}`); msal = new UserAgentApplication(config); @@ -1294,8 +1304,8 @@ describe("UserAgentApplication.ts Class", function () { }); it("tests saveTokenForHash in case of login_required error code and description", function() { - window.location.hash = TemporaryCacheKeys.URL_HASH, TEST_HASHES.TEST_LOGIN_REQ_ERROR_HASH2 + TEST_USER_STATE_NUM; - cacheStorage.setItem(`${TemporaryCacheKeys.STATE_LOGIN}|RANDOM-GUID-HERE|${TEST_USER_STATE_NUM}`, "RANDOM-GUID-HERE|" + TEST_USER_STATE_NUM); + window.location.hash = testHashesForState(TEST_LIBRARY_STATE).TEST_LOGIN_REQ_ERROR_HASH2 + TEST_USER_STATE_NUM; + cacheStorage.setItem(`${TemporaryCacheKeys.STATE_LOGIN}|${TEST_LIBRARY_STATE}|${TEST_USER_STATE_NUM}`, `${TEST_LIBRARY_STATE}|${TEST_USER_STATE_NUM}`); msal = new UserAgentApplication(config); @@ -1314,8 +1324,8 @@ describe("UserAgentApplication.ts Class", function () { }); it("tests saveTokenForHash in case of consent_required error code", function() { - window.location.hash = TemporaryCacheKeys.URL_HASH, TEST_HASHES.TEST_CONSENT_REQ_ERROR_HASH1 + TEST_USER_STATE_NUM; - cacheStorage.setItem(`${TemporaryCacheKeys.STATE_LOGIN}|RANDOM-GUID-HERE|${TEST_USER_STATE_NUM}`, "RANDOM-GUID-HERE|" + TEST_USER_STATE_NUM); + window.location.hash = testHashesForState(TEST_LIBRARY_STATE).TEST_CONSENT_REQ_ERROR_HASH1 + TEST_USER_STATE_NUM; + cacheStorage.setItem(`${TemporaryCacheKeys.STATE_LOGIN}|${TEST_LIBRARY_STATE}|${TEST_USER_STATE_NUM}`, `${TEST_LIBRARY_STATE}|${TEST_USER_STATE_NUM}`); msal = new UserAgentApplication(config); @@ -1332,8 +1342,8 @@ describe("UserAgentApplication.ts Class", function () { }); it("tests saveTokenForHash in case of consent_required error code and description", function() { - window.location.hash = TemporaryCacheKeys.URL_HASH, TEST_HASHES.TEST_CONSENT_REQ_ERROR_HASH2 + TEST_USER_STATE_NUM; - cacheStorage.setItem(`${TemporaryCacheKeys.STATE_LOGIN}|RANDOM-GUID-HERE|${TEST_USER_STATE_NUM}`, "RANDOM-GUID-HERE|" + TEST_USER_STATE_NUM); + window.location.hash = testHashesForState(TEST_LIBRARY_STATE).TEST_CONSENT_REQ_ERROR_HASH2 + TEST_USER_STATE_NUM; + cacheStorage.setItem(`${TemporaryCacheKeys.STATE_LOGIN}|${TEST_LIBRARY_STATE}|${TEST_USER_STATE_NUM}`, `${TEST_LIBRARY_STATE}|${TEST_USER_STATE_NUM}`); msal = new UserAgentApplication(config); diff --git a/lib/msal-core/test/utils/RequestUtils.spec.ts b/lib/msal-core/test/utils/RequestUtils.spec.ts index 91beb5c61e..97b8f321fd 100644 --- a/lib/msal-core/test/utils/RequestUtils.spec.ts +++ b/lib/msal-core/test/utils/RequestUtils.spec.ts @@ -3,8 +3,10 @@ import { RequestUtils } from "../../src/utils/RequestUtils"; import { CryptoUtils } from "../../src/utils/CryptoUtils"; import { AuthenticationParameters } from "../../src/AuthenticationParameters"; import { ClientConfigurationError, ClientConfigurationErrorMessage } from "../../src/error/ClientConfigurationError"; -import { TEST_CONFIG } from "../TestConstants"; +import { TEST_CONFIG, TEST_TOKEN_LIFETIMES } from "../TestConstants"; import { StringDict } from "../../src/MsalTypes"; +import { TimeUtils } from "../../src/utils/TimeUtils"; +import sinon from "sinon"; describe("RequestUtils.ts class", () => { @@ -89,13 +91,36 @@ describe("RequestUtils.ts class", () => { }); it("validate and generate state", () => { + const nowStub = sinon.stub(TimeUtils, "now").returns(TEST_TOKEN_LIFETIMES.BASELINE_DATE_CHECK); const userState: string = "abcd"; const state: string = RequestUtils.validateAndGenerateState(userState); + const now = TimeUtils.now(); const splitKey: Array = state.split("|"); + expect(splitKey[1]).to.contain("abcd"); - expect(state).to.contain("|"); - expect(state).to.contain("abcd"); - expect(CryptoUtils.isGuid(splitKey[0])).to.be.equal(true); + const parsedState = RequestUtils.parseLibraryState(state); + expect(CryptoUtils.isGuid(parsedState.id)).to.be.equal(true); + expect(parsedState.ts).to.be.equal(now); + nowStub.restore(); + }); + + it("generates expected state if there is a delay between generating and parsing", function(done) { + this.timeout(5000); + + sinon.restore(); + const now = TimeUtils.now(); + const nowStub = sinon.stub(TimeUtils, "now").returns(now); + + const userState: string = "abcd"; + const state: string = RequestUtils.validateAndGenerateState(userState); + nowStub.restore(); + + // Mimicks tab suspending + setTimeout(() => { + const parsedState = RequestUtils.parseLibraryState(state); + expect(parsedState.ts).to.be.equal(now); + done(); + }, 4000); }); it("validate and generate correlationId", () => { @@ -112,7 +137,7 @@ describe("RequestUtils.ts class", () => { expect(request.scopes).to.be.equal(undefined); expect(request.prompt).to.be.equal(undefined); expect(request.extraQueryParameters).to.be.equal(undefined); - expect(CryptoUtils.isGuid(request.state)).to.be.equal(true); + expect(typeof request.state).to.be.equal("string"); expect(CryptoUtils.isGuid(request.correlationId)).to.be.equal(true); }); diff --git a/samples/react-sample-app/package-lock.json b/samples/react-sample-app/package-lock.json index 22856c45ff..d2763a48bd 100644 --- a/samples/react-sample-app/package-lock.json +++ b/samples/react-sample-app/package-lock.json @@ -8308,9 +8308,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "msal": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/msal/-/msal-1.2.1.tgz", - "integrity": "sha512-Zo28eyRtT/Un+zcpMfPtTPD+eo/OqzsRER0k5dyk8Mje/K1oLlaEOAgZHlJs59Y2xyuVg8OrcKqSn/1MeNjZYw==", + "version": "1.2.2-beta.3", + "resolved": "https://registry.npmjs.org/msal/-/msal-1.2.2-beta.3.tgz", + "integrity": "sha512-UifKAEPPCbXWaUx2UCyuXBJVdDt3DKx7qbo8HzU2p7JQ9qa1Ab9ycNGluOXc6Ik73aA9sqdPuyV5coQAuX2z9A==", "requires": { "tslib": "^1.9.3" } diff --git a/samples/react-sample-app/package.json b/samples/react-sample-app/package.json index 56b05f289c..b5597d25f3 100644 --- a/samples/react-sample-app/package.json +++ b/samples/react-sample-app/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { - "msal": "^1.0.0", + "msal": "^1.2.2-beta.3", "prop-types": "^15.7.2", "react": "^16.8.6", "react-app-polyfill": "^1.0.1",