1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

feat(auth): [PM-8978] migrate SSO connector to Tailwind

- Convert Bootstrap styles to Tailwind
- Remove deprecated sso.scss
- Add test coverage for SSO connector

[PM-8978]
This commit is contained in:
Alec Rippberger
2025-04-28 10:14:29 -05:00
committed by GitHub
parent dff58de619
commit c2c31e54c1
5 changed files with 132 additions and 20 deletions

View File

@@ -15,18 +15,16 @@
</head>
<body class="layout_frontend">
<div class="mt-5 d-flex justify-content-center">
<div>
<img src="../images/logo-dark@2x.png" class="mb-4 logo" alt="Bitwarden" />
<div id="content">
<p class="text-center">
<i
class="bwi bwi-spinner bwi-spin bwi-2x text-muted"
title="Loading"
aria-hidden="true"
></i>
</p>
</div>
<div class="tw-p-8 tw-flex tw-flex-col tw-items-center">
<img class="new-logo-themed tw-mb-4" alt="Bitwarden" />
<div id="content">
<p class="tw-text-center">
<i
class="bwi bwi-spinner bwi-spin bwi-2x tw-text-muted"
title="Loading"
aria-hidden="true"
></i>
</p>
</div>
</div>
</body>

View File

@@ -1 +0,0 @@
@import "../scss/styles.scss";

View File

@@ -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,
);
});
});
});

View File

@@ -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=")

View File

@@ -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",