Skip to content

Commit

Permalink
Add Fingerprint API to IDX bundle (#1530)
Browse files Browse the repository at this point in the history
OKTA-418160 Added fingerprint API to idx
  • Loading branch information
denysoblohin-okta authored Aug 27, 2024
1 parent 0277ce1 commit a3efc35
Show file tree
Hide file tree
Showing 9 changed files with 80 additions and 36 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
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;
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)
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 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'));
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);
}
}) 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

0 comments on commit a3efc35

Please sign in to comment.