From 33ac6a7c3e3f48693f0b2d70c3ee2addfe41ae1f Mon Sep 17 00:00:00 2001 From: Hanyuan Zhang Date: Thu, 9 Feb 2023 15:16:05 -0800 Subject: [PATCH 01/11] Added code to hit /idp/idx/device/challenge remediation after /idp/idx/introspect --- lib/idx/flow/AuthenticationFlow.ts | 2 + lib/idx/getDeviceChallenge.ts | 66 +++++++++++++++++++ lib/idx/index.ts | 1 + .../DeviceIdentificationChallenge.ts | 22 +++++++ lib/idx/remediators/index.ts | 1 + lib/idx/run.ts | 21 ++++++ 6 files changed, 113 insertions(+) create mode 100644 lib/idx/getDeviceChallenge.ts create mode 100644 lib/idx/remediators/DeviceIdentificationChallenge.ts diff --git a/lib/idx/flow/AuthenticationFlow.ts b/lib/idx/flow/AuthenticationFlow.ts index e14d3bc8a..850a4b49a 100644 --- a/lib/idx/flow/AuthenticationFlow.ts +++ b/lib/idx/flow/AuthenticationFlow.ts @@ -14,6 +14,7 @@ import { RemediationFlow } from './RemediationFlow'; import { Identify, + DeviceIdentificationChallenge, SelectAuthenticatorAuthenticate, ChallengeAuthenticator, ReEnrollAuthenticator, @@ -30,6 +31,7 @@ import { } from '../remediators'; export const AuthenticationFlow: RemediationFlow = { + 'device-identification-challenge': DeviceIdentificationChallenge, 'identify': Identify, 'select-authenticator-authenticate': SelectAuthenticatorAuthenticate, 'select-authenticator-enroll': SelectAuthenticatorEnroll, diff --git a/lib/idx/getDeviceChallenge.ts b/lib/idx/getDeviceChallenge.ts new file mode 100644 index 000000000..57929a527 --- /dev/null +++ b/lib/idx/getDeviceChallenge.ts @@ -0,0 +1,66 @@ +/* eslint-disable complexity */ +/*! + * Copyright (c) 2021, Okta, Inc. and/or its affiliates. All rights reserved. + * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") + * + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and limitations under the License. + */ + +import { makeIdxState, validateVersionConfig } from './idxState'; +import { IntrospectOptions, OktaAuthIdxInterface } from './types'; +import { IdxRemediation, IdxResponse, isRawIdxResponse } from './types/idx-js'; +import { IDX_API_VERSION } from '../constants'; +import { httpRequest } from '../http'; +import { isAuthApiError } from '../errors'; + +export async function getDeviceChallenge ( + authClient: OktaAuthIdxInterface, + remediation: IdxRemediation, + options: IntrospectOptions = {} +): Promise { + let rawIdxResponse; + let requestDidSucceed; + + // try load from storage first + const savedIdxResponse = authClient.transactionManager.loadIdxResponse(options); + if (savedIdxResponse) { + rawIdxResponse = savedIdxResponse.rawIdxResponse; + requestDidSucceed = savedIdxResponse.requestDidSucceed; + } + + if (!rawIdxResponse) { + const version = options.version || IDX_API_VERSION; + const withCredentials = false; + try { + requestDidSucceed = true; + validateVersionConfig(version); + // remediation.href already contains domain, otherwise get domain from getOAuthDomain(authClient); + const url = remediation.href; + const headers = { + 'Content-Type': `application/ion+json; okta-version=${version}`, + Accept: `application/ion+json; okta-version=${version}`, + }; + rawIdxResponse = await httpRequest(authClient, { + method: 'GET', + url, + headers, + withCredentials + }); + } catch (err) { + if (isAuthApiError(err) && err.xhr && isRawIdxResponse(err.xhr.responseJSON)) { + rawIdxResponse = err.xhr.responseJSON; + requestDidSucceed = false; + } else { + throw err; + } + } + } + + const { withCredentials } = options; + return makeIdxState(authClient, rawIdxResponse, { withCredentials }, requestDidSucceed); +} diff --git a/lib/idx/index.ts b/lib/idx/index.ts index 6e319fcc8..18b2ce1e3 100644 --- a/lib/idx/index.ts +++ b/lib/idx/index.ts @@ -21,6 +21,7 @@ export { } from './emailVerify'; export { interact } from './interact'; export { introspect } from './introspect'; +export { getDeviceChallenge } from './getDeviceChallenge'; export { poll } from './poll'; export { proceed, canProceed } from './proceed'; export { register } from './register'; diff --git a/lib/idx/remediators/DeviceIdentificationChallenge.ts b/lib/idx/remediators/DeviceIdentificationChallenge.ts new file mode 100644 index 000000000..a67b9b508 --- /dev/null +++ b/lib/idx/remediators/DeviceIdentificationChallenge.ts @@ -0,0 +1,22 @@ +/*! + * Copyright (c) 2015-present, Okta, Inc. and/or its affiliates. All rights reserved. + * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") + * + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and limitations under the License. + */ + + +// import { Credentials } from '../authenticator'; +import { Remediator, RemediationValues } from './Base/Remediator'; +export class DeviceIdentificationChallenge extends Remediator { + static remediationName = 'device-identification-challenge'; + + canRemediate(): boolean { + return false; + } +} diff --git a/lib/idx/remediators/index.ts b/lib/idx/remediators/index.ts index 100051a61..ea990d77d 100644 --- a/lib/idx/remediators/index.ts +++ b/lib/idx/remediators/index.ts @@ -21,6 +21,7 @@ export * from './ChallengePoll'; export * from './ResetAuthenticator'; export * from './EnrollProfile'; export * from './Identify'; +export * from './DeviceIdentificationChallenge'; export * from './ReEnrollAuthenticator'; export * from './RedirectIdp'; export * from './SelectAuthenticatorAuthenticate'; diff --git a/lib/idx/run.ts b/lib/idx/run.ts index b7550ce00..5da71d6b7 100644 --- a/lib/idx/run.ts +++ b/lib/idx/run.ts @@ -15,6 +15,7 @@ /* eslint-disable max-statements, complexity, max-depth */ import { interact } from './interact'; import { introspect } from './introspect'; +import { getDeviceChallenge } from './getDeviceChallenge'; import { remediate } from './remediate'; import { getFlowSpecification } from './flow'; import * as remediators from './remediators'; @@ -32,6 +33,7 @@ import { getSavedTransactionMeta, saveTransactionMeta } from './transactionMeta' import { getAvailableSteps, getEnabledFeatures, getMessagesFromResponse, isTerminalResponse } from './util'; import { Tokens } from '../oidc/types'; import { APIError } from '../errors/types'; +import { DeviceIdentificationChallenge } from './remediators'; declare interface RunData { options: RunOptions; values: remediators.RemediationValues; @@ -154,6 +156,24 @@ async function getDataFromIntrospect(authClient, data: RunData): Promise { + const { options } = data; + const { + withCredentials, + version + } = options; + + const remediations = data.idxResponse?.rawIdxState.remediation?.value; + remediations?.forEach(async remediation => { + if (remediation['name'] == DeviceIdentificationChallenge.remediationName) { + // get challenge from Google VA api + const idxResponse = await getDeviceChallenge(authClient, remediation, { withCredentials, version }); + return { ...data, idxResponse }; + } + }); + return data; +} + async function getDataFromRemediate(authClient, data: RunData): Promise { let { idxResponse, @@ -308,6 +328,7 @@ export async function run( data = initializeData(authClient, data); data = await getDataFromIntrospect(authClient, data); + data = await getDataFromDeviceChallenge(authClient, data); data = await getDataFromRemediate(authClient, data); data = await finalizeData(authClient, data); From 7198526ff443d7c8014581223d60f62a2f56ff10 Mon Sep 17 00:00:00 2001 From: Hanyuan Zhang Date: Mon, 13 Feb 2023 10:25:06 -0800 Subject: [PATCH 02/11] Added code to hit /idp/idx/device/challenge-response, a little cleanup --- lib/idx/getDeviceChallenge.ts | 18 +++++----- lib/idx/getDeviceChallengeResponse.ts | 52 +++++++++++++++++++++++++++ lib/idx/index.ts | 1 + lib/idx/run.ts | 15 +++++--- 4 files changed, 72 insertions(+), 14 deletions(-) create mode 100644 lib/idx/getDeviceChallengeResponse.ts diff --git a/lib/idx/getDeviceChallenge.ts b/lib/idx/getDeviceChallenge.ts index 57929a527..1faa57204 100644 --- a/lib/idx/getDeviceChallenge.ts +++ b/lib/idx/getDeviceChallenge.ts @@ -11,9 +11,9 @@ * See the License for the specific language governing permissions and limitations under the License. */ -import { makeIdxState, validateVersionConfig } from './idxState'; +import { validateVersionConfig } from './idxState'; import { IntrospectOptions, OktaAuthIdxInterface } from './types'; -import { IdxRemediation, IdxResponse, isRawIdxResponse } from './types/idx-js'; +import { IdxRemediation, isRawIdxResponse } from './types/idx-js'; import { IDX_API_VERSION } from '../constants'; import { httpRequest } from '../http'; import { isAuthApiError } from '../errors'; @@ -22,28 +22,27 @@ export async function getDeviceChallenge ( authClient: OktaAuthIdxInterface, remediation: IdxRemediation, options: IntrospectOptions = {} -): Promise { +): Promise { let rawIdxResponse; - let requestDidSucceed; // try load from storage first const savedIdxResponse = authClient.transactionManager.loadIdxResponse(options); if (savedIdxResponse) { rawIdxResponse = savedIdxResponse.rawIdxResponse; - requestDidSucceed = savedIdxResponse.requestDidSucceed; } if (!rawIdxResponse) { const version = options.version || IDX_API_VERSION; const withCredentials = false; try { - requestDidSucceed = true; validateVersionConfig(version); // remediation.href already contains domain, otherwise get domain from getOAuthDomain(authClient); const url = remediation.href; + // TODO: remove x-device-trust header, it should be added by managed Chrome browser const headers = { 'Content-Type': `application/ion+json; okta-version=${version}`, Accept: `application/ion+json; okta-version=${version}`, + 'x-device-trust': 'VerifiedAccess' }; rawIdxResponse = await httpRequest(authClient, { method: 'GET', @@ -54,13 +53,12 @@ export async function getDeviceChallenge ( } catch (err) { if (isAuthApiError(err) && err.xhr && isRawIdxResponse(err.xhr.responseJSON)) { rawIdxResponse = err.xhr.responseJSON; - requestDidSucceed = false; + // requestDidSucceed = false; } else { throw err; } } } - - const { withCredentials } = options; - return makeIdxState(authClient, rawIdxResponse, { withCredentials }, requestDidSucceed); + // remove 'redirect:' to avoid CSP violation + return rawIdxResponse.substring('redirect:'.length); } diff --git a/lib/idx/getDeviceChallengeResponse.ts b/lib/idx/getDeviceChallengeResponse.ts new file mode 100644 index 000000000..457d87a3a --- /dev/null +++ b/lib/idx/getDeviceChallengeResponse.ts @@ -0,0 +1,52 @@ +/* eslint-disable complexity */ +/*! + * Copyright (c) 2021, Okta, Inc. and/or its affiliates. All rights reserved. + * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") + * + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and limitations under the License. + */ + +import { validateVersionConfig } from './idxState'; +import { IntrospectOptions, OktaAuthIdxInterface } from './types'; +import { isRawIdxResponse } from './types/idx-js'; +import { IDX_API_VERSION } from '../constants'; +import { httpRequest } from '../http'; +import { isAuthApiError } from '../errors'; + +export async function getDeviceChallengeResponse ( + authClient: OktaAuthIdxInterface, + url: string, + options: IntrospectOptions = {} +): Promise { + let rawIdxResponse; + + if (!rawIdxResponse) { + const version = options.version || IDX_API_VERSION; + const withCredentials = false; + try { + validateVersionConfig(version); + const headers = { + 'Content-Type': `application/ion+json; okta-version=${version}`, + Accept: `application/ion+json; okta-version=${version}` + }; + // Managed Chrome should add the x-device-challenge-response header + rawIdxResponse = await httpRequest(authClient, { + method: 'GET', + url, + headers, + withCredentials + }); + } catch (err) { + if (isAuthApiError(err) && err.xhr && isRawIdxResponse(err.xhr.responseJSON)) { + rawIdxResponse = err.xhr.responseJSON; + } else { + throw err; + } + } + } +} diff --git a/lib/idx/index.ts b/lib/idx/index.ts index 18b2ce1e3..cc64f5753 100644 --- a/lib/idx/index.ts +++ b/lib/idx/index.ts @@ -22,6 +22,7 @@ export { export { interact } from './interact'; export { introspect } from './introspect'; export { getDeviceChallenge } from './getDeviceChallenge'; +export { getDeviceChallengeResponse } from './getDeviceChallengeResponse'; export { poll } from './poll'; export { proceed, canProceed } from './proceed'; export { register } from './register'; diff --git a/lib/idx/run.ts b/lib/idx/run.ts index 5da71d6b7..a5f56a11e 100644 --- a/lib/idx/run.ts +++ b/lib/idx/run.ts @@ -16,6 +16,7 @@ import { interact } from './interact'; import { introspect } from './introspect'; import { getDeviceChallenge } from './getDeviceChallenge'; +import { getDeviceChallengeResponse } from './getDeviceChallengeResponse'; import { remediate } from './remediate'; import { getFlowSpecification } from './flow'; import * as remediators from './remediators'; @@ -156,7 +157,7 @@ async function getDataFromIntrospect(authClient, data: RunData): Promise { +async function getDataFromDeviceChallenge(authClient, data: RunData): Promise { const { options } = data; const { withCredentials, @@ -168,10 +169,13 @@ async function getDataFromDeviceChallenge(authClient, data: RunData): Promise { @@ -328,7 +332,10 @@ export async function run( data = initializeData(authClient, data); data = await getDataFromIntrospect(authClient, data); - data = await getDataFromDeviceChallenge(authClient, data); + + // data = await getDataFromDeviceChallenge(authClient, data); + await getDataFromDeviceChallenge(authClient, data); + data = await getDataFromRemediate(authClient, data); data = await finalizeData(authClient, data); From e1ca380fc4ffef36985e3a57c0b8e61da50b59a5 Mon Sep 17 00:00:00 2001 From: Hanyuan Zhang Date: Mon, 13 Feb 2023 10:28:56 -0800 Subject: [PATCH 03/11] Cleanup --- lib/idx/getDeviceChallenge.ts | 1 - lib/idx/remediators/DeviceIdentificationChallenge.ts | 1 - lib/idx/run.ts | 11 ++++++----- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/idx/getDeviceChallenge.ts b/lib/idx/getDeviceChallenge.ts index 1faa57204..9d7793c42 100644 --- a/lib/idx/getDeviceChallenge.ts +++ b/lib/idx/getDeviceChallenge.ts @@ -53,7 +53,6 @@ export async function getDeviceChallenge ( } catch (err) { if (isAuthApiError(err) && err.xhr && isRawIdxResponse(err.xhr.responseJSON)) { rawIdxResponse = err.xhr.responseJSON; - // requestDidSucceed = false; } else { throw err; } diff --git a/lib/idx/remediators/DeviceIdentificationChallenge.ts b/lib/idx/remediators/DeviceIdentificationChallenge.ts index a67b9b508..417633c2f 100644 --- a/lib/idx/remediators/DeviceIdentificationChallenge.ts +++ b/lib/idx/remediators/DeviceIdentificationChallenge.ts @@ -11,7 +11,6 @@ */ -// import { Credentials } from '../authenticator'; import { Remediator, RemediationValues } from './Base/Remediator'; export class DeviceIdentificationChallenge extends Remediator { static remediationName = 'device-identification-challenge'; diff --git a/lib/idx/run.ts b/lib/idx/run.ts index a5f56a11e..4d6df4aec 100644 --- a/lib/idx/run.ts +++ b/lib/idx/run.ts @@ -168,12 +168,14 @@ async function getDataFromDeviceChallenge(authClient, data: RunData): Promise { if (remediation['name'] == DeviceIdentificationChallenge.remediationName) { // get challenge from Google VA api - const idxResponse = await getDeviceChallenge(authClient, remediation, { withCredentials, version }); + const redirectUrl = await getDeviceChallenge(authClient, remediation, { withCredentials, version }); // Managed Chrome should generate challenge-response, send it as the value of // x-device-challenge-response header, okta-core will verify the challenge-response // and get device signals from Google VA api - await getDeviceChallengeResponse(authClient, idxResponse, { withCredentials, version }); - return idxResponse; + if (redirectUrl) { + await getDeviceChallengeResponse(authClient, redirectUrl, { withCredentials, version }); + } + return redirectUrl; } }); } @@ -333,8 +335,7 @@ export async function run( data = initializeData(authClient, data); data = await getDataFromIntrospect(authClient, data); - // data = await getDataFromDeviceChallenge(authClient, data); - await getDataFromDeviceChallenge(authClient, data); + await getDataFromDeviceChallenge(authClient, data); data = await getDataFromRemediate(authClient, data); data = await finalizeData(authClient, data); From b21a9447dd557487cd6a395cc47cd09d78c99465 Mon Sep 17 00:00:00 2001 From: Hanyuan Zhang Date: Wed, 22 Feb 2023 09:41:13 -0800 Subject: [PATCH 04/11] replaced XHR with iFrame, 2nd GET still does not have correct header --- lib/idx/getDeviceChallenge.ts | 33 ++++++++++----------------- lib/idx/getDeviceChallengeResponse.ts | 21 +++++++---------- lib/idx/run.ts | 18 ++++++++------- lib/oidc/util/browser.ts | 13 +++++++++++ 4 files changed, 43 insertions(+), 42 deletions(-) diff --git a/lib/idx/getDeviceChallenge.ts b/lib/idx/getDeviceChallenge.ts index 9d7793c42..320221175 100644 --- a/lib/idx/getDeviceChallenge.ts +++ b/lib/idx/getDeviceChallenge.ts @@ -15,49 +15,40 @@ import { validateVersionConfig } from './idxState'; import { IntrospectOptions, OktaAuthIdxInterface } from './types'; import { IdxRemediation, isRawIdxResponse } from './types/idx-js'; import { IDX_API_VERSION } from '../constants'; -import { httpRequest } from '../http'; import { isAuthApiError } from '../errors'; +import { loadInvisibleFrame } from '../oidc/util/browser'; export async function getDeviceChallenge ( authClient: OktaAuthIdxInterface, remediation: IdxRemediation, options: IntrospectOptions = {} ): Promise { - let rawIdxResponse; + let response; // try load from storage first const savedIdxResponse = authClient.transactionManager.loadIdxResponse(options); if (savedIdxResponse) { - rawIdxResponse = savedIdxResponse.rawIdxResponse; + response = savedIdxResponse.rawIdxResponse; } - if (!rawIdxResponse) { + if (!response) { const version = options.version || IDX_API_VERSION; - const withCredentials = false; try { validateVersionConfig(version); - // remediation.href already contains domain, otherwise get domain from getOAuthDomain(authClient); const url = remediation.href; - // TODO: remove x-device-trust header, it should be added by managed Chrome browser - const headers = { - 'Content-Type': `application/ion+json; okta-version=${version}`, - Accept: `application/ion+json; okta-version=${version}`, - 'x-device-trust': 'VerifiedAccess' - }; - rawIdxResponse = await httpRequest(authClient, { - method: 'GET', - url, - headers, - withCredentials - }); + + // Test if this triggers Cross-Document Navigations + const iFrameId = 'deviceChallengeIFrameId'; + response = loadInvisibleFrame(url, iFrameId); + // TODO: need to figure out how to get the content of the iFrame (basically the url for the next GET endpoint) + // var iFrame = document.getElementById(iFrameId); } catch (err) { if (isAuthApiError(err) && err.xhr && isRawIdxResponse(err.xhr.responseJSON)) { - rawIdxResponse = err.xhr.responseJSON; + return JSON.stringify(err.xhr.responseJSON); } else { throw err; } } } - // remove 'redirect:' to avoid CSP violation - return rawIdxResponse.substring('redirect:'.length); + return response.contentWindow?.document.body.innerHTML; } diff --git a/lib/idx/getDeviceChallengeResponse.ts b/lib/idx/getDeviceChallengeResponse.ts index 457d87a3a..ef0106f15 100644 --- a/lib/idx/getDeviceChallengeResponse.ts +++ b/lib/idx/getDeviceChallengeResponse.ts @@ -15,8 +15,8 @@ import { validateVersionConfig } from './idxState'; import { IntrospectOptions, OktaAuthIdxInterface } from './types'; import { isRawIdxResponse } from './types/idx-js'; import { IDX_API_VERSION } from '../constants'; -import { httpRequest } from '../http'; import { isAuthApiError } from '../errors'; +import { loadInvisibleFrame } from '../oidc/util/browser'; export async function getDeviceChallengeResponse ( authClient: OktaAuthIdxInterface, @@ -25,22 +25,17 @@ export async function getDeviceChallengeResponse ( ): Promise { let rawIdxResponse; + // try load from storage first + const savedIdxResponse = authClient.transactionManager.loadIdxResponse(options); + if (savedIdxResponse) { + rawIdxResponse = savedIdxResponse.rawIdxResponse; + } + if (!rawIdxResponse) { const version = options.version || IDX_API_VERSION; - const withCredentials = false; try { validateVersionConfig(version); - const headers = { - 'Content-Type': `application/ion+json; okta-version=${version}`, - Accept: `application/ion+json; okta-version=${version}` - }; - // Managed Chrome should add the x-device-challenge-response header - rawIdxResponse = await httpRequest(authClient, { - method: 'GET', - url, - headers, - withCredentials - }); + loadInvisibleFrame(url, 'deviceChallengeResponseIFrameId'); } catch (err) { if (isAuthApiError(err) && err.xhr && isRawIdxResponse(err.xhr.responseJSON)) { rawIdxResponse = err.xhr.responseJSON; diff --git a/lib/idx/run.ts b/lib/idx/run.ts index 4d6df4aec..d6da96a73 100644 --- a/lib/idx/run.ts +++ b/lib/idx/run.ts @@ -157,7 +157,7 @@ async function getDataFromIntrospect(authClient, data: RunData): Promise { +async function collectChromeDeviceSignals(authClient, data: RunData): Promise { const { options } = data; const { withCredentials, @@ -168,14 +168,16 @@ async function getDataFromDeviceChallenge(authClient, data: RunData): Promise { if (remediation['name'] == DeviceIdentificationChallenge.remediationName) { // get challenge from Google VA api - const redirectUrl = await getDeviceChallenge(authClient, remediation, { withCredentials, version }); - // Managed Chrome should generate challenge-response, send it as the value of - // x-device-challenge-response header, okta-core will verify the challenge-response - // and get device signals from Google VA api - if (redirectUrl) { + await getDeviceChallenge(authClient, remediation, { withCredentials, version }); + const redirectUrl = remediation.href?.replace('/challenge', '/challenge-response'); + console.log('2nd get endpoint: ' + redirectUrl); + if (redirectUrl) { // TODO: error check in case getDeviceChallenge errors out + // Managed Chrome should generate challenge-response, send it as the value of + // x-device-challenge-response header, okta-core will verify the challenge-response + // and get device signals from Google VA api + // TODO: make use of the idxResponse here await getDeviceChallengeResponse(authClient, redirectUrl, { withCredentials, version }); } - return redirectUrl; } }); } @@ -335,7 +337,7 @@ export async function run( data = initializeData(authClient, data); data = await getDataFromIntrospect(authClient, data); - await getDataFromDeviceChallenge(authClient, data); + await collectChromeDeviceSignals(authClient, data); data = await getDataFromRemediate(authClient, data); data = await finalizeData(authClient, data); diff --git a/lib/oidc/util/browser.ts b/lib/oidc/util/browser.ts index 7e63b0612..e5f9b406e 100644 --- a/lib/oidc/util/browser.ts +++ b/lib/oidc/util/browser.ts @@ -39,6 +39,19 @@ export function loadFrame(src) { return document.body.appendChild(iframe); } +export function loadInvisibleFrame(src, id) { + var iframe = document.createElement('iframe'); + iframe.src = src; + iframe.id = id; + // invisible and not take up any space + iframe.height = '0'; + iframe.width = '0'; + iframe.style.position = 'absolute'; + iframe.style.border = '0'; + + return document.body.appendChild(iframe); +} + export function loadPopup(src, options) { var title = options.popupTitle || 'External Identity Provider User Authentication'; var appearance = 'toolbar=no, scrollbars=yes, resizable=yes, ' + From 4f7140a7708203fd8353e8ad1aff52be576c0c4b Mon Sep 17 00:00:00 2001 From: Hanyuan Zhang Date: Wed, 22 Feb 2023 17:47:45 -0800 Subject: [PATCH 05/11] clean up --- lib/idx/run.ts | 11 ----------- lib/oidc/util/browser.ts | 4 ++++ 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/lib/idx/run.ts b/lib/idx/run.ts index d6da96a73..1137d153a 100644 --- a/lib/idx/run.ts +++ b/lib/idx/run.ts @@ -16,7 +16,6 @@ import { interact } from './interact'; import { introspect } from './introspect'; import { getDeviceChallenge } from './getDeviceChallenge'; -import { getDeviceChallengeResponse } from './getDeviceChallengeResponse'; import { remediate } from './remediate'; import { getFlowSpecification } from './flow'; import * as remediators from './remediators'; @@ -167,17 +166,7 @@ async function collectChromeDeviceSignals(authClient, data: RunData): Promise { if (remediation['name'] == DeviceIdentificationChallenge.remediationName) { - // get challenge from Google VA api await getDeviceChallenge(authClient, remediation, { withCredentials, version }); - const redirectUrl = remediation.href?.replace('/challenge', '/challenge-response'); - console.log('2nd get endpoint: ' + redirectUrl); - if (redirectUrl) { // TODO: error check in case getDeviceChallenge errors out - // Managed Chrome should generate challenge-response, send it as the value of - // x-device-challenge-response header, okta-core will verify the challenge-response - // and get device signals from Google VA api - // TODO: make use of the idxResponse here - await getDeviceChallengeResponse(authClient, redirectUrl, { withCredentials, version }); - } } }); } diff --git a/lib/oidc/util/browser.ts b/lib/oidc/util/browser.ts index e5f9b406e..41bf21825 100644 --- a/lib/oidc/util/browser.ts +++ b/lib/oidc/util/browser.ts @@ -49,6 +49,10 @@ export function loadInvisibleFrame(src, id) { iframe.style.position = 'absolute'; iframe.style.border = '0'; + iframe.onload = function() { + console.log('+++location=' + iframe.contentWindow?.location); + }; + return document.body.appendChild(iframe); } From c1c8267123742c21ef3a063c7bd72dfebf6f972d Mon Sep 17 00:00:00 2001 From: Hanyuan Zhang Date: Wed, 22 Feb 2023 17:49:07 -0800 Subject: [PATCH 06/11] clean up --- lib/idx/getDeviceChallenge.ts | 3 --- lib/oidc/util/browser.ts | 4 ---- 2 files changed, 7 deletions(-) diff --git a/lib/idx/getDeviceChallenge.ts b/lib/idx/getDeviceChallenge.ts index 320221175..35a117f6b 100644 --- a/lib/idx/getDeviceChallenge.ts +++ b/lib/idx/getDeviceChallenge.ts @@ -37,11 +37,8 @@ export async function getDeviceChallenge ( validateVersionConfig(version); const url = remediation.href; - // Test if this triggers Cross-Document Navigations const iFrameId = 'deviceChallengeIFrameId'; response = loadInvisibleFrame(url, iFrameId); - // TODO: need to figure out how to get the content of the iFrame (basically the url for the next GET endpoint) - // var iFrame = document.getElementById(iFrameId); } catch (err) { if (isAuthApiError(err) && err.xhr && isRawIdxResponse(err.xhr.responseJSON)) { return JSON.stringify(err.xhr.responseJSON); diff --git a/lib/oidc/util/browser.ts b/lib/oidc/util/browser.ts index 41bf21825..e5f9b406e 100644 --- a/lib/oidc/util/browser.ts +++ b/lib/oidc/util/browser.ts @@ -49,10 +49,6 @@ export function loadInvisibleFrame(src, id) { iframe.style.position = 'absolute'; iframe.style.border = '0'; - iframe.onload = function() { - console.log('+++location=' + iframe.contentWindow?.location); - }; - return document.body.appendChild(iframe); } From 11dec0c8ece74df80af69aa666eb242be84b7613 Mon Sep 17 00:00:00 2001 From: Hanyuan Zhang Date: Thu, 23 Feb 2023 14:31:20 -0800 Subject: [PATCH 07/11] Clean up --- lib/idx/getDeviceChallengeResponse.ts | 47 --------------------------- lib/idx/index.ts | 1 - lib/idx/run.ts | 1 + 3 files changed, 1 insertion(+), 48 deletions(-) delete mode 100644 lib/idx/getDeviceChallengeResponse.ts diff --git a/lib/idx/getDeviceChallengeResponse.ts b/lib/idx/getDeviceChallengeResponse.ts deleted file mode 100644 index ef0106f15..000000000 --- a/lib/idx/getDeviceChallengeResponse.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* eslint-disable complexity */ -/*! - * Copyright (c) 2021, Okta, Inc. and/or its affiliates. All rights reserved. - * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") - * - * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * - * See the License for the specific language governing permissions and limitations under the License. - */ - -import { validateVersionConfig } from './idxState'; -import { IntrospectOptions, OktaAuthIdxInterface } from './types'; -import { isRawIdxResponse } from './types/idx-js'; -import { IDX_API_VERSION } from '../constants'; -import { isAuthApiError } from '../errors'; -import { loadInvisibleFrame } from '../oidc/util/browser'; - -export async function getDeviceChallengeResponse ( - authClient: OktaAuthIdxInterface, - url: string, - options: IntrospectOptions = {} -): Promise { - let rawIdxResponse; - - // try load from storage first - const savedIdxResponse = authClient.transactionManager.loadIdxResponse(options); - if (savedIdxResponse) { - rawIdxResponse = savedIdxResponse.rawIdxResponse; - } - - if (!rawIdxResponse) { - const version = options.version || IDX_API_VERSION; - try { - validateVersionConfig(version); - loadInvisibleFrame(url, 'deviceChallengeResponseIFrameId'); - } catch (err) { - if (isAuthApiError(err) && err.xhr && isRawIdxResponse(err.xhr.responseJSON)) { - rawIdxResponse = err.xhr.responseJSON; - } else { - throw err; - } - } - } -} diff --git a/lib/idx/index.ts b/lib/idx/index.ts index cc64f5753..18b2ce1e3 100644 --- a/lib/idx/index.ts +++ b/lib/idx/index.ts @@ -22,7 +22,6 @@ export { export { interact } from './interact'; export { introspect } from './introspect'; export { getDeviceChallenge } from './getDeviceChallenge'; -export { getDeviceChallengeResponse } from './getDeviceChallengeResponse'; export { poll } from './poll'; export { proceed, canProceed } from './proceed'; export { register } from './register'; diff --git a/lib/idx/run.ts b/lib/idx/run.ts index 1137d153a..9a313ce35 100644 --- a/lib/idx/run.ts +++ b/lib/idx/run.ts @@ -166,6 +166,7 @@ async function collectChromeDeviceSignals(authClient, data: RunData): Promise { if (remediation['name'] == DeviceIdentificationChallenge.remediationName) { + // TODO: get the idxResopnse from the 2nd GET endpoint (1st GET receives a 302) and process it as usual await getDeviceChallenge(authClient, remediation, { withCredentials, version }); } }); From f7c7db8cd59d8817684ed91383a4f2597cb327d5 Mon Sep 17 00:00:00 2001 From: Hanyuan Zhang Date: Sun, 26 Feb 2023 15:37:31 -0800 Subject: [PATCH 08/11] redirect over iFrame --- lib/idx/getDeviceChallenge.ts | 15 ++++++++------- lib/idx/run.ts | 8 +++++++- lib/oidc/util/browser.ts | 4 ++++ 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/lib/idx/getDeviceChallenge.ts b/lib/idx/getDeviceChallenge.ts index 35a117f6b..4a9111351 100644 --- a/lib/idx/getDeviceChallenge.ts +++ b/lib/idx/getDeviceChallenge.ts @@ -16,13 +16,14 @@ import { IntrospectOptions, OktaAuthIdxInterface } from './types'; import { IdxRemediation, isRawIdxResponse } from './types/idx-js'; import { IDX_API_VERSION } from '../constants'; import { isAuthApiError } from '../errors'; -import { loadInvisibleFrame } from '../oidc/util/browser'; +// import { loadInvisibleFrame } from '../oidc/util/browser'; +import { redirect } from '../oidc/util/browser'; export async function getDeviceChallenge ( authClient: OktaAuthIdxInterface, remediation: IdxRemediation, options: IntrospectOptions = {} -): Promise { +): Promise { let response; // try load from storage first @@ -36,16 +37,16 @@ export async function getDeviceChallenge ( try { validateVersionConfig(version); const url = remediation.href; - - const iFrameId = 'deviceChallengeIFrameId'; - response = loadInvisibleFrame(url, iFrameId); + redirect(url); + // const iFrameId = 'deviceChallengeIFrameId'; + // response = loadInvisibleFrame(url, iFrameId); } catch (err) { if (isAuthApiError(err) && err.xhr && isRawIdxResponse(err.xhr.responseJSON)) { - return JSON.stringify(err.xhr.responseJSON); + console.log(err.xhr.responseJSON); + throw new Error('Auth Api Error'); } else { throw err; } } } - return response.contentWindow?.document.body.innerHTML; } diff --git a/lib/idx/run.ts b/lib/idx/run.ts index 9a313ce35..1052de993 100644 --- a/lib/idx/run.ts +++ b/lib/idx/run.ts @@ -158,7 +158,9 @@ async function getDataFromIntrospect(authClient, data: RunData): Promise { const { options } = data; + let idxResponse; const { + stateHandle, withCredentials, version } = options; @@ -166,8 +168,11 @@ async function collectChromeDeviceSignals(authClient, data: RunData): Promise { if (remediation['name'] == DeviceIdentificationChallenge.remediationName) { - // TODO: get the idxResopnse from the 2nd GET endpoint (1st GET receives a 302) and process it as usual await getDeviceChallenge(authClient, remediation, { withCredentials, version }); + // TODO: the following calls /idp/idx/introspect again right after the 1st GET returns, before hitting the 2nd GET + // TODO: Is it ok to reuse the remediation from the previous introspect? If so there is no need to call it again + idxResponse = await introspect(authClient, { withCredentials, version, stateHandle }); + return { ...data, idxResponse }; } }); } @@ -327,6 +332,7 @@ export async function run( data = initializeData(authClient, data); data = await getDataFromIntrospect(authClient, data); + // collect device signals if elibigle await collectChromeDeviceSignals(authClient, data); data = await getDataFromRemediate(authClient, data); diff --git a/lib/oidc/util/browser.ts b/lib/oidc/util/browser.ts index e5f9b406e..80fb1ad66 100644 --- a/lib/oidc/util/browser.ts +++ b/lib/oidc/util/browser.ts @@ -52,6 +52,10 @@ export function loadInvisibleFrame(src, id) { return document.body.appendChild(iframe); } +export function redirect(url) { + window.location.href = url; +} + export function loadPopup(src, options) { var title = options.popupTitle || 'External Identity Provider User Authentication'; var appearance = 'toolbar=no, scrollbars=yes, resizable=yes, ' + From f0858839ed6353170cae50ef7ae5fcf71958e50a Mon Sep 17 00:00:00 2001 From: Hanyuan Zhang Date: Tue, 28 Feb 2023 20:30:49 -0800 Subject: [PATCH 09/11] commentted out the 2nd /idp/idx/introspect endpoint --- lib/idx/run.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/idx/run.ts b/lib/idx/run.ts index 1052de993..ed3694477 100644 --- a/lib/idx/run.ts +++ b/lib/idx/run.ts @@ -158,9 +158,9 @@ async function getDataFromIntrospect(authClient, data: RunData): Promise { const { options } = data; - let idxResponse; + // let idxResponse; const { - stateHandle, + // stateHandle, withCredentials, version } = options; @@ -171,8 +171,8 @@ async function collectChromeDeviceSignals(authClient, data: RunData): Promise Date: Tue, 7 Mar 2023 18:03:11 -0800 Subject: [PATCH 10/11] clean up --- lib/idx/getDeviceChallenge.ts | 8 +++--- lib/idx/run.ts | 48 +++++++++++++++++++++++++++++------ lib/oidc/util/browser.ts | 4 --- 3 files changed, 43 insertions(+), 17 deletions(-) diff --git a/lib/idx/getDeviceChallenge.ts b/lib/idx/getDeviceChallenge.ts index 4a9111351..617d40880 100644 --- a/lib/idx/getDeviceChallenge.ts +++ b/lib/idx/getDeviceChallenge.ts @@ -16,8 +16,7 @@ import { IntrospectOptions, OktaAuthIdxInterface } from './types'; import { IdxRemediation, isRawIdxResponse } from './types/idx-js'; import { IDX_API_VERSION } from '../constants'; import { isAuthApiError } from '../errors'; -// import { loadInvisibleFrame } from '../oidc/util/browser'; -import { redirect } from '../oidc/util/browser'; +import { loadInvisibleFrame } from '../oidc/util/browser'; export async function getDeviceChallenge ( authClient: OktaAuthIdxInterface, @@ -37,9 +36,8 @@ export async function getDeviceChallenge ( try { validateVersionConfig(version); const url = remediation.href; - redirect(url); - // const iFrameId = 'deviceChallengeIFrameId'; - // response = loadInvisibleFrame(url, iFrameId); + const iFrameId = 'deviceChallengeIFrameId'; + response = loadInvisibleFrame(url, iFrameId); } catch (err) { if (isAuthApiError(err) && err.xhr && isRawIdxResponse(err.xhr.responseJSON)) { console.log(err.xhr.responseJSON); diff --git a/lib/idx/run.ts b/lib/idx/run.ts index ed3694477..f6b203374 100644 --- a/lib/idx/run.ts +++ b/lib/idx/run.ts @@ -34,6 +34,7 @@ import { getAvailableSteps, getEnabledFeatures, getMessagesFromResponse, isTermi import { Tokens } from '../oidc/types'; import { APIError } from '../errors/types'; import { DeviceIdentificationChallenge } from './remediators'; +import { makeIdxState } from './idxState'; declare interface RunData { options: RunOptions; values: remediators.RemediationValues; @@ -158,25 +159,56 @@ async function getDataFromIntrospect(authClient, data: RunData): Promise { const { options } = data; - // let idxResponse; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + let idxResponse; const { - // stateHandle, withCredentials, version } = options; const remediations = data.idxResponse?.rawIdxState.remediation?.value; - remediations?.forEach(async remediation => { + remediations?.forEach(async remediation => { // TODO: only if 1st remediation is DeviceIdentificationChallenge if (remediation['name'] == DeviceIdentificationChallenge.remediationName) { await getDeviceChallenge(authClient, remediation, { withCredentials, version }); - // TODO: the following calls /idp/idx/introspect again right after the 1st GET returns, before hitting the 2nd GET - // TODO: Is it ok to reuse the remediation from the previous introspect? If so there is no need to call it again - // idxResponse = await introspect(authClient, { withCredentials, version, stateHandle }); - // return { ...data, idxResponse }; + idxResponse = await new Promise((resolve) => { + window.addEventListener('message', async function (e) { + // 3 resolve promise with idx object + // TODO: security enhancement on origin and source (e.source must be the one from iFrame) + // This is no longer needed as we will go with idx polling + if (e && !e.data.messageType) { // filters out the iframe load event + const parsed = JSON.parse(e.data); + try { + idxResponse = makeIdxState(authClient, parsed, { withCredentials }, true); + removeIframe(); + resolve(parsed); + } catch(e) { + console.log('invalid json in event ' + JSON.stringify(e)); + removeIframe(); + return data; + } + } + }); + // optionally reject with a `setTimeout` to set a time-limit + }); } }); + + // remove the 1st remediations, regardless if we collect device signals successfully or not. + // backend will have log, syslog and Splunk monitor set up. + // This is not needed when backend has correct logic to add DeviceIdentificationChallenge + data.idxResponse?.rawIdxState.remediation?.value?.shift(); + data.idxResponse?.neededToProceed.shift(); + return data; } +function removeIframe() { + const iFrame = document.getElementById('deviceChallengeIFrameId'); + if (iFrame) { + iFrame.parentElement?.removeChild(iFrame); + } +} + + async function getDataFromRemediate(authClient, data: RunData): Promise { let { idxResponse, @@ -333,7 +365,7 @@ export async function run( data = await getDataFromIntrospect(authClient, data); // collect device signals if elibigle - await collectChromeDeviceSignals(authClient, data); + data = await collectChromeDeviceSignals(authClient, data); data = await getDataFromRemediate(authClient, data); data = await finalizeData(authClient, data); diff --git a/lib/oidc/util/browser.ts b/lib/oidc/util/browser.ts index 80fb1ad66..e5f9b406e 100644 --- a/lib/oidc/util/browser.ts +++ b/lib/oidc/util/browser.ts @@ -52,10 +52,6 @@ export function loadInvisibleFrame(src, id) { return document.body.appendChild(iframe); } -export function redirect(url) { - window.location.href = url; -} - export function loadPopup(src, options) { var title = options.popupTitle || 'External Identity Provider User Authentication'; var appearance = 'toolbar=no, scrollbars=yes, resizable=yes, ' + From 6b1fc3800099baa1beb246756b7dd324e35e40c6 Mon Sep 17 00:00:00 2001 From: Hanyuan Zhang Date: Fri, 17 Mar 2023 15:37:57 -0700 Subject: [PATCH 11/11] Updated logic to use polling instead of backend send data back to frontend --- lib/idx/getDeviceChallenge.ts | 22 +++++++-------- lib/idx/run.ts | 50 +++++++++++------------------------ 2 files changed, 25 insertions(+), 47 deletions(-) diff --git a/lib/idx/getDeviceChallenge.ts b/lib/idx/getDeviceChallenge.ts index 617d40880..87f8ff915 100644 --- a/lib/idx/getDeviceChallenge.ts +++ b/lib/idx/getDeviceChallenge.ts @@ -13,31 +13,29 @@ import { validateVersionConfig } from './idxState'; import { IntrospectOptions, OktaAuthIdxInterface } from './types'; -import { IdxRemediation, isRawIdxResponse } from './types/idx-js'; +import { isRawIdxResponse } from './types/idx-js'; import { IDX_API_VERSION } from '../constants'; import { isAuthApiError } from '../errors'; import { loadInvisibleFrame } from '../oidc/util/browser'; export async function getDeviceChallenge ( authClient: OktaAuthIdxInterface, - remediation: IdxRemediation, + href: string, options: IntrospectOptions = {} ): Promise { - let response; - // try load from storage first - const savedIdxResponse = authClient.transactionManager.loadIdxResponse(options); - if (savedIdxResponse) { - response = savedIdxResponse.rawIdxResponse; - } + // try load from storage first, TODO: delete this + // const savedIdxResponse = authClient.transactionManager.loadIdxResponse(options); + // if (savedIdxResponse) { + // response = savedIdxResponse.rawIdxResponse; + // } - if (!response) { + // if (!response) { const version = options.version || IDX_API_VERSION; try { validateVersionConfig(version); - const url = remediation.href; const iFrameId = 'deviceChallengeIFrameId'; - response = loadInvisibleFrame(url, iFrameId); + loadInvisibleFrame(href, iFrameId); // SIW will init polling, no reponse needed } catch (err) { if (isAuthApiError(err) && err.xhr && isRawIdxResponse(err.xhr.responseJSON)) { console.log(err.xhr.responseJSON); @@ -46,5 +44,5 @@ export async function getDeviceChallenge ( throw err; } } - } + // } } diff --git a/lib/idx/run.ts b/lib/idx/run.ts index f6b203374..4afe0d0fa 100644 --- a/lib/idx/run.ts +++ b/lib/idx/run.ts @@ -157,6 +157,8 @@ async function getDataFromIntrospect(authClient, data: RunData): Promise new Promise(res => setTimeout(res, ms)); + async function collectChromeDeviceSignals(authClient, data: RunData): Promise { const { options } = data; // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -167,48 +169,26 @@ async function collectChromeDeviceSignals(authClient, data: RunData): Promise { // TODO: only if 1st remediation is DeviceIdentificationChallenge - if (remediation['name'] == DeviceIdentificationChallenge.remediationName) { - await getDeviceChallenge(authClient, remediation, { withCredentials, version }); - idxResponse = await new Promise((resolve) => { - window.addEventListener('message', async function (e) { - // 3 resolve promise with idx object - // TODO: security enhancement on origin and source (e.source must be the one from iFrame) - // This is no longer needed as we will go with idx polling - if (e && !e.data.messageType) { // filters out the iframe load event - const parsed = JSON.parse(e.data); - try { - idxResponse = makeIdxState(authClient, parsed, { withCredentials }, true); - removeIframe(); - resolve(parsed); - } catch(e) { - console.log('invalid json in event ' + JSON.stringify(e)); - removeIframe(); - return data; - } - } - }); - // optionally reject with a `setTimeout` to set a time-limit - }); + remediations?.forEach(async remediation => { // TODO: only if 1st remediation is device-challenge-poll + if (remediation['name'] == 'device-challenge-poll') { + const challengeMethod = data.idxResponse?.rawIdxState['authenticatorChallenge']?.value.challengeMethod; + if (challengeMethod == 'CHROME_DTC') { + const href = data.idxResponse?.rawIdxState['authenticatorChallenge']?.value.href; + await getDeviceChallenge(authClient, href, { withCredentials, version }); + } } }); + // TODO: add a wait here for 2 seconds so that signals are collected, check if view is gone and continue to next + // remove the 1st remediations, regardless if we collect device signals successfully or not. // backend will have log, syslog and Splunk monitor set up. // This is not needed when backend has correct logic to add DeviceIdentificationChallenge - data.idxResponse?.rawIdxState.remediation?.value?.shift(); - data.idxResponse?.neededToProceed.shift(); - return data; + // data.idxResponse?.rawIdxState.remediation?.value?.shift(); + // data.idxResponse?.neededToProceed.shift(); + // return data; } -function removeIframe() { - const iFrame = document.getElementById('deviceChallengeIFrameId'); - if (iFrame) { - iFrame.parentElement?.removeChild(iFrame); - } -} - - async function getDataFromRemediate(authClient, data: RunData): Promise { let { idxResponse, @@ -365,7 +345,7 @@ export async function run( data = await getDataFromIntrospect(authClient, data); // collect device signals if elibigle - data = await collectChromeDeviceSignals(authClient, data); + await collectChromeDeviceSignals(authClient, data); data = await getDataFromRemediate(authClient, data); data = await finalizeData(authClient, data);