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

Add Fingerprint API to IDX bundle #1530

Merged
merged 11 commits into from
Aug 27, 2024
3 changes: 1 addition & 2 deletions lib/authn/mixin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {
} from '../util';
import fingerprint from '../browser/fingerprint';
import {
FingerprintAPI,
SigninWithCredentialsOptions,
ForgotPasswordOptions,
VerifyRecoveryTokenOptions,
Expand All @@ -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
<
Expand Down
11 changes: 2 additions & 9 deletions lib/authn/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@

import { FingerprintAPI } from '../base/types';
import { StorageManagerInterface } from '../storage/types';
import { RequestData, RequestOptions, OktaAuthHttpInterface, OktaAuthHttpOptions } from '../http/types';

Expand Down Expand Up @@ -120,14 +122,6 @@ export interface AuthnAPI extends SigninAPI {
verifyRecoveryToken(opts: VerifyRecoveryTokenOptions): Promise<AuthnTransaction>;
}

// Fingerprint
export interface FingerprintOptions {
timeout?: number;
}

export type FingerprintAPI = (options?: FingerprintOptions) => Promise<string>;


export interface OktaAuthTxInterface
<
S extends StorageManagerInterface = StorageManagerInterface,
Expand All @@ -138,5 +132,4 @@ export interface OktaAuthTxInterface
tx: AuthnTransactionAPI; // legacy name
authn: AuthnTransactionAPI; // new name
fingerprint: FingerprintAPI;

}
6 changes: 6 additions & 0 deletions lib/base/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ export interface FeaturesAPI {
}


export interface FingerprintOptions {
timeout?: number;
container?: Element | null;
}
export type FingerprintAPI = (options?: FingerprintOptions) => Promise<string>;

// options that can be passed to AuthJS
export interface OktaAuthBaseOptions {
devMode?: boolean;
Expand Down
38 changes: 23 additions & 15 deletions lib/browser/fingerprint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> {
options = options || {};
const isMessageFromCorrectSource = (iframe: HTMLIFrameElement, event: MessageEvent)
: boolean => event.source === iframe.contentWindow;

export default function fingerprint(sdk: OktaAuthHttpInterface, options?: FingerprintOptions): Promise<string> {
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;
let msg;
denysoblohin-okta marked this conversation as resolved.
Show resolved Hide resolved
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;
}

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)
Expand All @@ -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 Error('No data'));
denysoblohin-okta marked this conversation as resolved.
Show resolved Hide resolved
}
};
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'));
Expand All @@ -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);
jaredperreault-okta marked this conversation as resolved.
Show resolved Hide resolved
}
}) as Promise<string>;
}
5 changes: 4 additions & 1 deletion lib/idx/mixin.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { OktaAuthConstructor } from '../base/types';
import { FingerprintAPI, OktaAuthConstructor } from '../base/types';
import { OktaAuthOAuthInterface } from '../oidc/types';
import {
IdxAPI,
Expand All @@ -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
Expand All @@ -27,11 +28,13 @@ export function mixinIdx
return class OktaAuthIdx extends Base implements OktaAuthIdxInterface<M, S, O, TM>
{
idx: IdxAPI;
fingerprint: FingerprintAPI;
static webauthn: WebauthnAPI = webauthn;

constructor(...args: any[]) {
super(...args);
this.idx = createIdxAPI(this);
this.fingerprint = fingerprint.bind(null, this);
}
};
}
5 changes: 4 additions & 1 deletion lib/idx/mixinMinimal.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { OktaAuthConstructor } from '../base/types';
import { FingerprintAPI, OktaAuthConstructor } from '../base/types';
import { MinimalOktaOAuthInterface } from '../oidc/types';
import {
IdxTransactionManagerInterface,
Expand All @@ -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
Expand All @@ -29,11 +30,13 @@ export function mixinMinimalIdx
return class OktaAuthIdx extends Base implements MinimalOktaAuthIdxInterface<M, S, O, TM>
{
idx: MinimalIdxAPI;
fingerprint: FingerprintAPI;
static webauthn: WebauthnAPI = webauthn;

constructor(...args: any[]) {
super(...args);
this.idx = createMinimalIdxAPI(this);
this.fingerprint = fingerprint.bind(null, this);
}
};
}
5 changes: 4 additions & 1 deletion lib/idx/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -258,6 +258,7 @@ export interface WebauthnAPI {
): CredentialCreationOptions;
}


export interface OktaAuthIdxInterface
<
M extends IdxTransactionMeta = IdxTransactionMeta,
Expand All @@ -268,6 +269,7 @@ export interface OktaAuthIdxInterface
extends OktaAuthOAuthInterface<M, S, O, TM>
{
idx: IdxAPI;
fingerprint: FingerprintAPI;
}

export interface MinimalOktaAuthIdxInterface
Expand All @@ -280,6 +282,7 @@ export interface MinimalOktaAuthIdxInterface
extends MinimalOktaOAuthInterface<M, S, O, TM>
{
idx: MinimalIdxAPI;
fingerprint: FingerprintAPI;
}

export interface OktaAuthIdxConstructor
Expand Down
37 changes: 30 additions & 7 deletions test/spec/fingerprint.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,18 @@ describe('fingerprint', function() {
type: 'FingerprintAvailable',
fingerprint: 'ABCD'
}),
origin: 'http://example.okta.com'
origin: 'http://example.okta.com',
source: test.iframe.contentWindow
});
});

test.iframe = {
style: {},
parentElement: {
removeChild: jest.fn()
},
contentWindow: {
postMessage: postMessageSpy
}
};

Expand All @@ -61,23 +65,32 @@ 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"}',
origin: 'http://not.okta.com'
});
});
}
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
});
});
});
Expand Down Expand Up @@ -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();
})
Expand Down