Repository: authts/oidc-client-ts
Pull request #1626 (2024-08-17 18:18:26)

Popup Abort Signals
Closes: #1625

- Adds `popupSignal` property to `signinPopup` and `signoutPop` methods to cancel windows similar to [fetch signal](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#canceling_a_request)

### Checklist

- [x] This PR makes changes to the public API <!-- was the API report (docs/oidc-client-ts.api.md) updated by this PR? -->
- [x] I have included links for closing relevant issue numbers



Patch:
diff --git a/docs/index.md b/docs/index.md
index c55caba9..db2d9d85 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -82,18 +82,19 @@ The [User](classes/User.html) type is returned from the [UserManager](classes/Us
 
 
 # Logging
-The oidc-client-ts library supports logging. You can set a logger by assigning `Oidc.Log.logger` to anything that supports a `info`, `warn`, and `error` methods that accept a params array. By default, no logger is configured.
+The oidc-client-ts library supports logging. You can set a logger to anything that supports a `info`, `warn`, and `error` methods that accept a params array by calling `Log.setLogger()`. By default, no logger is configured.
 
 The `console` object in the browser supports these, so a common way to easily
 enable logging in the browser is to simply add this code:
 
 ```javascript
-Oidc.Log.setLogger(console);
+import { Log } from 'oidc-client-ts';
+Log.setLogger(console);
 ```
 
 Also, logging has levels so you can control the verbosity by calling
-`Oidc.Log.setLevel()` with one of `Oidc.Log.NONE`, `Oidc.Log.ERROR`,
-`Oidc.Log.WARN`, or `Oidc.Log.INFO`. The default is `Oidc.Log.INFO`.
+`Log.setLevel()` with one of `Log.NONE`, `Log.ERROR`,
+`Log.WARN`, or `Log.INFO`. The default is `Log.INFO`.
 
 # Provider specific settings
 Additional provider specific settings may be needed for a flawless operation:
diff --git a/docs/oidc-client-ts.api.md b/docs/oidc-client-ts.api.md
index 838553e6..41faf371 100644
--- a/docs/oidc-client-ts.api.md
+++ b/docs/oidc-client-ts.api.md
@@ -581,6 +581,7 @@ export interface PopupWindowFeatures {
 
 // @public (undocumented)
 export interface PopupWindowParams {
+    popupSignal?: AbortSignal | null;
     // (undocumented)
     popupWindowFeatures?: PopupWindowFeatures;
     // (undocumented)
diff --git a/package-lock.json b/package-lock.json
index 7666a88b..e1d8d3bb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1511,18 +1511,18 @@
       }
     },
     "node_modules/@microsoft/api-extractor": {
-      "version": "7.47.2",
-      "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.47.2.tgz",
-      "integrity": "sha512-YWE2HGrSTZaPPSr7xiNizSuViZpC7Jsa7+DwRW5rYVgrMXNbfX/PpBOoSkl5uaz9I2sv2JKLJ75kVNt64BvS3g==",
+      "version": "7.47.6",
+      "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.47.6.tgz",
+      "integrity": "sha512-saI7n319+PdJ8PAePr14LWeIPOW2fHSr3KZfYFqJ2VUpIc1TTSh6ATFZfLPWI1LK7eZHun8+FpNsuxonyvxTgQ==",
       "dev": true,
       "dependencies": {
-        "@microsoft/api-extractor-model": "7.29.3",
+        "@microsoft/api-extractor-model": "7.29.5",
         "@microsoft/tsdoc": "~0.15.0",
         "@microsoft/tsdoc-config": "~0.17.0",
-        "@rushstack/node-core-library": "5.5.0",
-        "@rushstack/rig-package": "0.5.2",
-        "@rushstack/terminal": "0.13.2",
-        "@rushstack/ts-command-line": "4.22.2",
+        "@rushstack/node-core-library": "5.6.0",
+        "@rushstack/rig-package": "0.5.3",
+        "@rushstack/terminal": "0.13.4",
+        "@rushstack/ts-command-line": "4.22.5",
         "lodash": "~4.17.15",
         "minimatch": "~3.0.3",
         "resolve": "~1.22.1",
@@ -1535,14 +1535,14 @@
       }
     },
     "node_modules/@microsoft/api-extractor-model": {
-      "version": "7.29.3",
-      "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.29.3.tgz",
-      "integrity": "sha512-kEWjLr2ygL3ku9EGyjeTnL2S5IxyH9NaF1k1UoI0Nzwr4xEJBSWCVsWuF2+0lPUrRPA6mTY95fR264SJ5ETKQA==",
+      "version": "7.29.5",
+      "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.29.5.tgz",
+      "integrity": "sha512-axMwj4pgtYH6/IclP9ly33laSwTym1kBwSUcoHElc2LYAE5NNlhGT78ucEpIZtqEZaGgA8yxGXIyS17XCC2Iuw==",
       "dev": true,
       "dependencies": {
         "@microsoft/tsdoc": "~0.15.0",
         "@microsoft/tsdoc-config": "~0.17.0",
-        "@rushstack/node-core-library": "5.5.0"
+        "@rushstack/node-core-library": "5.6.0"
       }
     },
     "node_modules/@microsoft/api-extractor/node_modules/minimatch": {
@@ -1630,9 +1630,9 @@
       }
     },
     "node_modules/@rushstack/node-core-library": {
-      "version": "5.5.0",
-      "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.5.0.tgz",
-      "integrity": "sha512-Cl3MYQ74Je5Y/EngMxcA3SpHjGZ/022nKbAO1aycGfQ+7eKyNCBu0oywj5B1f367GCzuHBgy+3BlVLKysHkXZw==",
+      "version": "5.6.0",
+      "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.6.0.tgz",
+      "integrity": "sha512-3ixIcEHseqU1sbnvoQkvxvfTYWbi1IIhnq/vexJcex7j6D8lnQCiYnd/E2oXbUH0Zv48CjtfslC/2MVFd71mpg==",
       "dev": true,
       "dependencies": {
         "ajv": "~8.13.0",
@@ -1690,9 +1690,9 @@
       "dev": true
     },
     "node_modules/@rushstack/rig-package": {
-      "version": "0.5.2",
-      "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.5.2.tgz",
-      "integrity": "sha512-mUDecIJeH3yYGZs2a48k+pbhM6JYwWlgjs2Ca5f2n1G2/kgdgP9D/07oglEGf6mRyXEnazhEENeYTSNDRCwdqA==",
+      "version": "0.5.3",
+      "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.5.3.tgz",
+      "integrity": "sha512-olzSSjYrvCNxUFZowevC3uz8gvKr3WTpHQ7BkpjtRpA3wK+T0ybep/SRUMfr195gBzJm5gaXw0ZMgjIyHqJUow==",
       "dev": true,
       "dependencies": {
         "resolve": "~1.22.1",
@@ -1700,12 +1700,12 @@
       }
     },
     "node_modules/@rushstack/terminal": {
-      "version": "0.13.2",
-      "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.13.2.tgz",
-      "integrity": "sha512-t8i0PsGvBHmFBY8pryO3badqFlxQsm2rw3KYrzjcmVkG/WGklKg1qVkr9beAS1Oe8XWDRgj6SkoHkpNjs7aaNw==",
+      "version": "0.13.4",
+      "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.13.4.tgz",
+      "integrity": "sha512-h7g2RuffpqBCDKOijlUmvQ0b2O9kpIOK9TWCX9IR+2kvudp6MdtCYDu29zeqweWwCSWUnuAaUfB5HT88s0YCiw==",
       "dev": true,
       "dependencies": {
-        "@rushstack/node-core-library": "5.5.0",
+        "@rushstack/node-core-library": "5.6.0",
         "supports-color": "~8.1.1"
       },
       "peerDependencies": {
@@ -1742,12 +1742,12 @@
       }
     },
     "node_modules/@rushstack/ts-command-line": {
-      "version": "4.22.2",
-      "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.22.2.tgz",
-      "integrity": "sha512-xkvrGd6D9dPlI3I401Thc640WNsEPB1sGEmy12a2VJaPQPwhE6Ik0gEVPZJ/2G1w213eaCAdxUY1xpiTulsmpA==",
+      "version": "4.22.5",
+      "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.22.5.tgz",
+      "integrity": "sha512-eFm+5DJboPHAy3epLNQtmG+hDlBzS950g26nZPbciMQeXmZ5shGGNe6ERjV77wnr5IuxfLhYGJ4ZjPy8Z56MBA==",
       "dev": true,
       "dependencies": {
-        "@rushstack/terminal": "0.13.2",
+        "@rushstack/terminal": "0.13.4",
         "@types/argparse": "1.0.38",
         "argparse": "~1.0.9",
         "string-argv": "~0.3.1"
@@ -1784,9 +1784,9 @@
       }
     },
     "node_modules/@testing-library/jest-dom": {
-      "version": "6.4.6",
-      "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.6.tgz",
-      "integrity": "sha512-8qpnGVincVDLEcQXWaHOf6zmlbwTKc6Us6PPu4CRnPXCzo2OGBS5cwgMMOWdxDpEz1mkbvXHpEy99M5Yvt682w==",
+      "version": "6.4.8",
+      "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.8.tgz",
+      "integrity": "sha512-JD0G+Zc38f5MBHA4NgxQMR5XtO5Jx9g86jqturNTt2WUfRmLDIY7iKkWHDCCTiDuFMre6nxAD5wHw9W5kI4rGw==",
       "dev": true,
       "dependencies": {
         "@adobe/css-tools": "^4.4.0",
@@ -1802,30 +1802,6 @@
         "node": ">=14",
         "npm": ">=6",
         "yarn": ">=1"
-      },
-      "peerDependencies": {
-        "@jest/globals": ">= 28",
-        "@types/bun": "latest",
-        "@types/jest": ">= 28",
-        "jest": ">= 28",
-        "vitest": ">= 0.32"
-      },
-      "peerDependenciesMeta": {
-        "@jest/globals": {
-          "optional": true
-        },
-        "@types/bun": {
-          "optional": true
-        },
-        "@types/jest": {
-          "optional": true
-        },
-        "jest": {
-          "optional": true
-        },
-        "vitest": {
-          "optional": true
-        }
       }
     },
     "node_modules/@testing-library/jest-dom/node_modules/ansi-styles": {
@@ -4461,12 +4437,12 @@
       }
     },
     "node_modules/husky": {
-      "version": "9.0.11",
-      "resolved": "https://registry.npmjs.org/husky/-/husky-9.0.11.tgz",
-      "integrity": "sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw==",
+      "version": "9.1.4",
+      "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.4.tgz",
+      "integrity": "sha512-bho94YyReb4JV7LYWRWxZ/xr6TtOTt8cMfmQ39MQYJ7f/YE268s3GdghGwi+y4zAeqewE5zYLvuhV0M0ijsDEA==",
       "dev": true,
       "bin": {
-        "husky": "bin.mjs"
+        "husky": "bin.js"
       },
       "engines": {
         "node": ">=18"
@@ -7103,9 +7079,9 @@
       }
     },
     "node_modules/ts-jest": {
-      "version": "29.2.3",
-      "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.3.tgz",
-      "integrity": "sha512-yCcfVdiBFngVz9/keHin9EnsrQtQtEu3nRykNy9RVp+FiPFFbPJ3Sg6Qg4+TkmH0vMP5qsTKgXSsk80HRwvdgQ==",
+      "version": "29.2.4",
+      "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.4.tgz",
+      "integrity": "sha512-3d6tgDyhCI29HlpwIq87sNuI+3Q6GLTTCeYRHCs7vDz+/3GCMwEtV9jezLyl4ZtnBgx00I7hm8PCP8cTksMGrw==",
       "dev": true,
       "dependencies": {
         "bs-logger": "0.x",
diff --git a/src/UserManager.ts b/src/UserManager.ts
index aafe47c5..7f3ab43f 100644
--- a/src/UserManager.ts
+++ b/src/UserManager.ts
@@ -253,6 +253,7 @@ export class UserManager {
         const {
             popupWindowFeatures,
             popupWindowTarget,
+            popupSignal,
             ...requestArgs
         } = args;
         const url = this.settings.popup_redirect_uri;
@@ -260,7 +261,7 @@ export class UserManager {
             logger.throw(new Error("No popup_redirect_uri configured"));
         }
 
-        const handle = await this._popupNavigator.prepare({ popupWindowFeatures, popupWindowTarget });
+        const handle = await this._popupNavigator.prepare({ popupWindowFeatures, popupWindowTarget, popupSignal });
         const user = await this._signin({
             request_type: "si:p",
             redirect_uri: url,
@@ -597,11 +598,12 @@ export class UserManager {
         const {
             popupWindowFeatures,
             popupWindowTarget,
+            popupSignal,
             ...requestArgs
         } = args;
         const url = this.settings.popup_post_logout_redirect_uri;
 
-        const handle = await this._popupNavigator.prepare({ popupWindowFeatures, popupWindowTarget });
+        const handle = await this._popupNavigator.prepare({ popupWindowFeatures, popupWindowTarget, popupSignal });
         await this._signout({
             request_type: "so:p",
             post_logout_redirect_uri: url,
diff --git a/src/navigators/PopupNavigator.ts b/src/navigators/PopupNavigator.ts
index 11c80fce..d9875fb7 100644
--- a/src/navigators/PopupNavigator.ts
+++ b/src/navigators/PopupNavigator.ts
@@ -12,13 +12,14 @@ import type { UserManagerSettingsStore } from "../UserManagerSettings";
 export class PopupNavigator implements INavigator {
     private readonly _logger = new Logger("PopupNavigator");
 
-    constructor(private _settings: UserManagerSettingsStore) {}
+    constructor(private _settings: UserManagerSettingsStore) { }
 
     public async prepare({
         popupWindowFeatures = this._settings.popupWindowFeatures,
         popupWindowTarget = this._settings.popupWindowTarget,
+        popupSignal,
     }: PopupWindowParams): Promise<PopupWindow> {
-        return new PopupWindow({ popupWindowFeatures, popupWindowTarget });
+        return new PopupWindow({ popupWindowFeatures, popupWindowTarget, popupSignal });
     }
 
     public async callback(url: string, { keepOpen = false }): Promise<void> {
diff --git a/src/navigators/PopupWindow.ts b/src/navigators/PopupWindow.ts
index 4f5144e3..6f626aa3 100644
--- a/src/navigators/PopupWindow.ts
+++ b/src/navigators/PopupWindow.ts
@@ -15,6 +15,8 @@ const second = 1000;
 export interface PopupWindowParams {
     popupWindowFeatures?: PopupWindowFeatures;
     popupWindowTarget?: string;
+    /** An AbortSignal to set request's signal. */
+    popupSignal?: AbortSignal | null;
 }
 
 /**
@@ -28,10 +30,18 @@ export class PopupWindow extends AbstractChildWindow {
     public constructor({
         popupWindowTarget = DefaultPopupTarget,
         popupWindowFeatures = {},
+        popupSignal,
     }: PopupWindowParams) {
         super();
         const centeredPopup = PopupUtils.center({ ...DefaultPopupWindowFeatures, ...popupWindowFeatures });
         this._window = window.open(undefined, popupWindowTarget, PopupUtils.serialize(centeredPopup));
+
+        if (popupSignal) {
+            popupSignal.addEventListener("abort", () => {
+                void this._abort.raise(new Error(popupSignal.reason ?? "Popup aborted"));
+            });
+        }
+
         if (popupWindowFeatures.closePopupWindowAfterInSeconds && popupWindowFeatures.closePopupWindowAfterInSeconds > 0) {
             setTimeout(() => {
                 if (!this._window || typeof this._window.closed !== "boolean" || this._window.closed) {


Test patch:
diff --git a/src/navigators/PopupWindow.test.ts b/src/navigators/PopupWindow.test.ts
index 6a203f8a..73b169cc 100644
--- a/src/navigators/PopupWindow.test.ts
+++ b/src/navigators/PopupWindow.test.ts
@@ -141,6 +141,18 @@ describe("PopupWindow", () => {
         jest.runAllTimers();
     });
 
+    it("should reject when the window is aborted by signal", async () => {
+        const customReason = "Custom reason";
+        const controller = new AbortController();
+        const popupWindow = new PopupWindow({ popupSignal: controller.signal });
+
+        const promise = popupWindow.navigate({ url: "http://sts/authorize?x=y", state: "someid" });
+        controller.abort(customReason);
+
+        await expect(promise).rejects.toThrow(customReason);
+        jest.runAllTimers();
+    });
+
     it("should notify the parent window", async () => {
         PopupWindow.notifyOpener("http://sts/authorize?x=y", false);
         expect((window.opener as WindowProxy).postMessage).toHaveBeenCalledWith({