From cb538d0426f0c76078da8eb442e7c024f3ff0c08 Mon Sep 17 00:00:00 2001
From: Jared Perreault <jared.perreault@okta.com>
Date: Fri, 17 Jan 2025 18:52:58 -0500
Subject: [PATCH 1/2] feat: adds advancedOption to getWithPopup

---
 lib/oidc/getToken.ts           | 23 ++++++++----
 lib/oidc/getWithPopup.ts       |  9 +++--
 lib/oidc/types/api.ts          |  5 ++-
 test/spec/oidc/getWithPopup.ts | 64 ++++++++++++++++++++++++++++++++++
 yarn.lock                      | 31 ++--------------
 5 files changed, 95 insertions(+), 37 deletions(-)

diff --git a/lib/oidc/getToken.ts b/lib/oidc/getToken.ts
index 5c6b05ff1..d56199682 100644
--- a/lib/oidc/getToken.ts
+++ b/lib/oidc/getToken.ts
@@ -166,14 +166,25 @@ export function getToken(sdk: OktaAuthOAuthInterface, options: TokenParams & Pop
             popupWindow.location.assign(requestUrl);
           }
 
-          // The popup may be closed without receiving an OAuth response. Setup a poller to monitor the window.
           var popupPromise = new Promise(function (resolve, reject) {
-            var closePoller = setInterval(function () {
-              if (!popupWindow || popupWindow.closed) {
-                clearInterval(closePoller);
-                reject(new AuthSdkError('Unable to parse OAuth flow response'));
+            let closePoller;
+            // Using external IDPs within a popup can cause `popupWindow.closed` to prematurely return `true`
+            // the origin of the popup changes (to the external IDP). Disable the popup monitor to avoid this
+            // WARNING: This resulting promise will remain pending until the user completes the flow or timeout
+            if (options?.monitorPopupWindow === false) {
+              if (!options.timeout) {
+                throw new AuthSdkError('`timeout` must be set when `monitorPopupWindow` is set to `false`!');
               }
-            }, 100);
+            }
+            else {
+              // The popup may be closed without receiving an OAuth response. Setup a poller to monitor the window.
+              closePoller = setInterval(function () {
+                if (!popupWindow || popupWindow.closed) {
+                  clearInterval(closePoller);
+                  reject(new AuthSdkError('Unable to parse OAuth flow response'));
+                }
+              }, 100);
+            }
 
             // Proxy the OAuth promise results
             oauthPromise
diff --git a/lib/oidc/getWithPopup.ts b/lib/oidc/getWithPopup.ts
index b4c153576..99cb89445 100644
--- a/lib/oidc/getWithPopup.ts
+++ b/lib/oidc/getWithPopup.ts
@@ -11,12 +11,17 @@
  *
  */
 import { AuthSdkError } from '../errors';
-import { OktaAuthOAuthInterface, TokenParams, TokenResponse } from './types';
+import { OktaAuthOAuthInterface, WithPopupOptions, TokenResponse } from './types';
 import { clone } from '../util';
 import { getToken } from './getToken';
 import { loadPopup } from './util';
 
-export function getWithPopup(sdk: OktaAuthOAuthInterface, options: TokenParams): Promise<TokenResponse> {
+
+// `monitorPopupWindow: false` is an advance setting. Use with caution! 
+// When `false`, polling to determine whether or not the popup is open is perforemd.
+// A `timeout` must be provided when `monitorPopupWindow` is set to `false`
+
+export function getWithPopup(sdk: OktaAuthOAuthInterface, options: WithPopupOptions): Promise<TokenResponse> {
   if (arguments.length > 2) {
     return Promise.reject(new AuthSdkError('As of version 3.0, "getWithPopup" takes only a single set of options'));
   }
diff --git a/lib/oidc/types/api.ts b/lib/oidc/types/api.ts
index 1299cd9f8..cde4e633f 100644
--- a/lib/oidc/types/api.ts
+++ b/lib/oidc/types/api.ts
@@ -25,6 +25,7 @@ import { Endpoints } from './endpoints';
 export interface PopupParams {
   popupTitle?: string;
   popupWindow?: Window;
+  monitorPopupWindow?: boolean;
 }
 
 export interface TokenResponse {
@@ -57,6 +58,8 @@ export interface BaseTokenAPI {
   exchangeCodeForTokens(params: TokenParams, urls?: CustomUrls): Promise<TokenResponse>;
 }
 
+export type WithPopupOptions = TokenParams & { monitorPopupWindow?: boolean; };
+
 export interface TokenAPI extends BaseTokenAPI {
   getUserInfo<S extends CustomUserClaims = CustomUserClaims>(
     accessToken?: AccessToken,
@@ -65,7 +68,7 @@ export interface TokenAPI extends BaseTokenAPI {
   getWithRedirect: GetWithRedirectFunction;
   parseFromUrl: ParseFromUrlInterface;
   getWithoutPrompt(params?: TokenParams): Promise<TokenResponse>;
-  getWithPopup(params?: TokenParams): Promise<TokenResponse>;
+  getWithPopup(params?: WithPopupOptions): Promise<TokenResponse>;
   revoke(token: RevocableToken): Promise<object>;
   renew(token: Token): Promise<Token | undefined>;
   renewTokens(options?: RenewTokensParams): Promise<Tokens>;
diff --git a/test/spec/oidc/getWithPopup.ts b/test/spec/oidc/getWithPopup.ts
index 0aa7de99a..da812e8f6 100644
--- a/test/spec/oidc/getWithPopup.ts
+++ b/test/spec/oidc/getWithPopup.ts
@@ -24,6 +24,9 @@ import tokens from '@okta/test.support/tokens';
 import util from '@okta/test.support/util';
 import oauthUtil from '@okta/test.support/oauthUtil';
 import waitFor from '@okta/test.support/waitFor';
+import { setImmediate } from 'timers';
+import { AuthSdkError } from 'lib/errors';
+
 
 describe('token.getWithPopup', function() {
   beforeEach(() => {
@@ -499,4 +502,65 @@ describe('token.getWithPopup', function() {
     });
   });
 
+  it('does not detect when popup window closes', async function () {
+    // `oauthUtil.setupPopup` performs 2 assertions (other 3 are locally in this test)
+    expect.assertions(5);   // ensures the expect()s in .catch are called
+    const intervalSpy = jest.spyOn(global, 'setInterval');
+
+    jest.useFakeTimers();
+    const promise = oauthUtil.setupPopup({
+      willFail: true,
+      closePopup: true,
+      oktaAuthArgs: {
+        pkce: false,
+        issuer: 'https://auth-js-test.okta.com/oauth2/aus8aus76q8iphupD0h7',
+        clientId: 'NPSfOkH5eZrTy8PMDlvx',
+        redirectUri: 'https://example.com/redirect'
+      },
+      getWithPopupArgs: {
+        monitorPopupWindow: false,
+        timeout: 120000
+      },
+    })
+    .catch(err => {
+      expect(err).toBeInstanceOf(AuthSdkError);
+      expect(err.message).toBe('OAuth flow timed out');
+    });
+
+    // flushes promise queue (can be replaced by `jest.advanceTimersByTimeAsync` in jest@29)
+    await (new Promise(resolve => setImmediate(resolve)));
+    jest.advanceTimersByTime(150000);
+    await promise;
+
+    expect(intervalSpy).not.toHaveBeenCalled();
+
+    jest.useRealTimers();
+  });
+
+  it('throws if timeout is not provided', async function () {
+    // `oauthUtil.setupPopup` performs 2 assertions (other 3 are locally in this test)
+    expect.assertions(5);   // ensures the expect()s in .catch are called
+    const intervalSpy = jest.spyOn(global, 'setInterval');
+
+    await oauthUtil.setupPopup({
+      willFail: true,
+      closePopup: true,
+      oktaAuthArgs: {
+        pkce: false,
+        issuer: 'https://auth-js-test.okta.com/oauth2/aus8aus76q8iphupD0h7',
+        clientId: 'NPSfOkH5eZrTy8PMDlvx',
+        redirectUri: 'https://example.com/redirect'
+      },
+      getWithPopupArgs: {
+        monitorPopupWindow: false,
+      },
+    })
+    .catch(err => {
+      expect(err).toBeInstanceOf(AuthSdkError);
+      expect(err.message).toBe('`timeout` must be set when `monitorPopupWindow` is set to `false`!');
+    });
+
+    expect(intervalSpy).not.toHaveBeenCalled();
+  });
+
 });
diff --git a/yarn.lock b/yarn.lock
index b4fe94d26..131f2ba4b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -12066,7 +12066,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-cjs@npm:string-width@^4.2.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==
@@ -12084,15 +12084,6 @@ 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"
@@ -12153,14 +12144,7 @@ string_decoder@~1.1.1:
   dependencies:
     safe-buffer "~5.1.0"
 
-"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:
+"strip-ansi-cjs@npm:strip-ansi@^6.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==
@@ -13616,7 +13600,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-cjs@npm:wrap-ansi@^7.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==
@@ -13642,15 +13626,6 @@ 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"

From 101c92a2ccbbbf6ba1462d017387982ddfc66e22 Mon Sep 17 00:00:00 2001
From: Jared Perreault <jared.perreault@okta.com>
Date: Fri, 17 Jan 2025 19:31:07 -0500
Subject: [PATCH 2/2] CHLOG

---
 CHANGELOG.md | 6 ++++++
 package.json | 2 +-
 2 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 71de06ba4..f3ed4257a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
 # Changelog
 
+# 7.10.2
+
+### Other
+
+- [#1566](https://github.com/okta/okta-auth-js/pull/1566) feat: adds advance option `monitorPopupWindow` to `getWithPopup`
+
 # 7.10.1
 
 ### Other
diff --git a/package.json b/package.json
index 2bcc6d9de..0a3dcea98 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.1",
+  "version": "7.10.2",
   "homepage": "https://github.com/okta/okta-auth-js",
   "license": "Apache-2.0",
   "main": "build/cjs/exports/default.js",