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

Chrome Device Signals project POC #1379

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions lib/idx/flow/AuthenticationFlow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import { RemediationFlow } from './RemediationFlow';
import {
Identify,
DeviceIdentificationChallenge,
SelectAuthenticatorAuthenticate,
ChallengeAuthenticator,
ReEnrollAuthenticator,
Expand All @@ -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,
Expand Down
48 changes: 48 additions & 0 deletions lib/idx/getDeviceChallenge.ts
Original file line number Diff line number Diff line change
@@ -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<void> {

// 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;
}
}
// }
}
1 change: 1 addition & 0 deletions lib/idx/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
21 changes: 21 additions & 0 deletions lib/idx/remediators/DeviceIdentificationChallenge.ts
Original file line number Diff line number Diff line change
@@ -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<RemediationValues> {
static remediationName = 'device-identification-challenge';

canRemediate(): boolean {
return false;
}
}
1 change: 1 addition & 0 deletions lib/idx/remediators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
39 changes: 39 additions & 0 deletions lib/idx/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand Down Expand Up @@ -154,6 +157,38 @@ async function getDataFromIntrospect(authClient, data: RunData): Promise<RunData
return { ...data, idxResponse, meta };
}

// const delay = ms => new Promise(res => setTimeout(res, ms));

async function collectChromeDeviceSignals(authClient, data: RunData): Promise<any> {
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<RunData> {
let {
idxResponse,
Expand Down Expand Up @@ -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);

Expand Down
13 changes: 13 additions & 0 deletions lib/oidc/util/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, ' +
Expand Down