@okta/okta-signin-widget 7.15.0 → 7.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "private": false,
3
3
  "name": "@okta/okta-signin-widget",
4
4
  "description": "The Okta Sign-In Widget",
5
- "version": "7.15.0",
5
+ "version": "7.15.1",
6
6
  "homepage": "https://github.com/okta/okta-signin-widget",
7
7
  "license": "Apache-2.0",
8
8
  "repository": {
@@ -291,7 +291,7 @@
291
291
  "workerDirectory": "playground"
292
292
  },
293
293
  "okta": {
294
- "commitSha": "6c0484557e0b82ee512ee37a926902f3d7bd63d1",
295
- "fullVersion": "7.15.0-g6c04845"
294
+ "commitSha": "48be3a5bce89767aa556742410c72e54b646d122",
295
+ "fullVersion": "7.15.1-g48be3a5"
296
296
  }
297
297
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "defaultLanguage": "en",
3
- "version": "7.15.0",
3
+ "version": "7.15.1",
4
4
  "supportedLanguages": [
5
5
  "en",
6
6
  "cs",
@@ -20,6 +20,7 @@ import {
20
20
  } from '@okta/okta-auth-js';
21
21
  import { cloneDeep, merge, omit } from 'lodash';
22
22
  import { useCallback } from 'preact/hooks';
23
+ import { generateDeviceFingerprint } from 'src/util/deviceFingerprintingUtils';
23
24
 
24
25
  import { IDX_STEP, ON_PREM_TOKEN_CHANGE_ERROR_KEY } from '../constants';
25
26
  import { useWidgetContext } from '../contexts';
@@ -27,6 +28,7 @@ import { MessageType } from '../types';
27
28
  import {
28
29
  areTransactionsEqual,
29
30
  containsMessageKey,
31
+ getBaseUrl,
30
32
  getErrorEventContext,
31
33
  getImmutableData,
32
34
  isConfigRecoverFlow,
@@ -209,6 +211,16 @@ export const useOnSubmit = (): (options: OnSubmitHandlerOptions) => Promise<void
209
211
  if (step === IDX_STEP.CANCEL_TRANSACTION) {
210
212
  SessionStorage.removeStateHandle();
211
213
  }
214
+ if (step === IDX_STEP.IDENTIFY && features?.deviceFingerprinting) {
215
+ const baseUrl = getBaseUrl(widgetProps);
216
+ if (baseUrl) {
217
+ // Proceeds with form submission even if device fingerprinting fails
218
+ const fingerprint = await generateDeviceFingerprint(baseUrl).catch(() => undefined);
219
+ if (fingerprint) {
220
+ authClient.http.setRequestHeader('X-Device-Fingerprint', fingerprint);
221
+ }
222
+ }
223
+ }
212
224
  setMessage(undefined);
213
225
  try {
214
226
  let newTransaction = await fn(payload);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "v3",
3
- "version": "7.15.0",
3
+ "version": "7.15.1",
4
4
  "private": true,
5
5
  "description": "Okta Sign In Widget Next",
6
6
  "homepage": "https://github.com/okta/okta-signin-widget",
@@ -20,6 +20,7 @@ import {
20
20
  } from '@okta/okta-auth-js';
21
21
  import { cloneDeep, merge, omit } from 'lodash';
22
22
  import { useCallback } from 'preact/hooks';
23
+ import { generateDeviceFingerprint } from 'src/util/deviceFingerprintingUtils';
23
24
 
24
25
  import { IDX_STEP, ON_PREM_TOKEN_CHANGE_ERROR_KEY } from '../constants';
25
26
  import { useWidgetContext } from '../contexts';
@@ -27,6 +28,7 @@ import { MessageType } from '../types';
27
28
  import {
28
29
  areTransactionsEqual,
29
30
  containsMessageKey,
31
+ getBaseUrl,
30
32
  getErrorEventContext,
31
33
  getImmutableData,
32
34
  isConfigRecoverFlow,
@@ -209,6 +211,16 @@ export const useOnSubmit = (): (options: OnSubmitHandlerOptions) => Promise<void
209
211
  if (step === IDX_STEP.CANCEL_TRANSACTION) {
210
212
  SessionStorage.removeStateHandle();
211
213
  }
214
+ if (step === IDX_STEP.IDENTIFY && features?.deviceFingerprinting) {
215
+ const baseUrl = getBaseUrl(widgetProps);
216
+ if (baseUrl) {
217
+ // Proceeds with form submission even if device fingerprinting fails
218
+ const fingerprint = await generateDeviceFingerprint(baseUrl).catch(() => undefined);
219
+ if (fingerprint) {
220
+ authClient.http.setRequestHeader('X-Device-Fingerprint', fingerprint);
221
+ }
222
+ }
223
+ }
212
224
  setMessage(undefined);
213
225
  try {
214
226
  let newTransaction = await fn(payload);
@@ -24,3 +24,7 @@ export const isIOS = (): boolean => (
24
24
  );
25
25
 
26
26
  export const isAndroidOrIOS = (): boolean => isAndroid() || isIOS();
27
+
28
+ export const getUserAgent = (): string => navigator.userAgent;
29
+
30
+ export const isWindowsPhone = (userAgent: string): boolean => /windows phone|iemobile|wpdesktop/i.test(userAgent);
@@ -0,0 +1,130 @@
1
+ /*
2
+ * Copyright (c) 2023-present, Okta, Inc. and/or its affiliates. All rights reserved.
3
+ * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
4
+ *
5
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
6
+ * Unless required by applicable law or agreed to in writing, software
7
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
8
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9
+ *
10
+ * See the License for the specific language governing permissions and limitations under the License.
11
+ */
12
+
13
+ import * as DeviceFingerprintingUtils from './deviceFingerprintingUtils';
14
+
15
+ describe('DeviceFingerprintingUtils', () => {
16
+ const oktaDomainUrl = '';
17
+
18
+ beforeAll(() => {
19
+ const mockForm = document.createElement('form');
20
+ mockForm.setAttribute('data-se', 'o-form');
21
+ document.body.append(mockForm);
22
+ });
23
+
24
+ afterEach(() => {
25
+ jest.clearAllMocks();
26
+ });
27
+
28
+ const mockIFrameMessages = (success: boolean, errorMessage?: { type: string }) => {
29
+ const message = success
30
+ ? { type: 'FingerprintAvailable', fingerprint: 'thisIsTheFingerprint' }
31
+ : errorMessage;
32
+
33
+ // TODO (jest): event is missing `origin` property
34
+ window.postMessage(JSON.stringify(message), '*');
35
+ };
36
+
37
+ const mockUserAgent = (userAgent: string) => {
38
+ jest.spyOn(navigator, 'userAgent', 'get').mockReturnValue(userAgent);
39
+ };
40
+
41
+ const bypassMessageSourceCheck = () => {
42
+ jest.spyOn(DeviceFingerprintingUtils, 'isMessageFromCorrectSource').mockReturnValue(true);
43
+ };
44
+
45
+ it('creates hidden iframe during fingerprint generation', async () => {
46
+ mockIFrameMessages(true);
47
+ bypassMessageSourceCheck();
48
+ const fingerprintPromise = DeviceFingerprintingUtils.generateDeviceFingerprint(oktaDomainUrl);
49
+ let iframe = document.getElementById('device-fingerprint-container');
50
+ expect(iframe).not.toBeNull();
51
+ expect(iframe).not.toBeVisible();
52
+ expect(iframe?.getAttribute('src')).toBe('/auth/services/devicefingerprint');
53
+ await fingerprintPromise;
54
+ iframe = document.getElementById('device-fingerprint-container');
55
+ expect(iframe).toBeNull();
56
+ });
57
+
58
+ it('returns a fingerprint if the communication with the iframe is successful', async () => {
59
+ mockIFrameMessages(true);
60
+ bypassMessageSourceCheck();
61
+ const fingerprint = await DeviceFingerprintingUtils.generateDeviceFingerprint(oktaDomainUrl);
62
+ expect(fingerprint).toBe('thisIsTheFingerprint');
63
+ });
64
+
65
+ it('fails if there is a problem with communicating with the iframe', async () => {
66
+ mockIFrameMessages(false);
67
+ bypassMessageSourceCheck();
68
+
69
+ await expect(DeviceFingerprintingUtils.generateDeviceFingerprint(oktaDomainUrl))
70
+ .rejects
71
+ .toThrow('No data');
72
+ const iframe = document.getElementById('device-fingerprint-container');
73
+ expect(iframe).toBeNull();
74
+ });
75
+
76
+ it('fails if the iframe sends invalid message content', async () => {
77
+ mockIFrameMessages(false, { type: 'InvalidMessageType' });
78
+ bypassMessageSourceCheck();
79
+
80
+ await expect(DeviceFingerprintingUtils.generateDeviceFingerprint(oktaDomainUrl))
81
+ .rejects
82
+ .toThrow('No data');
83
+ const iframe = document.getElementById('device-fingerprint-container');
84
+ expect(iframe).toBeNull();
85
+ });
86
+
87
+ it('fails if user agent is not defined', async () => {
88
+ mockUserAgent('');
89
+ mockIFrameMessages(true);
90
+ bypassMessageSourceCheck();
91
+
92
+ await expect(DeviceFingerprintingUtils.generateDeviceFingerprint(oktaDomainUrl))
93
+ .rejects
94
+ .toThrow('User agent is not defined');
95
+ const iframe = document.getElementById('device-fingerprint-container');
96
+ expect(iframe).toBeNull();
97
+ });
98
+
99
+ it('fails if it is called from a Windows phone', async () => {
100
+ mockUserAgent('Windows Phone');
101
+ mockIFrameMessages(true);
102
+ bypassMessageSourceCheck();
103
+
104
+ await expect(DeviceFingerprintingUtils.generateDeviceFingerprint(oktaDomainUrl))
105
+ .rejects
106
+ .toThrow('Device fingerprint is not supported on Windows phones');
107
+ const iframe = document.getElementById('device-fingerprint-container');
108
+ expect(iframe).toBeNull();
109
+ });
110
+
111
+ it('fails if the iframe does not receive any messages', async () => {
112
+ // Not sending any mock messages should trigger a timeout
113
+ await expect(DeviceFingerprintingUtils.generateDeviceFingerprint(oktaDomainUrl, 1000))
114
+ .rejects
115
+ .toThrow('Device fingerprinting timed out');
116
+ const iframe = document.getElementById('device-fingerprint-container');
117
+ expect(iframe).toBeNull();
118
+ });
119
+
120
+ it('fails if there is no form to attach the iframe to', async () => {
121
+ const form = document.querySelector('form[data-se="o-form"]');
122
+ expect(form).not.toBeNull();
123
+ document.body.removeChild(form!);
124
+ await expect(DeviceFingerprintingUtils.generateDeviceFingerprint(oktaDomainUrl))
125
+ .rejects
126
+ .toThrow('Form does not exist');
127
+ const iframe = document.getElementById('device-fingerprint-container');
128
+ expect(iframe).toBeNull();
129
+ });
130
+ });
@@ -0,0 +1,90 @@
1
+ /*
2
+ * Copyright (c) 2023-present, Okta, Inc. and/or its affiliates. All rights reserved.
3
+ * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
4
+ *
5
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
6
+ * Unless required by applicable law or agreed to in writing, software
7
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
8
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9
+ *
10
+ * See the License for the specific language governing permissions and limitations under the License.
11
+ */
12
+
13
+ import { getUserAgent, isWindowsPhone } from './browserUtils';
14
+
15
+ export const isMessageFromCorrectSource = (iframe: HTMLIFrameElement, event: MessageEvent)
16
+ : boolean => event.source === iframe.contentWindow;
17
+
18
+ // NOTE: This utility is similar to the DeviceFingerprinting.js file used for V2 authentication flows.
19
+ export const generateDeviceFingerprint = (
20
+ oktaDomainUrl: string,
21
+ timeoutDuration?: number,
22
+ ): Promise<string> => {
23
+ const userAgent = getUserAgent();
24
+ if (!userAgent) {
25
+ return Promise.reject(new Error('User agent is not defined'));
26
+ }
27
+ if (isWindowsPhone(userAgent)) {
28
+ return Promise.reject(new Error('Device fingerprint is not supported on Windows phones'));
29
+ }
30
+
31
+ let timeout: NodeJS.Timeout;
32
+ let iframe: HTMLIFrameElement;
33
+ let listener: (this: Window, ev: MessageEvent) => void;
34
+ let msg;
35
+ const formElement = document.querySelector('form[data-se="o-form"]');
36
+ return new Promise<string>((resolve, reject) => {
37
+ iframe = document.createElement('iframe');
38
+ iframe.style.display = 'none';
39
+ iframe.id = 'device-fingerprint-container';
40
+
41
+ listener = (event: MessageEvent) => {
42
+ if (!isMessageFromCorrectSource(iframe, event)) {
43
+ return undefined;
44
+ }
45
+
46
+ if (!event || !event.data || event.origin !== oktaDomainUrl) {
47
+ return reject(new Error('No data'));
48
+ }
49
+
50
+ try {
51
+ msg = JSON.parse(event.data);
52
+ } catch (err) {
53
+ // iframe messages should all be parsable, skip not parsable messages that come from other
54
+ // sources in the same origin (browser extensions)
55
+ return undefined;
56
+ }
57
+
58
+ if (!msg) { return undefined; }
59
+ if (msg.type === 'FingerprintAvailable') {
60
+ return resolve(msg.fingerprint as string);
61
+ } if (msg.type === 'FingerprintServiceReady') {
62
+ const win = iframe.contentWindow;
63
+ win?.postMessage(JSON.stringify({
64
+ type: 'GetFingerprint',
65
+ }), event.origin );
66
+ } else {
67
+ return reject(new Error('No data'));
68
+ }
69
+ return undefined;
70
+ };
71
+ window.addEventListener('message', listener, false);
72
+
73
+ iframe.src = `${oktaDomainUrl}/auth/services/devicefingerprint`;
74
+ if (formElement === null) {
75
+ reject(new Error('Form does not exist'));
76
+ }
77
+ formElement!.appendChild(iframe);
78
+
79
+ timeout = setTimeout(() => {
80
+ // If the iframe does not load, receive the right message type, or there is a slow connection, throw an error
81
+ reject(new Error('Device fingerprinting timed out'));
82
+ }, timeoutDuration || 2000);
83
+ }).finally(() => {
84
+ clearTimeout(timeout);
85
+ window.removeEventListener('message', listener);
86
+ if (formElement?.contains(iframe)) {
87
+ iframe.parentElement?.removeChild(iframe);
88
+ }
89
+ });
90
+ };
@@ -24,3 +24,7 @@ export const isIOS = (): boolean => (
24
24
  );
25
25
 
26
26
  export const isAndroidOrIOS = (): boolean => isAndroid() || isIOS();
27
+
28
+ export const getUserAgent = (): string => navigator.userAgent;
29
+
30
+ export const isWindowsPhone = (userAgent: string): boolean => /windows phone|iemobile|wpdesktop/i.test(userAgent);
@@ -0,0 +1,90 @@
1
+ /*
2
+ * Copyright (c) 2023-present, Okta, Inc. and/or its affiliates. All rights reserved.
3
+ * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
4
+ *
5
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
6
+ * Unless required by applicable law or agreed to in writing, software
7
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
8
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9
+ *
10
+ * See the License for the specific language governing permissions and limitations under the License.
11
+ */
12
+
13
+ import { getUserAgent, isWindowsPhone } from './browserUtils';
14
+
15
+ export const isMessageFromCorrectSource = (iframe: HTMLIFrameElement, event: MessageEvent)
16
+ : boolean => event.source === iframe.contentWindow;
17
+
18
+ // NOTE: This utility is similar to the DeviceFingerprinting.js file used for V2 authentication flows.
19
+ export const generateDeviceFingerprint = (
20
+ oktaDomainUrl: string,
21
+ timeoutDuration?: number,
22
+ ): Promise<string> => {
23
+ const userAgent = getUserAgent();
24
+ if (!userAgent) {
25
+ return Promise.reject(new Error('User agent is not defined'));
26
+ }
27
+ if (isWindowsPhone(userAgent)) {
28
+ return Promise.reject(new Error('Device fingerprint is not supported on Windows phones'));
29
+ }
30
+
31
+ let timeout: NodeJS.Timeout;
32
+ let iframe: HTMLIFrameElement;
33
+ let listener: (this: Window, ev: MessageEvent) => void;
34
+ let msg;
35
+ const formElement = document.querySelector('form[data-se="o-form"]');
36
+ return new Promise<string>((resolve, reject) => {
37
+ iframe = document.createElement('iframe');
38
+ iframe.style.display = 'none';
39
+ iframe.id = 'device-fingerprint-container';
40
+
41
+ listener = (event: MessageEvent) => {
42
+ if (!isMessageFromCorrectSource(iframe, event)) {
43
+ return undefined;
44
+ }
45
+
46
+ if (!event || !event.data || event.origin !== oktaDomainUrl) {
47
+ return reject(new Error('No data'));
48
+ }
49
+
50
+ try {
51
+ msg = JSON.parse(event.data);
52
+ } catch (err) {
53
+ // iframe messages should all be parsable, skip not parsable messages that come from other
54
+ // sources in the same origin (browser extensions)
55
+ return undefined;
56
+ }
57
+
58
+ if (!msg) { return undefined; }
59
+ if (msg.type === 'FingerprintAvailable') {
60
+ return resolve(msg.fingerprint as string);
61
+ } if (msg.type === 'FingerprintServiceReady') {
62
+ const win = iframe.contentWindow;
63
+ win?.postMessage(JSON.stringify({
64
+ type: 'GetFingerprint',
65
+ }), event.origin );
66
+ } else {
67
+ return reject(new Error('No data'));
68
+ }
69
+ return undefined;
70
+ };
71
+ window.addEventListener('message', listener, false);
72
+
73
+ iframe.src = `${oktaDomainUrl}/auth/services/devicefingerprint`;
74
+ if (formElement === null) {
75
+ reject(new Error('Form does not exist'));
76
+ }
77
+ formElement!.appendChild(iframe);
78
+
79
+ timeout = setTimeout(() => {
80
+ // If the iframe does not load, receive the right message type, or there is a slow connection, throw an error
81
+ reject(new Error('Device fingerprinting timed out'));
82
+ }, timeoutDuration || 2000);
83
+ }).finally(() => {
84
+ clearTimeout(timeout);
85
+ window.removeEventListener('message', listener);
86
+ if (formElement?.contains(iframe)) {
87
+ iframe.parentElement?.removeChild(iframe);
88
+ }
89
+ });
90
+ };