Skip to content

Commit

Permalink
Merge pull request #1395 from AzureAD/encoded-state-iat
Browse files Browse the repository at this point in the history
Turn library state into encoded string that contains guid and timestamp
  • Loading branch information
jasonnutter authored Mar 30, 2020
2 parents d558bdb + 9b568bf commit 2fce441
Show file tree
Hide file tree
Showing 10 changed files with 181 additions and 88 deletions.
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

0 comments on commit 2fce441

Please sign in to comment.