From a3efc3559bcf8fc66b1a64570d2acade6df3dd0e Mon Sep 17 00:00:00 2001 From: Denys Oblohin <72614880+denysoblohin-okta@users.noreply.github.com> Date: Tue, 27 Aug 2024 20:12:51 +0300 Subject: [PATCH] Add Fingerprint API to IDX bundle (#1530) OKTA-418160 Added fingerprint API to idx --- CHANGELOG.md | 6 ++++++ lib/authn/mixin.ts | 3 +-- lib/authn/types.ts | 11 ++--------- lib/base/types.ts | 6 ++++++ lib/browser/fingerprint.ts | 38 +++++++++++++++++++++++--------------- lib/idx/mixin.ts | 5 ++++- lib/idx/mixinMinimal.ts | 5 ++++- lib/idx/types/api.ts | 5 ++++- test/spec/fingerprint.js | 37 ++++++++++++++++++++++++++++++------- 9 files changed, 80 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b973cf32..58fd3f954 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +# 7.8.0 + +### Features + +- [#1530](https://github.com/okta/okta-auth-js/pull/1530) add: fingerprint API to IDX bundle + # 7.7.1 - [#1529](https://github.com/okta/okta-auth-js/pull/1529) fix: persist `extraParams` passed to `/authorize` and include them during token refresh diff --git a/lib/authn/mixin.ts b/lib/authn/mixin.ts index b109c6bf4..ffa85213d 100644 --- a/lib/authn/mixin.ts +++ b/lib/authn/mixin.ts @@ -17,7 +17,6 @@ import { } from '../util'; import fingerprint from '../browser/fingerprint'; import { - FingerprintAPI, SigninWithCredentialsOptions, ForgotPasswordOptions, VerifyRecoveryTokenOptions, @@ -31,7 +30,7 @@ import { } from './factory'; import { StorageManagerInterface } from '../storage/types'; import { OktaAuthHttpInterface, OktaAuthHttpOptions } from '../http/types'; -import { OktaAuthConstructor } from '../base/types'; +import { FingerprintAPI, OktaAuthConstructor } from '../base/types'; export function mixinAuthn < diff --git a/lib/authn/types.ts b/lib/authn/types.ts index 8a604457a..0f2554ec5 100644 --- a/lib/authn/types.ts +++ b/lib/authn/types.ts @@ -1,3 +1,5 @@ + +import { FingerprintAPI } from '../base/types'; import { StorageManagerInterface } from '../storage/types'; import { RequestData, RequestOptions, OktaAuthHttpInterface, OktaAuthHttpOptions } from '../http/types'; @@ -120,14 +122,6 @@ export interface AuthnAPI extends SigninAPI { verifyRecoveryToken(opts: VerifyRecoveryTokenOptions): Promise; } -// Fingerprint -export interface FingerprintOptions { - timeout?: number; -} - -export type FingerprintAPI = (options?: FingerprintOptions) => Promise; - - export interface OktaAuthTxInterface < S extends StorageManagerInterface = StorageManagerInterface, @@ -138,5 +132,4 @@ export interface OktaAuthTxInterface tx: AuthnTransactionAPI; // legacy name authn: AuthnTransactionAPI; // new name fingerprint: FingerprintAPI; - } diff --git a/lib/base/types.ts b/lib/base/types.ts index 791870777..e4db88c0b 100644 --- a/lib/base/types.ts +++ b/lib/base/types.ts @@ -31,6 +31,12 @@ export interface FeaturesAPI { } +export interface FingerprintOptions { + timeout?: number; + container?: Element | null; +} +export type FingerprintAPI = (options?: FingerprintOptions) => Promise; + // options that can be passed to AuthJS export interface OktaAuthBaseOptions { devMode?: boolean; diff --git a/lib/browser/fingerprint.ts b/lib/browser/fingerprint.ts index df47756fd..5f89cd85b 100644 --- a/lib/browser/fingerprint.ts +++ b/lib/browser/fingerprint.ts @@ -17,31 +17,38 @@ import { addListener, removeListener } from '../oidc'; -import { FingerprintOptions } from '../authn/types'; +import { FingerprintOptions } from '../base/types'; import { OktaAuthHttpInterface } from '../http/types'; -export default function fingerprint(sdk: OktaAuthHttpInterface, options?: FingerprintOptions): Promise { - options = options || {}; +const isMessageFromCorrectSource = (iframe: HTMLIFrameElement, event: MessageEvent) +: boolean => event.source === iframe.contentWindow; +export default function fingerprint(sdk: OktaAuthHttpInterface, options?: FingerprintOptions): Promise { if (!isFingerprintSupported()) { return Promise.reject(new AuthSdkError('Fingerprinting is not supported on this device')); } - var timeout; - var iframe; - var listener; - var promise = new Promise(function (resolve, reject) { + const container = options?.container ?? document.body; + let timeout: NodeJS.Timeout; + let iframe: HTMLIFrameElement; + let listener: (this: Window, ev: MessageEvent) => void; + const promise = new Promise(function (resolve, reject) { iframe = document.createElement('iframe'); iframe.style.display = 'none'; // eslint-disable-next-line complexity - listener = function listener(e) { + listener = function listener(e: MessageEvent) { + if (!isMessageFromCorrectSource(iframe, e)) { + return; + } + if (!e || !e.data || e.origin !== sdk.getIssuerOrigin()) { return; } + let msg; try { - var msg = JSON.parse(e.data); + msg = JSON.parse(e.data); } catch (err) { // iframe messages should all be parsable // skip not parsable messages come from other sources in same origin (browser extensions) @@ -52,17 +59,18 @@ export default function fingerprint(sdk: OktaAuthHttpInterface, options?: Finger if (!msg) { return; } if (msg.type === 'FingerprintAvailable') { return resolve(msg.fingerprint as string); - } - if (msg.type === 'FingerprintServiceReady') { - e.source.postMessage(JSON.stringify({ + } else if (msg.type === 'FingerprintServiceReady') { + iframe?.contentWindow?.postMessage(JSON.stringify({ type: 'GetFingerprint' }), e.origin); + } else { + return reject(new AuthSdkError('No data')); } }; addListener(window, 'message', listener); iframe.src = sdk.getIssuerOrigin() + '/auth/services/devicefingerprint'; - document.body.appendChild(iframe); + container.appendChild(iframe); timeout = setTimeout(function() { reject(new AuthSdkError('Fingerprinting timed out')); @@ -72,8 +80,8 @@ export default function fingerprint(sdk: OktaAuthHttpInterface, options?: Finger return promise.finally(function() { clearTimeout(timeout); removeListener(window, 'message', listener); - if (document.body.contains(iframe)) { - iframe.parentElement.removeChild(iframe); + if (container.contains(iframe)) { + iframe.parentElement?.removeChild(iframe); } }) as Promise; } diff --git a/lib/idx/mixin.ts b/lib/idx/mixin.ts index 2939dec6c..49e6a5612 100644 --- a/lib/idx/mixin.ts +++ b/lib/idx/mixin.ts @@ -1,4 +1,4 @@ -import { OktaAuthConstructor } from '../base/types'; +import { FingerprintAPI, OktaAuthConstructor } from '../base/types'; import { OktaAuthOAuthInterface } from '../oidc/types'; import { IdxAPI, @@ -11,6 +11,7 @@ import { import { IdxTransactionMeta } from './types/meta'; import { IdxStorageManagerInterface } from './types/storage'; import { createIdxAPI } from './factory/api'; +import fingerprint from '../browser/fingerprint'; import * as webauthn from './webauthn'; export function mixinIdx @@ -27,11 +28,13 @@ export function mixinIdx return class OktaAuthIdx extends Base implements OktaAuthIdxInterface { idx: IdxAPI; + fingerprint: FingerprintAPI; static webauthn: WebauthnAPI = webauthn; constructor(...args: any[]) { super(...args); this.idx = createIdxAPI(this); + this.fingerprint = fingerprint.bind(null, this); } }; } diff --git a/lib/idx/mixinMinimal.ts b/lib/idx/mixinMinimal.ts index 901d55bac..2e6266da2 100644 --- a/lib/idx/mixinMinimal.ts +++ b/lib/idx/mixinMinimal.ts @@ -1,4 +1,4 @@ -import { OktaAuthConstructor } from '../base/types'; +import { FingerprintAPI, OktaAuthConstructor } from '../base/types'; import { MinimalOktaOAuthInterface } from '../oidc/types'; import { IdxTransactionManagerInterface, @@ -11,6 +11,7 @@ import { import { IdxTransactionMeta } from './types/meta'; import { IdxStorageManagerInterface } from './types/storage'; import { createMinimalIdxAPI } from '../idx/factory/minimalApi'; +import fingerprint from '../browser/fingerprint'; import * as webauthn from './webauthn'; export function mixinMinimalIdx @@ -29,11 +30,13 @@ export function mixinMinimalIdx return class OktaAuthIdx extends Base implements MinimalOktaAuthIdxInterface { idx: MinimalIdxAPI; + fingerprint: FingerprintAPI; static webauthn: WebauthnAPI = webauthn; constructor(...args: any[]) { super(...args); this.idx = createMinimalIdxAPI(this); + this.fingerprint = fingerprint.bind(null, this); } }; } diff --git a/lib/idx/types/api.ts b/lib/idx/types/api.ts index 40f31c3cf..0f535925d 100644 --- a/lib/idx/types/api.ts +++ b/lib/idx/types/api.ts @@ -54,7 +54,7 @@ import type { WebauthnEnrollValues, WebauthnVerificationValues } from '../authenticator'; -import { OktaAuthConstructor } from '../../base/types'; +import { OktaAuthConstructor, FingerprintAPI } from '../../base/types'; export enum IdxStatus { SUCCESS = 'SUCCESS', @@ -258,6 +258,7 @@ export interface WebauthnAPI { ): CredentialCreationOptions; } + export interface OktaAuthIdxInterface < M extends IdxTransactionMeta = IdxTransactionMeta, @@ -268,6 +269,7 @@ export interface OktaAuthIdxInterface extends OktaAuthOAuthInterface { idx: IdxAPI; + fingerprint: FingerprintAPI; } export interface MinimalOktaAuthIdxInterface @@ -280,6 +282,7 @@ export interface MinimalOktaAuthIdxInterface extends MinimalOktaOAuthInterface { idx: MinimalIdxAPI; + fingerprint: FingerprintAPI; } export interface OktaAuthIdxConstructor diff --git a/test/spec/fingerprint.js b/test/spec/fingerprint.js index 31ab7776a..ad91e281f 100644 --- a/test/spec/fingerprint.js +++ b/test/spec/fingerprint.js @@ -40,7 +40,8 @@ describe('fingerprint', function() { type: 'FingerprintAvailable', fingerprint: 'ABCD' }), - origin: 'http://example.okta.com' + origin: 'http://example.okta.com', + source: test.iframe.contentWindow }); }); @@ -48,6 +49,9 @@ describe('fingerprint', function() { style: {}, parentElement: { removeChild: jest.fn() + }, + contentWindow: { + postMessage: postMessageSpy } }; @@ -61,7 +65,7 @@ describe('fingerprint', function() { jest.spyOn(document.body, 'appendChild').mockImplementation(function() { if (options.timeout) { return; } // mimic async page load with setTimeouts - if (options.sendOtherMessage) { + if (options.sendMessageFromAnotherOrigin) { setTimeout(function() { listeners.message({ data: '{"not":"forUs"}', @@ -69,15 +73,24 @@ describe('fingerprint', function() { }); }); } + if (options.sendMessageFromAnotherSource) { + setTimeout(function() { + listeners.message({ + data: '{"not":"forUs"}', + origin: 'http://example.okta.com', + source: { + postMessage: postMessageSpy + } + }); + }); + } setTimeout(function() { listeners.message({ data: options.firstMessage || JSON.stringify({ type: 'FingerprintServiceReady' }), origin: 'http://example.okta.com', - source: { - postMessage: postMessageSpy - } + source: test.iframe.contentWindow }); }); }); @@ -112,8 +125,18 @@ describe('fingerprint', function() { }); }); - it('allows non-Okta postMessages', function () { - return setup({ sendOtherMessage: true }).fingerprint() + it('ignores postMessages from another origin', function () { + return setup({ sendMessageFromAnotherOrigin: true }).fingerprint() + .catch(function(err) { + expect(err).toBeUndefined(); + }) + .then(function(fingerprint) { + expect(fingerprint).toEqual('ABCD'); + }); + }); + + it('ignores postMessages from another source', function () { + return setup({ sendMessageFromAnotherSource: true }).fingerprint() .catch(function(err) { expect(err).toBeUndefined(); })