diff --git a/apps/web/src/connectors/sso.html b/apps/web/src/connectors/sso.html index 77996173cd7..55088c9f812 100644 --- a/apps/web/src/connectors/sso.html +++ b/apps/web/src/connectors/sso.html @@ -15,18 +15,16 @@ -
-
- -
-

- -

-
+
+ Bitwarden +
+

+ +

diff --git a/apps/web/src/connectors/sso.scss b/apps/web/src/connectors/sso.scss deleted file mode 100644 index a4c7f9b25b7..00000000000 --- a/apps/web/src/connectors/sso.scss +++ /dev/null @@ -1 +0,0 @@ -@import "../scss/styles.scss"; diff --git a/apps/web/src/connectors/sso.spec.ts b/apps/web/src/connectors/sso.spec.ts new file mode 100644 index 00000000000..45a41d94171 --- /dev/null +++ b/apps/web/src/connectors/sso.spec.ts @@ -0,0 +1,119 @@ +import { initiateWebAppSso, initiateBrowserSso } from "./sso"; + +describe("sso", () => { + let originalLocation: Location; + let originalPostMessage: any; + let postMessageSpy: jest.SpyInstance; + + beforeEach(() => { + // Save original window methods + originalLocation = window.location; + originalPostMessage = window.postMessage; + + // Mock location + Object.defineProperty(window, "location", { + value: { + href: "", + origin: "https://test.bitwarden.com", + }, + writable: true, + }); + + // Mock postMessage + postMessageSpy = jest.spyOn(window, "postMessage"); + + // Set up document + document.cookie = "ssoHandOffMessage=SSO login successful;SameSite=strict"; + const contentElement = document.createElement("div"); + contentElement.id = "content"; + document.body.appendChild(contentElement); + }); + + afterEach(() => { + // Restore original window methods + Object.defineProperty(window, "location", { value: originalLocation }); + window.postMessage = originalPostMessage; + + // Clean up document + const contentElement = document.getElementById("content"); + if (contentElement) { + document.body.removeChild(contentElement); + } + document.cookie = "ssoHandOffMessage=;SameSite=strict;max-age=0"; + + // Clear mocks + jest.clearAllMocks(); + }); + + describe("initiateWebAppSso", () => { + it("redirects to the SSO component with code and state", () => { + const code = "testcode"; + const state = "teststate"; + + initiateWebAppSso(code, state); + + expect(window.location.href).toBe( + "https://test.bitwarden.com/#/sso?code=testcode&state=teststate", + ); + }); + + it("redirects to the return URI when included in state", () => { + const code = "testcode"; + const state = "teststate_returnUri='/organizations'"; + + initiateWebAppSso(code, state); + + expect(window.location.href).toBe("https://test.bitwarden.com/#/organizations"); + }); + + it("handles empty code parameter", () => { + initiateWebAppSso("", "teststate"); + expect(window.location.href).toBe("https://test.bitwarden.com/#/sso?code=&state=teststate"); + }); + + it("handles empty state parameter", () => { + initiateWebAppSso("testcode", ""); + expect(window.location.href).toBe("https://test.bitwarden.com/#/sso?code=testcode&state="); + }); + }); + + describe("initiateBrowserSso", () => { + it("posts message with code and state", () => { + const code = "testcode"; + const state = "teststate"; + const lastpass = false; + + initiateBrowserSso(code, state, lastpass); + + expect(postMessageSpy).toHaveBeenCalledWith( + { command: "authResult", code, state, lastpass }, + window.location.origin, + ); + }); + + it("updates content with message from cookie", () => { + const code = "testcode"; + const state = "teststate"; + const lastpass = false; + + initiateBrowserSso(code, state, lastpass); + + const contentElement = document.getElementById("content"); + const paragraphElement = contentElement?.querySelector("p"); + expect(paragraphElement?.innerText).toBe("SSO login successful"); + }); + + it("handles lastpass flag correctly", () => { + const code = "testcode"; + const state = "teststate"; + const lastpass = true; + + initiateBrowserSso(code, state, lastpass); + + expect(postMessageSpy).toHaveBeenCalledWith( + { command: "authResult", code, state, lastpass }, + window.location.origin, + ); + }); + }); +}); diff --git a/apps/web/src/connectors/sso.ts b/apps/web/src/connectors/sso.ts index 886742c4c49..55d661b35e8 100644 --- a/apps/web/src/connectors/sso.ts +++ b/apps/web/src/connectors/sso.ts @@ -2,10 +2,6 @@ // @ts-strict-ignore import { getQsParam } from "./common"; -// FIXME: Remove when updating file. Eslint update -// eslint-disable-next-line @typescript-eslint/no-require-imports -require("./sso.scss"); - window.addEventListener("load", () => { const code = getQsParam("code"); const state = getQsParam("state"); @@ -20,7 +16,7 @@ window.addEventListener("load", () => { } }); -function initiateWebAppSso(code: string, state: string) { +export function initiateWebAppSso(code: string, state: string) { // If we've initiated SSO from somewhere other than the SSO component on the web app, the SSO component will add // a _returnUri to the state variable. Here we're extracting that URI and sending the user there instead of to the SSO component. const returnUri = extractFromRegex(state, "(?<=_returnUri=')(.*)(?=')"); @@ -31,7 +27,7 @@ function initiateWebAppSso(code: string, state: string) { } } -function initiateBrowserSso(code: string, state: string, lastpass: boolean) { +export function initiateBrowserSso(code: string, state: string, lastpass: boolean) { window.postMessage({ command: "authResult", code, state, lastpass }, window.location.origin); const handOffMessage = ("; " + document.cookie) .split("; ssoHandOffMessage=") diff --git a/apps/web/webpack.config.js b/apps/web/webpack.config.js index 9ccccee21bf..a4ac3322200 100644 --- a/apps/web/webpack.config.js +++ b/apps/web/webpack.config.js @@ -122,7 +122,7 @@ const plugins = [ new HtmlWebpackPlugin({ template: "./src/connectors/sso.html", filename: "sso-connector.html", - chunks: ["connectors/sso"], + chunks: ["connectors/sso", "styles"], }), new HtmlWebpackPlugin({ template: "./src/connectors/redirect.html",