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

Turn library state into encoded string that contains guid and timestamp #1395

Merged
merged 13 commits into from
Mar 30, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions lib/msal-core/src/ScopeSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import { ClientConfigurationError } from "./error/ClientConfigurationError";
import { AuthenticationParameters } from "./AuthenticationParameters";
import { Constants } from "./utils/Constants";

export class ScopeSet {

Expand Down Expand Up @@ -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);
}
Expand Down
21 changes: 13 additions & 8 deletions lib/msal-core/src/UserAgentApplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export interface CacheResult {
*/
export type ResponseStateInfo = {
state: string;
timestamp: number,
stateMatch: boolean;
requestType: string;
};
Expand Down Expand Up @@ -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.
*/
Expand All @@ -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 = "";

Expand Down Expand Up @@ -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 {
Expand All @@ -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;
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);
}
Expand Down
1 change: 1 addition & 0 deletions lib/msal-core/src/utils/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export class Constants {
*/
export enum ServerHashParamKeys {
SCOPE = "scope",
STATE = "state",
ERROR = "error",
ERROR_DESCRIPTION = "error_description",
ACCESS_TOKEN = "access_token",
Expand Down
58 changes: 54 additions & 4 deletions lib/msal-core/src/utils/RequestUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
}

/**
Expand Down
2 changes: 1 addition & 1 deletion lib/msal-core/src/utils/TimeUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
24 changes: 13 additions & 11 deletions lib/msal-core/test/TestConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Loading