-

-
+
+
![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",