diff --git a/CHANGELOG.md b/CHANGELOG.md index 407100cbd..71de06ba4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +# 7.10.1 + +### Other + +- [#1562](https://github.com/okta/okta-auth-js/pull/1562) chore: Remove `ua-parser-js`. Change `isMobileSafari18` to `isSafari18` + # 7.10.0 ### Bug Fix diff --git a/lib/authn/util/poll.ts b/lib/authn/util/poll.ts index 989da99bf..779ebdf7e 100644 --- a/lib/authn/util/poll.ts +++ b/lib/authn/util/poll.ts @@ -18,7 +18,7 @@ import AuthSdkError from '../../errors/AuthSdkError'; import AuthPollStopError from '../../errors/AuthPollStopError'; import { AuthnTransactionState } from '../types'; import { getStateToken } from './stateToken'; -import { isMobileSafari18 } from '../../features'; +import { isSafari18 } from '../../features'; interface PollOptions { delay?: number; @@ -86,7 +86,7 @@ export function getPollFn(sdk, res: AuthnTransactionState, ref) { const delayNextPoll = (ms) => { // no need for extra logic in non-iOS environments, just continue polling - if (!isMobileSafari18()) { + if (!isSafari18()) { return delayFn(ms); } @@ -136,7 +136,7 @@ export function getPollFn(sdk, res: AuthnTransactionState, ref) { } // don't trigger polling request if page is hidden wait until window is visible again - if (isMobileSafari18() && document.hidden) { + if (isSafari18() && document.hidden) { let handler; return new Promise((resolve) => { handler = () => { diff --git a/lib/base/types.ts b/lib/base/types.ts index 3b36b95ce..02b7b476b 100644 --- a/lib/base/types.ts +++ b/lib/base/types.ts @@ -29,7 +29,7 @@ export interface FeaturesAPI { isIE11OrLess(): boolean; isDPoPSupported(): boolean; isIOS(): boolean; - isMobileSafari18(): boolean; + isSafari18(): boolean; } diff --git a/lib/features.ts b/lib/features.ts index b7ba951f7..2883dc154 100644 --- a/lib/features.ts +++ b/lib/features.ts @@ -13,7 +13,6 @@ /* eslint-disable node/no-unsupported-features/node-builtins */ /* global document, window, TextEncoder, navigator */ -import { UAParser } from 'ua-parser-js'; import { webcrypto } from './crypto'; const isWindowsPhone = /windows phone|iemobile|wpdesktop/i; @@ -97,11 +96,19 @@ export function isIOS () { (/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream); } -export function isMobileSafari18 () { - if (isBrowser()) { - const { browser, os } = new UAParser().getResult(); - return os.name?.toLowerCase() === 'ios' && !!browser.name?.toLowerCase()?.includes('safari') - && browser.major === '18'; +const isIOSRegex = /iPad|iPhone|iPod/; +const v18Regex = /Version\/18(\.| |$)/; +const notSafariRegex = /EdgiOS|CriOS|Chrome/; + +/* eslint complexity:[0,8] */ +export function isSafari18 () { + if (isBrowser() && typeof navigator !== 'undefined' && typeof navigator.userAgent !== 'undefined') { + const isIOS = isIOSRegex.test(navigator.userAgent); + // Mobile Safari in desktop mode emulates Macintosh in user agent + const isDesktop = navigator.userAgent.includes('Macintosh'); + const isSafari18 = navigator.userAgent.includes('Safari/') && v18Regex.test(navigator.userAgent); + const isOtherBrowser = notSafariRegex.test(navigator.userAgent); + return isSafari18 && !isOtherBrowser && (isIOS || isDesktop); } return false; } diff --git a/lib/http/request.ts b/lib/http/request.ts index 3832df7f0..0cfcbcb5b 100644 --- a/lib/http/request.ts +++ b/lib/http/request.ts @@ -28,13 +28,13 @@ import { HttpResponse } from './types'; import { AuthApiError, OAuthError, APIError, WWWAuthError } from '../errors'; -import { isMobileSafari18 } from '../features'; +import { isSafari18 } from '../features'; // For iOS track last date when document became visible let dateDocumentBecameVisible = 0; let trackDateDocumentBecameVisible: () => void; -if (isMobileSafari18()) { +if (isSafari18()) { dateDocumentBecameVisible = Date.now(); trackDateDocumentBecameVisible = () => { if (!document.hidden) { @@ -165,7 +165,7 @@ export function httpRequest(sdk: OktaAuthHttpInterface, options: RequestOptions) var err, res, promise; - if (pollingIntent && isMobileSafari18()) { + if (pollingIntent && isSafari18()) { let waitForVisibleAndAwakenDocument: () => Promise; let waitForAwakenDocument: () => Promise; let recursiveFetch: () => Promise; diff --git a/package.json b/package.json index 0046df74b..2bcc6d9de 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "private": true, "name": "@okta/okta-auth-js", "description": "The Okta Auth SDK", - "version": "7.10.0", + "version": "7.10.1", "homepage": "https://github.com/okta/okta-auth-js", "license": "Apache-2.0", "main": "build/cjs/exports/default.js", @@ -162,7 +162,6 @@ "node-cache": "^5.1.2", "p-cancelable": "^2.0.0", "tiny-emitter": "1.1.0", - "ua-parser-js": "^2.0.0", "webcrypto-shim": "^0.1.5", "xhr2": "0.1.3" }, diff --git a/test/spec/TokenManager/browser.ts b/test/spec/TokenManager/browser.ts index 6f8ace6fe..2caf5d003 100644 --- a/test/spec/TokenManager/browser.ts +++ b/test/spec/TokenManager/browser.ts @@ -21,7 +21,7 @@ const mocked = { isIE11OrLess: () => false, isLocalhost: () => false, isTokenVerifySupported: () => true, - isMobileSafari18: () => false + isSafari18: () => false } }; jest.mock('../../../lib/features', () => { diff --git a/test/spec/TokenManager/expireEvents.ts b/test/spec/TokenManager/expireEvents.ts index 1c40e0f21..3fd9d242f 100644 --- a/test/spec/TokenManager/expireEvents.ts +++ b/test/spec/TokenManager/expireEvents.ts @@ -2,7 +2,7 @@ jest.mock('../../../lib/features', () => { return { isLocalhost: () => true, // to allow configuring expireEarlySeconds isIE11OrLess: () => false, - isMobileSafari18: () => false + isSafari18: () => false }; }); diff --git a/test/spec/authn/mfa-challenge.js b/test/spec/authn/mfa-challenge.js index bb06a88ab..4281659f2 100644 --- a/test/spec/authn/mfa-challenge.js +++ b/test/spec/authn/mfa-challenge.js @@ -32,7 +32,7 @@ jest.mock('lib/features', () => { const actual = jest.requireActual('../../../lib/features'); return { ...actual, - isMobileSafari18: () => false + isSafari18: () => false }; }); import OktaAuth from '@okta/okta-auth-js'; @@ -1581,7 +1581,7 @@ describe('MFA_CHALLENGE', function () { }); // mocks iOS environment - jest.spyOn(mocked.features, 'isMobileSafari18').mockReturnValue(true); + jest.spyOn(mocked.features, 'isSafari18').mockReturnValue(true); const { response: mfaPush } = await util.generateXHRPair({ uri: 'https://auth-js-test.okta.com' diff --git a/test/spec/features/browser.ts b/test/spec/features/browser.ts index 90918b171..a9cfc2b08 100644 --- a/test/spec/features/browser.ts +++ b/test/spec/features/browser.ts @@ -64,7 +64,7 @@ describe('features (browser)', function() { }); }); - describe('isIOS, isMobileSafari18', () => { + describe('isIOS, isSafari18', () => { const iOSAgents = [ 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/92.0.4515.90 Mobile/15E148 Safari/604.1', 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.1 Mobile/15E148 Safari/604.1', @@ -79,33 +79,47 @@ describe('features (browser)', function() { 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.2 Mobile/15E148 Safari/604.1', 'Mozilla/5.0 (iPad; CPU OS 18_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.2 Mobile/15E148 Safari/604.1', ]; + const desktopSafari18Agents = [ + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15', + ]; + const notSafariAgents = [ + 'Mozilla/5.0 (Linux; Android 15) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.260 Mobile Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/92.0.4515.90 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_2_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) EdgiOS/132.0.2957.32 Version/18.0 Mobile/15E148 Safari/604.1', + ]; for (let userAgent of iOSAgents) { - it('can succeed for ' + userAgent, () => { + it('isIOS() should be true for ' + userAgent, () => { jest.spyOn(global.navigator, 'userAgent', 'get').mockReturnValue(userAgent); expect(OktaAuth.features.isIOS()).toBe(true); - expect(OktaAuth.features.isMobileSafari18()).toBe(false); }); } - for (let userAgent of mobileSafari18Agents) { + for (let userAgent of [...mobileSafari18Agents, ...desktopSafari18Agents]) { // eslint-disable-next-line jasmine/no-spec-dupes - it('can succeed for ' + userAgent, () => { + it('isSafari18() should be true for ' + userAgent, () => { jest.spyOn(global.navigator, 'userAgent', 'get').mockReturnValue(userAgent); - expect(OktaAuth.features.isIOS()).toBe(true); - expect(OktaAuth.features.isMobileSafari18()).toBe(true); + expect(OktaAuth.features.isSafari18()).toBe(true); + }); + } + for (let userAgent of notSafariAgents) { + // eslint-disable-next-line jasmine/no-spec-dupes + it('isSafari18() should be false for ' + userAgent, () => { + jest.spyOn(global.navigator, 'userAgent', 'get').mockReturnValue(userAgent); + expect(OktaAuth.features.isSafari18()).toBe(false); }); } it('returns false if navigator is unavailable', () => { jest.spyOn(global, 'navigator', 'get').mockReturnValue(undefined as never); expect(OktaAuth.features.isIOS()).toBe(false); - expect(OktaAuth.features.isMobileSafari18()).toBe(false); + expect(OktaAuth.features.isSafari18()).toBe(false); }); it('returns false if userAgent is unavailable', () => { jest.spyOn(global.navigator, 'userAgent', 'get').mockReturnValue(undefined as never); expect(OktaAuth.features.isIOS()).toBe(false); - expect(OktaAuth.features.isMobileSafari18()).toBe(false); + expect(OktaAuth.features.isSafari18()).toBe(false); }); }); }); diff --git a/test/spec/http/request.ts b/test/spec/http/request.ts index 736db3ddb..23161a258 100644 --- a/test/spec/http/request.ts +++ b/test/spec/http/request.ts @@ -29,7 +29,7 @@ jest.mock('../../../lib/features', () => { isIE11OrLess: () => false, isLocalhost: () => false, isHTTPS: () => false, - isMobileSafari18: () => false + isSafari18: () => false }; }); @@ -376,7 +376,7 @@ describe('HTTP Requestor', () => { jest.mock('../../../lib/features', () => { return { ...mocked.features, - isMobileSafari18: () => true + isSafari18: () => true }; }); const { httpRequest: reloadedHttpRequest } = jest.requireActual('../../../lib/http'); @@ -390,7 +390,7 @@ describe('HTTP Requestor', () => { jest.mock('../../../lib/features', () => { return { ...mocked.features, - isMobileSafari18: () => false + isSafari18: () => false }; }); }); diff --git a/test/spec/idx/IdxStorageManager.ts b/test/spec/idx/IdxStorageManager.ts index 207dae6c2..899e75fd9 100644 --- a/test/spec/idx/IdxStorageManager.ts +++ b/test/spec/idx/IdxStorageManager.ts @@ -29,7 +29,7 @@ jest.mock('../../../lib/util', () => { jest.mock('../../../lib/features', () => { return { isBrowser: () => {}, - isMobileSafari18: () => false + isSafari18: () => false }; }); diff --git a/test/spec/oidc/OAuthStorageManager.ts b/test/spec/oidc/OAuthStorageManager.ts index a9a360ae6..5cf95bf5e 100644 --- a/test/spec/oidc/OAuthStorageManager.ts +++ b/test/spec/oidc/OAuthStorageManager.ts @@ -31,7 +31,7 @@ jest.mock('../../../lib/util', () => { jest.mock('../../../lib/features', () => { return { isBrowser: () => {}, - isMobileSafari18: () => false + isSafari18: () => false }; }); diff --git a/test/spec/oidc/endpoints/well-known.ts b/test/spec/oidc/endpoints/well-known.ts index bd2ef8765..63d3cd9c0 100644 --- a/test/spec/oidc/endpoints/well-known.ts +++ b/test/spec/oidc/endpoints/well-known.ts @@ -18,7 +18,7 @@ const mocked = { isBrowser: () => typeof window !== 'undefined', isIE11OrLess: () => false, isLocalhost: () => false, - isMobileSafari18: () => false + isSafari18: () => false } }; jest.mock('../../../../lib/features', () => { diff --git a/test/spec/oidc/util/prepareEnrollAuthenticatorParams.ts b/test/spec/oidc/util/prepareEnrollAuthenticatorParams.ts index a8dfe7d99..54d8892c8 100644 --- a/test/spec/oidc/util/prepareEnrollAuthenticatorParams.ts +++ b/test/spec/oidc/util/prepareEnrollAuthenticatorParams.ts @@ -17,7 +17,7 @@ const mocked = { isLocalhost: () => true, isHTTPS: () => false, isPKCESupported: () => true, - isMobileSafari18: () => false, + isSafari18: () => false, }, }; jest.mock('../../../../lib/features', () => { diff --git a/test/spec/oidc/util/prepareTokenParams.ts b/test/spec/oidc/util/prepareTokenParams.ts index 16bc627ad..b7b2a581c 100644 --- a/test/spec/oidc/util/prepareTokenParams.ts +++ b/test/spec/oidc/util/prepareTokenParams.ts @@ -20,7 +20,7 @@ const mocked = { isPKCESupported: () => true, hasTextEncoder: () => true, isDPoPSupported: () => true, - isMobileSafari18: () => false, + isSafari18: () => false, }, wellKnown: { getWellKnown: (): Promise => Promise.resolve() diff --git a/yarn.lock b/yarn.lock index 5ec1e6298..b4fe94d26 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4882,11 +4882,6 @@ destroy@1.2.0: resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== -detect-europe-js@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/detect-europe-js/-/detect-europe-js-0.1.2.tgz#aa76642e05dae786efc2e01a23d4792cd24c7b88" - integrity sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow== - detect-file@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7" @@ -7770,11 +7765,6 @@ is-shared-array-buffer@^1.0.2: dependencies: call-bind "^1.0.2" -is-standalone-pwa@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-standalone-pwa/-/is-standalone-pwa-0.1.1.tgz#7a1b0459471a95378aa0764d5dc0a9cec95f2871" - integrity sha512-9Cbovsa52vNQCjdXOzeQq5CnCbAcRk05aU62K20WO372NrTv0NxibLFCK6lQ4/iZEFdEA3p3t2VNOn8AJ53F5g== - is-stream@^2.0.0, is-stream@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" @@ -12808,20 +12798,6 @@ u2f-api-polyfill@0.4.3: resolved "https://registry.yarnpkg.com/u2f-api-polyfill/-/u2f-api-polyfill-0.4.3.tgz#b7ad165a6f962558517a867c5c4bf9399fcf7e98" integrity sha512-0DVykdzG3tKft2GciQCGzgO8BinDEfIhTBo7FKbLBmA+sVTPYmNOFbsZuduYQmnc3+ykUadTHNqXVqnvBfLCvg== -ua-is-frozen@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/ua-is-frozen/-/ua-is-frozen-0.1.2.tgz#bfbc5f06336e379590e36beca444188c7dc3a7f3" - integrity sha512-RwKDW2p3iyWn4UbaxpP2+VxwqXh0jpvdxsYpZ5j/MLLiQOfbsV5shpgQiw93+KMYQPcteeMQ289MaAFzs3G9pw== - -ua-parser-js@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-2.0.0.tgz#fae88e352510198bd29a6dd41624c7cd0d2c7ade" - integrity sha512-SASgD4RlB7+SCMmlVNqrhPw0f/2pGawWBzJ2+LwGTD0GgNnrKGzPJDiraGHJDwW9Zm5DH2lTmUpqDpbZjJY4+Q== - dependencies: - detect-europe-js "^0.1.2" - is-standalone-pwa "^0.1.1" - ua-is-frozen "^0.1.2" - uglify-js@^3.1.4: version "3.17.3" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.3.tgz#f0feedf019c4510f164099e8d7e72ff2d7304377"