diff --git a/lib/authn/util/poll.ts b/lib/authn/util/poll.ts index 6377472cb..989da99bf 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 { isIOS } from '../../features'; +import { isMobileSafari18 } from '../../features'; interface PollOptions { delay?: number; @@ -79,13 +79,14 @@ export function getPollFn(sdk, res: AuthnTransactionState, ref) { var href = pollLink.href + toQueryString(opts); return post(sdk, href, getStateToken(res), { saveAuthnState: false, - withCredentials: true + withCredentials: true, + pollingIntent: true, }); } const delayNextPoll = (ms) => { // no need for extra logic in non-iOS environments, just continue polling - if (!isIOS()) { + if (!isMobileSafari18()) { return delayFn(ms); } @@ -135,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 (isIOS() && document.hidden) { + if (isMobileSafari18() && document.hidden) { let handler; return new Promise((resolve) => { handler = () => { diff --git a/lib/base/types.ts b/lib/base/types.ts index c5a665f7e..3b36b95ce 100644 --- a/lib/base/types.ts +++ b/lib/base/types.ts @@ -29,6 +29,7 @@ export interface FeaturesAPI { isIE11OrLess(): boolean; isDPoPSupported(): boolean; isIOS(): boolean; + isMobileSafari18(): boolean; } diff --git a/lib/constants.ts b/lib/constants.ts index abcf80db4..37c0f521a 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -12,6 +12,8 @@ export const STATE_TOKEN_KEY_NAME = 'oktaStateToken'; export const DEFAULT_POLLING_DELAY = 500; +export const IOS_PAGE_AWAKEN_TIMEOUT = 500; +export const IOS_MAX_RETRY_COUNT = 3; export const DEFAULT_MAX_CLOCK_SKEW = 300; export const DEFAULT_CACHE_DURATION = 86400; export const TOKEN_STORAGE_NAME = 'okta-token-storage'; diff --git a/lib/features.ts b/lib/features.ts index d57f0181a..b7ba951f7 100644 --- a/lib/features.ts +++ b/lib/features.ts @@ -13,6 +13,7 @@ /* 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; @@ -95,3 +96,12 @@ export function isIOS () { // @ts-expect-error - MSStream is not in `window` type, unsurprisingly (/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'; + } + return false; +} diff --git a/lib/http/request.ts b/lib/http/request.ts index 3f935cda0..3832df7f0 100644 --- a/lib/http/request.ts +++ b/lib/http/request.ts @@ -14,7 +14,12 @@ /* eslint-disable complexity */ import { isString, clone, isAbsoluteUrl, removeNils } from '../util'; -import { STATE_TOKEN_KEY_NAME, DEFAULT_CACHE_DURATION } from '../constants'; +import { + STATE_TOKEN_KEY_NAME, + DEFAULT_CACHE_DURATION, + IOS_MAX_RETRY_COUNT, + IOS_PAGE_AWAKEN_TIMEOUT +} from '../constants'; import { OktaAuthHttpInterface, RequestOptions, @@ -23,7 +28,21 @@ import { HttpResponse } from './types'; import { AuthApiError, OAuthError, APIError, WWWAuthError } from '../errors'; +import { isMobileSafari18 } from '../features'; + +// For iOS track last date when document became visible +let dateDocumentBecameVisible = 0; +let trackDateDocumentBecameVisible: () => void; +if (isMobileSafari18()) { + dateDocumentBecameVisible = Date.now(); + trackDateDocumentBecameVisible = () => { + if (!document.hidden) { + dateDocumentBecameVisible = Date.now(); + } + }; + document.addEventListener('visibilitychange', trackDateDocumentBecameVisible); +} const formatError = (sdk: OktaAuthHttpInterface, error: HttpResponse | Error): AuthApiError | OAuthError => { if (error instanceof Error) { @@ -96,6 +115,7 @@ const formatError = (sdk: OktaAuthHttpInterface, error: HttpResponse | Error): A return err; }; +// eslint-disable-next-line max-statements export function httpRequest(sdk: OktaAuthHttpInterface, options: RequestOptions): Promise { options = options || {}; @@ -113,7 +133,8 @@ export function httpRequest(sdk: OktaAuthHttpInterface, options: RequestOptions) withCredentials = options.withCredentials === true, // default value is false storageUtil = sdk.options.storageUtil, storage = storageUtil!.storage, - httpCache = sdk.storageManager.getHttpCache(sdk.options.cookies); + httpCache = sdk.storageManager.getHttpCache(sdk.options.cookies), + pollingIntent = options.pollingIntent; if (options.cacheResponse) { var cacheContents = httpCache.getStorage(); @@ -142,8 +163,76 @@ export function httpRequest(sdk: OktaAuthHttpInterface, options: RequestOptions) withCredentials }; - var err, res; - return sdk.options.httpRequestClient!(method!, url!, ajaxOptions) + var err, res, promise; + + if (pollingIntent && isMobileSafari18()) { + let waitForVisibleAndAwakenDocument: () => Promise; + let waitForAwakenDocument: () => Promise; + let recursiveFetch: () => Promise; + let retryCount = 0; + + // Safari on iOS has a bug: + // Performing `fetch` right after document became visible can fail with `Load failed` error. + // Running fetch after short timeout fixes this issue. + waitForAwakenDocument = () => { + const timeSinceDocumentIsVisible = Date.now() - dateDocumentBecameVisible; + if (timeSinceDocumentIsVisible < IOS_PAGE_AWAKEN_TIMEOUT) { + return new Promise((resolve) => setTimeout(() => { + if (!document.hidden) { + resolve(); + } else { + resolve(waitForVisibleAndAwakenDocument()); + } + }, IOS_PAGE_AWAKEN_TIMEOUT - timeSinceDocumentIsVisible)); + } else { + return Promise.resolve(); + } + }; + + // Returns a promise that resolves when document is visible for 500 ms + waitForVisibleAndAwakenDocument = () => { + if (document.hidden) { + let pageVisibilityHandler: () => void; + return new Promise((resolve) => { + pageVisibilityHandler = () => { + if (!document.hidden) { + document.removeEventListener('visibilitychange', pageVisibilityHandler); + resolve(waitForAwakenDocument()); + } + }; + document.addEventListener('visibilitychange', pageVisibilityHandler); + }); + } else { + return waitForAwakenDocument(); + } + }; + + // Restarts fetch on 'Load failed' error + // This error can occur when `fetch` does not respond + // (due to CORS error, non-existing host, or network error) + const retryableFetch = (): Promise => { + return sdk.options.httpRequestClient!(method!, url!, ajaxOptions).catch((err) => { + const isNetworkError = err?.message === 'Load failed'; + if (isNetworkError && retryCount < IOS_MAX_RETRY_COUNT) { + retryCount++; + return recursiveFetch(); + } + throw err; + }); + }; + + // Final promise to fetch that wraps logic with waiting for visible document + // and retrying fetch request on network error + recursiveFetch = (): Promise => { + return waitForVisibleAndAwakenDocument().then(retryableFetch); + }; + + promise = recursiveFetch(); + } else { + promise = sdk.options.httpRequestClient!(method!, url!, ajaxOptions); + } + + return promise .then(function(resp) { res = resp.responseText; if (res && isString(res)) { diff --git a/lib/http/types.ts b/lib/http/types.ts index 46a060a16..ac5d4accc 100644 --- a/lib/http/types.ts +++ b/lib/http/types.ts @@ -33,6 +33,7 @@ export interface RequestOptions { storageUtil?: StorageUtil; cacheResponse?: boolean; headers?: RequestHeaders; + pollingIntent?: boolean; } export interface FetchOptions { diff --git a/lib/idx/idxState/v1/generateIdxAction.ts b/lib/idx/idxState/v1/generateIdxAction.ts index 23d2c8971..13b01707b 100644 --- a/lib/idx/idxState/v1/generateIdxAction.ts +++ b/lib/idx/idxState/v1/generateIdxAction.ts @@ -11,7 +11,7 @@ */ /* eslint-disable max-len, complexity */ -import { httpRequest } from '../../../http'; +import { httpRequest, RequestOptions } from '../../../http'; import { OktaAuthIdxInterface } from '../../types'; // auth-js/types import { IdxActionFunction, IdxActionParams, IdxResponse, IdxToPersist } from '../../types/idx-js'; import { divideActionParamsByMutability } from './actionParser'; @@ -36,13 +36,18 @@ const generateDirectFetch = function generateDirectFetch(authClient: OktaAuthIdx }); try { - const response = await httpRequest(authClient, { + const options: RequestOptions = { url: target, method: actionDefinition.method, headers, args: body, withCredentials: toPersist?.withCredentials ?? true - }); + }; + const isPolling = actionDefinition.name === 'poll' || actionDefinition.name?.endsWith('-poll'); + if (isPolling) { + options.pollingIntent = true; + } + const response = await httpRequest(authClient, options); return authClient.idx.makeIdxResponse({ ...response }, toPersist, true); } diff --git a/package.json b/package.json index a7f575c36..c00356066 100644 --- a/package.json +++ b/package.json @@ -162,6 +162,7 @@ "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" }, @@ -211,7 +212,7 @@ "rollup-plugin-cleanup": "^3.2.1", "rollup-plugin-license": "^2.8.1", "rollup-plugin-multi-input": "^1.3.1", - "rollup-plugin-typescript2": "^0.30.0", + "rollup-plugin-typescript2": "^0.36.0", "rollup-plugin-visualizer": "~5.5.4", "shelljs": "0.8.5", "ts-jest": "^28.0.2", diff --git a/test/spec/TokenManager/browser.ts b/test/spec/TokenManager/browser.ts index 7efb7f0db..6f8ace6fe 100644 --- a/test/spec/TokenManager/browser.ts +++ b/test/spec/TokenManager/browser.ts @@ -20,7 +20,8 @@ const mocked = { isBrowser: () => typeof window !== 'undefined', isIE11OrLess: () => false, isLocalhost: () => false, - isTokenVerifySupported: () => true + isTokenVerifySupported: () => true, + isMobileSafari18: () => false } }; jest.mock('../../../lib/features', () => { diff --git a/test/spec/TokenManager/expireEvents.ts b/test/spec/TokenManager/expireEvents.ts index 265940f3d..1c40e0f21 100644 --- a/test/spec/TokenManager/expireEvents.ts +++ b/test/spec/TokenManager/expireEvents.ts @@ -1,7 +1,8 @@ jest.mock('../../../lib/features', () => { return { isLocalhost: () => true, // to allow configuring expireEarlySeconds - isIE11OrLess: () => false + isIE11OrLess: () => false, + isMobileSafari18: () => false }; }); diff --git a/test/spec/authn/mfa-challenge.js b/test/spec/authn/mfa-challenge.js index dbae91f59..bb06a88ab 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, - isIOS: () => false + isMobileSafari18: () => false }; }); import OktaAuth from '@okta/okta-auth-js'; @@ -1581,7 +1581,7 @@ describe('MFA_CHALLENGE', function () { }); // mocks iOS environment - jest.spyOn(mocked.features, 'isIOS').mockReturnValue(true); + jest.spyOn(mocked.features, 'isMobileSafari18').mockReturnValue(true); const { response: mfaPush } = await util.generateXHRPair({ uri: 'https://auth-js-test.okta.com' @@ -1592,17 +1592,17 @@ describe('MFA_CHALLENGE', function () { }, 'success', 'https://auth-js-test.okta.com'); // mocks flow of wait, wait, wait, success - context.httpSpy = jest.spyOn(mocked.http, 'post') - .mockResolvedValueOnce(mfaPush.response) - .mockResolvedValueOnce(mfaPush.response) - .mockResolvedValueOnce(mfaPush.response) - .mockResolvedValueOnce(success.response); - + context.httpSpy = jest.fn() + .mockResolvedValueOnce({responseText: JSON.stringify(mfaPush.response)}) + .mockResolvedValueOnce({responseText: JSON.stringify(mfaPush.response)}) + .mockResolvedValueOnce({responseText: JSON.stringify(mfaPush.response)}) + .mockResolvedValueOnce({responseText: JSON.stringify(success.response)}); const oktaAuth = new OktaAuth({ - issuer: 'https://auth-js-test.okta.com' + issuer: 'https://auth-js-test.okta.com', + httpRequestClient: context.httpSpy }); - + context.transaction = oktaAuth.tx.createTransaction(mfaPush.response); }); diff --git a/test/spec/features/browser.ts b/test/spec/features/browser.ts index ba246d601..90918b171 100644 --- a/test/spec/features/browser.ts +++ b/test/spec/features/browser.ts @@ -64,29 +64,48 @@ describe('features (browser)', function() { }); }); - describe('isIOS', () => { - it('can succeed', () => { - 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', - '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_7_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.2 Mobile/15E148 Safari/604.1' - ]; + describe('isIOS, isMobileSafari18', () => { + 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', + '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_7_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.2 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) EdgiOS/130.0.2849.80 Version/18.0 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', + ]; + const mobileSafari18Agents = [ + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Teak/5.9 Version/18 Mobile/15E148 Safari/604.1', + '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', + ]; - for (let userAgent of iOSAgents) { + for (let userAgent of iOSAgents) { + it('can succeed 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) { + // eslint-disable-next-line jasmine/no-spec-dupes + it('can succeed for ' + userAgent, () => { + jest.spyOn(global.navigator, 'userAgent', 'get').mockReturnValue(userAgent); + expect(OktaAuth.features.isIOS()).toBe(true); + expect(OktaAuth.features.isMobileSafari18()).toBe(true); + }); + } 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); }); 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); }); }); }); diff --git a/test/spec/http/request.ts b/test/spec/http/request.ts index d9bd60aec..736db3ddb 100644 --- a/test/spec/http/request.ts +++ b/test/spec/http/request.ts @@ -13,16 +13,33 @@ declare var USER_AGENT: string; // set in jest config -import { httpRequest } from '../../../lib/http'; +import { httpRequest as originalHttpRequest } from '../../../lib/http'; import { OktaAuth, DEFAULT_CACHE_DURATION, AuthApiError, STATE_TOKEN_KEY_NAME } from '../../../lib/exports/core'; +import { setImmediate } from 'timers'; + + +jest.mock('../../../lib/features', () => { + return { + isBrowser: () => typeof window !== 'undefined', + isIE11OrLess: () => false, + isLocalhost: () => false, + isHTTPS: () => false, + isMobileSafari18: () => false + }; +}); + +const mocked = { + features: require('../../../lib/features'), +}; describe('HTTP Requestor', () => { let sdk; + let httpRequest = originalHttpRequest; let httpRequestClient; let url; let response1; @@ -347,4 +364,147 @@ describe('HTTP Requestor', () => { // TODO: OAuthError includes response object }); + + // OKTA-823470: iOS18 polling issue + // NOTE: only run these tests in browser environments + // eslint-disable-next-line no-extra-boolean-cast + (!!global.document ? describe : describe.skip)('iOS18 polling', () => { + beforeEach(() => { + jest.useFakeTimers(); + // Simulate iOS and reload `request.ts` module + jest.resetModules(); + jest.mock('../../../lib/features', () => { + return { + ...mocked.features, + isMobileSafari18: () => true + }; + }); + const { httpRequest: reloadedHttpRequest } = jest.requireActual('../../../lib/http'); + httpRequest = reloadedHttpRequest; + }); + + afterEach(() => { + jest.runOnlyPendingTimers(); + jest.useRealTimers(); + httpRequest = originalHttpRequest; + jest.mock('../../../lib/features', () => { + return { + ...mocked.features, + isMobileSafari18: () => false + }; + }); + }); + + const togglePageVisibility = () => { + (document as any).hidden = !document.hidden; + document.dispatchEvent(new Event('visibilitychange')); + }; + + const advanceTestTimers = async (ms?: number) => { + // see https://stackoverflow.com/a/52196951 for more info about jest/promises/timers + if (ms) { + jest.advanceTimersByTime(ms); + } else { + jest.runOnlyPendingTimers(); + } + // flushes promise queue + return new Promise(resolve => setImmediate(resolve)); + }; + + it('should wait for document to be visible for 500 ms before making request', async () => { + createAuthClient(); + expect(document.hidden).toBe(false); + + // Document is hidden + togglePageVisibility(); + const requestPromise = httpRequest(sdk, { + url, + pollingIntent: true, + }); + await advanceTestTimers(); + expect(httpRequestClient).toHaveBeenCalledTimes(0); + + // Document is visible for 200 ms + togglePageVisibility(); + await advanceTestTimers(200); + expect(httpRequestClient).toHaveBeenCalledTimes(0); + + // Document is visible for 600 ms + await advanceTestTimers(400); + expect(httpRequestClient).toHaveBeenCalledTimes(1); + expect(httpRequestClient).toHaveBeenCalledWith(undefined, url, { + data: undefined, + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'X-Okta-User-Agent-Extended': USER_AGENT + }, + withCredentials: false + }); + const res = await requestPromise; + expect(res).toBe(response1); + }); + + it('should retry on network error', async () => { + httpRequestClient = jest.fn() + .mockRejectedValueOnce(new TypeError('Load failed')) + .mockResolvedValueOnce({ + responseText: JSON.stringify(response1) + }); + createAuthClient(); + expect(document.hidden).toBe(false); + const requestPromise = httpRequest(sdk, { + url, + pollingIntent: true, + }); + await advanceTestTimers(); + expect(httpRequestClient).toHaveBeenCalledTimes(2); + expect(httpRequestClient).toHaveBeenCalledWith(undefined, url, { + data: undefined, + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'X-Okta-User-Agent-Extended': USER_AGENT + }, + withCredentials: false + }); + const res = await requestPromise; + expect(res).toBe(response1); + }); + + it('should retry on network error 3 times maximum', async () => { + httpRequestClient = jest.fn() + .mockRejectedValueOnce(new TypeError('Load failed')) + .mockRejectedValueOnce(new TypeError('Load failed')) + .mockRejectedValueOnce(new TypeError('Load failed')) + .mockRejectedValueOnce(new TypeError('Load failed')) + .mockResolvedValueOnce({ + responseText: JSON.stringify(response1) + }); + createAuthClient(); + expect(document.hidden).toBe(false); + let didThrow = false; + const requestPromise = httpRequest(sdk, { + url, + pollingIntent: true, + }).catch(error => { + didThrow = true; + expect((error as Error).message).toBe('Load failed'); + }); + await advanceTestTimers(); + expect(httpRequestClient).toHaveBeenCalledTimes(4); + expect(httpRequestClient).toHaveBeenCalledWith(undefined, url, { + data: undefined, + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'X-Okta-User-Agent-Extended': USER_AGENT + }, + withCredentials: false + }); + await requestPromise; + expect(didThrow).toBe(true); + }); + + }); }); \ No newline at end of file diff --git a/test/spec/idx/IdxStorageManager.ts b/test/spec/idx/IdxStorageManager.ts index 113b6efc1..207dae6c2 100644 --- a/test/spec/idx/IdxStorageManager.ts +++ b/test/spec/idx/IdxStorageManager.ts @@ -28,7 +28,8 @@ jest.mock('../../../lib/util', () => { jest.mock('../../../lib/features', () => { return { - isBrowser: () => {} + isBrowser: () => {}, + isMobileSafari18: () => false }; }); diff --git a/test/spec/oidc/OAuthStorageManager.ts b/test/spec/oidc/OAuthStorageManager.ts index 1e6978467..a9a360ae6 100644 --- a/test/spec/oidc/OAuthStorageManager.ts +++ b/test/spec/oidc/OAuthStorageManager.ts @@ -30,7 +30,8 @@ jest.mock('../../../lib/util', () => { jest.mock('../../../lib/features', () => { return { - isBrowser: () => {} + isBrowser: () => {}, + isMobileSafari18: () => false }; }); diff --git a/test/spec/oidc/endpoints/well-known.ts b/test/spec/oidc/endpoints/well-known.ts index 53148ed27..bd2ef8765 100644 --- a/test/spec/oidc/endpoints/well-known.ts +++ b/test/spec/oidc/endpoints/well-known.ts @@ -17,7 +17,8 @@ const mocked = { isHTTPS: () => false, isBrowser: () => typeof window !== 'undefined', isIE11OrLess: () => false, - isLocalhost: () => false + isLocalhost: () => false, + isMobileSafari18: () => false } }; jest.mock('../../../../lib/features', () => { diff --git a/test/spec/oidc/util/prepareEnrollAuthenticatorParams.ts b/test/spec/oidc/util/prepareEnrollAuthenticatorParams.ts index 4ab16cee3..a8dfe7d99 100644 --- a/test/spec/oidc/util/prepareEnrollAuthenticatorParams.ts +++ b/test/spec/oidc/util/prepareEnrollAuthenticatorParams.ts @@ -17,6 +17,7 @@ const mocked = { isLocalhost: () => true, isHTTPS: () => false, isPKCESupported: () => true, + isMobileSafari18: () => false, }, }; jest.mock('../../../../lib/features', () => { diff --git a/test/spec/oidc/util/prepareTokenParams.ts b/test/spec/oidc/util/prepareTokenParams.ts index 763b668da..16bc627ad 100644 --- a/test/spec/oidc/util/prepareTokenParams.ts +++ b/test/spec/oidc/util/prepareTokenParams.ts @@ -20,6 +20,7 @@ const mocked = { isPKCESupported: () => true, hasTextEncoder: () => true, isDPoPSupported: () => true, + isMobileSafari18: () => false, }, wellKnown: { getWellKnown: (): Promise => Promise.resolve() diff --git a/yarn.lock b/yarn.lock index 85974a41b..5ec1e6298 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1930,7 +1930,7 @@ estree-walker "^1.0.1" picomatch "^2.2.2" -"@rollup/pluginutils@^4.1.0", "@rollup/pluginutils@^4.2.1": +"@rollup/pluginutils@^4.1.2", "@rollup/pluginutils@^4.2.1": version "4.2.1" resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d" integrity sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ== @@ -4882,6 +4882,11 @@ 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" @@ -6195,7 +6200,7 @@ find-cache-dir@^2.0.0: make-dir "^2.0.0" pkg-dir "^3.0.0" -find-cache-dir@^3.3.1, find-cache-dir@^3.3.2: +find-cache-dir@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b" integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig== @@ -6408,7 +6413,16 @@ fs-constants@^1.0.0: resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== -fs-extra@8.1.0, fs-extra@^8.1.0: +fs-extra@^10.0.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" + integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-extra@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== @@ -7551,7 +7565,7 @@ is-callable@^1.1.4, is-callable@^1.2.7: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== -is-core-module@^2.2.0, is-core-module@^2.8.1, is-core-module@^2.9.0: +is-core-module@^2.8.1, is-core-module@^2.9.0: version "2.10.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.10.0.tgz#9012ede0a91c69587e647514e1d5277019e728ed" integrity sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg== @@ -7756,6 +7770,11 @@ 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" @@ -8689,6 +8708,15 @@ jsonfile@^4.0.0: optionalDependencies: graceful-fs "^4.1.6" +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + jsonpath-plus@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/jsonpath-plus/-/jsonpath-plus-6.0.1.tgz#9a3e16cedadfab07a3d8dc4e8cd5df4ed8f49c4d" @@ -10234,7 +10262,7 @@ path-key@^4.0.0: resolved "https://registry.yarnpkg.com/path-key/-/path-key-4.0.0.tgz#295588dc3aee64154f877adb9d780b81c554bf18" integrity sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ== -path-parse@^1.0.6, path-parse@^1.0.7: +path-parse@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== @@ -11128,14 +11156,6 @@ resolve.exports@^1.1.0: resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.0.tgz#5ce842b94b05146c0e03076985d1d0e7e48c90c9" integrity sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ== -resolve@1.20.0: - version "1.20.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" - integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== - dependencies: - is-core-module "^2.2.0" - path-parse "^1.0.6" - resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.14.2, resolve@^1.20.0, resolve@^1.22.0, resolve@^1.4.0, resolve@^1.9.0: version "1.22.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" @@ -11249,16 +11269,16 @@ rollup-plugin-multi-input@^1.3.1: fast-glob "^3.0.0" lodash "^4.17.11" -rollup-plugin-typescript2@^0.30.0: - version "0.30.0" - resolved "https://registry.yarnpkg.com/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.30.0.tgz#1cc99ac2309bf4b9d0a3ebdbc2002aecd56083d3" - integrity sha512-NUFszIQyhgDdhRS9ya/VEmsnpTe+GERDMmFo0Y+kf8ds51Xy57nPNGglJY+W6x1vcouA7Au7nsTgsLFj2I0PxQ== +rollup-plugin-typescript2@^0.36.0: + version "0.36.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.36.0.tgz#309564eb70d710412f5901344ca92045e180ed53" + integrity sha512-NB2CSQDxSe9+Oe2ahZbf+B4bh7pHwjV5L+RSYpCu7Q5ROuN94F9b6ioWwKfz3ueL3KTtmX4o2MUH2cgHDIEUsw== dependencies: - "@rollup/pluginutils" "^4.1.0" - find-cache-dir "^3.3.1" - fs-extra "8.1.0" - resolve "1.20.0" - tslib "2.1.0" + "@rollup/pluginutils" "^4.1.2" + find-cache-dir "^3.3.2" + fs-extra "^10.0.0" + semver "^7.5.4" + tslib "^2.6.2" rollup-plugin-visualizer@~5.5.4: version "5.5.4" @@ -11473,6 +11493,11 @@ semver@^6.0.0, semver@^6.1.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^7.5.4: + version "7.6.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + semver@~7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" @@ -12051,7 +12076,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -12069,6 +12094,15 @@ string-width@^1.0.1, string-width@^1.0.2: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -12129,7 +12163,14 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -12615,11 +12656,6 @@ tsconfig-paths@^4.1.0: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a" - integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A== - tslib@^1.8.1, tslib@^1.9.0: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" @@ -12772,6 +12808,20 @@ 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" @@ -12885,6 +12935,11 @@ universalify@^0.2.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== +universalify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== + unload@2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/unload/-/unload-2.3.1.tgz#9d16862d372a5ce5cb630ad1309c2fd6e35dacfe" @@ -13585,7 +13640,7 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -13611,6 +13666,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"