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..87f8ff915 --- /dev/null +++ b/lib/idx/getDeviceChallenge.ts @@ -0,0 +1,48 @@ +/* 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 getDeviceChallenge ( + authClient: OktaAuthIdxInterface, + href: string, + options: IntrospectOptions = {} +): Promise { + + // try load from storage first, TODO: delete this + // const savedIdxResponse = authClient.transactionManager.loadIdxResponse(options); + // if (savedIdxResponse) { + // response = savedIdxResponse.rawIdxResponse; + // } + + // if (!response) { + const version = options.version || IDX_API_VERSION; + try { + validateVersionConfig(version); + const iFrameId = 'deviceChallengeIFrameId'; + 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); + throw new Error('Auth Api Error'); + } else { + throw err; + } + } + // } +} 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..417633c2f --- /dev/null +++ b/lib/idx/remediators/DeviceIdentificationChallenge.ts @@ -0,0 +1,21 @@ +/*! + * 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 { 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..4afe0d0fa 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,8 @@ 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'; +import { makeIdxState } from './idxState'; declare interface RunData { options: RunOptions; values: remediators.RemediationValues; @@ -154,6 +157,38 @@ 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 + let idxResponse; + const { + withCredentials, + version + } = options; + + const remediations = data.idxResponse?.rawIdxState.remediation?.value; + 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; +} + async function getDataFromRemediate(authClient, data: RunData): Promise { let { idxResponse, @@ -308,6 +343,10 @@ 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); 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, ' +