From 088138ad8b4dacfe5863e98b04537c5aa828b08b Mon Sep 17 00:00:00 2001 From: yogeshwaran-c Date: Thu, 30 Apr 2026 17:44:19 +0530 Subject: [PATCH] fix(oauth2-redirect): guard window.opener for cross-origin flows Cross-origin navigation can sever window.opener (browser policy), causing the redirect page to crash with TypeError when accessing window.opener.swaggerUIRedirectOauth2. Guard the access and surface a clear message to the user instead of failing silently. Refs: #10786, #6150 --- dev-helpers/oauth2-redirect.js | 4 ++ test/unit/core/oauth2-redirect.js | 110 ++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 test/unit/core/oauth2-redirect.js diff --git a/dev-helpers/oauth2-redirect.js b/dev-helpers/oauth2-redirect.js index c0377806b8c..85ab2820cf6 100644 --- a/dev-helpers/oauth2-redirect.js +++ b/dev-helpers/oauth2-redirect.js @@ -1,5 +1,9 @@ "use strict" function run () { + if (!window.opener || !window.opener.swaggerUIRedirectOauth2) { + document.body.innerText = "OAuth redirect cannot complete because the window that started the flow is no longer reachable. Close this tab and start authorization again from Swagger UI." + return + } var oauth2 = window.opener.swaggerUIRedirectOauth2 var sentState = oauth2.state var redirectUrl = oauth2.redirectUrl diff --git a/test/unit/core/oauth2-redirect.js b/test/unit/core/oauth2-redirect.js new file mode 100644 index 00000000000..38594923115 --- /dev/null +++ b/test/unit/core/oauth2-redirect.js @@ -0,0 +1,110 @@ +/** + * @prettier + */ +import fs from "fs" +import path from "path" + +const REDIRECT_SCRIPT_PATH = path.resolve( + __dirname, + "../../../dev-helpers/oauth2-redirect.js" +) +const REDIRECT_SCRIPT = fs.readFileSync(REDIRECT_SCRIPT_PATH, "utf8") + +const setLocation = (search, hash) => { + Object.defineProperty(window, "location", { + configurable: true, + value: { + hash: hash || "", + search: search || "", + }, + }) +} + +const setOpener = (opener) => { + Object.defineProperty(window, "opener", { + configurable: true, + value: opener, + }) +} + +const runRedirectScript = () => { + // Evaluate the script in the current jsdom window context + // eslint-disable-next-line no-new-func + new Function(REDIRECT_SCRIPT)() +} + +describe("oauth2-redirect", () => { + let originalLocation + let originalOpener + let closeSpy + + beforeEach(() => { + originalLocation = window.location + originalOpener = window.opener + while (document.body.firstChild) { + document.body.removeChild(document.body.firstChild) + } + closeSpy = jest.spyOn(window, "close").mockImplementation(() => {}) + setLocation("", "") + }) + + afterEach(() => { + Object.defineProperty(window, "location", { + configurable: true, + value: originalLocation, + }) + setOpener(originalOpener) + closeSpy.mockRestore() + }) + + describe("when window.opener is null", () => { + it("renders an explanatory message and does not throw", () => { + setOpener(null) + + expect(() => runRedirectScript()).not.toThrow() + expect(document.body.innerText).toMatch(/OAuth redirect cannot complete/) + expect(closeSpy).not.toHaveBeenCalled() + }) + }) + + describe("when window.opener has no swaggerUIRedirectOauth2", () => { + it("renders an explanatory message and does not throw", () => { + setOpener({}) + + expect(() => runRedirectScript()).not.toThrow() + expect(document.body.innerText).toMatch(/OAuth redirect cannot complete/) + expect(closeSpy).not.toHaveBeenCalled() + }) + }) + + describe("when window.opener.swaggerUIRedirectOauth2 is present", () => { + it("relays the auth code via callback for authorization_code flow", () => { + const callback = jest.fn() + const errCb = jest.fn() + const oauth2 = { + state: "abc", + redirectUrl: "https://example.org/callback", + auth: { + name: "OAuth2", + schema: { + get: (key) => (key === "flow" ? "authorization_code" : null), + }, + }, + callback, + errCb, + } + setOpener({ swaggerUIRedirectOauth2: oauth2 }) + setLocation("?code=auth-code-123&state=abc", "") + + runRedirectScript() + + expect(callback).toHaveBeenCalledTimes(1) + expect(callback.mock.calls[0][0]).toEqual({ + auth: expect.objectContaining({ code: "auth-code-123" }), + redirectUrl: "https://example.org/callback", + }) + expect(errCb).not.toHaveBeenCalled() + expect(closeSpy).toHaveBeenCalledTimes(1) + }) + }) +})