1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-21 10:43:35 +00:00

[PM-17751] Store SSO email in state on web client (#13295)

* Moved saving of SSO email outside of browser/desktop code

* Clarified comments.

* Tests

* Refactored login component services to manage state

* Fixed input on login component

* Fixed tests

* Linting

* Moved web setting in state into web override

* updated tests

* Fixed typing.

* Fixed type safety issues.

* Added comments and renamed for clarity.

* Removed method parameters that weren't used

* Added clarifying comments

* Added more comments.

* Removed test that is not necessary on base

* Test cleanup

* More comments.

* Linting

* Fixed test.

* Fixed base URL

* Fixed typechecking.

* Type checking

* Moved setting of email state to default service

* Added comments.

* Consolidated SSO URL formatting

* Updated comment

* Fixed reference.

* Fixed missing parameter.

* Initialized service.

* Added comments

* Added initialization of new service

* Made email optional due to CLI.

* Fixed comment on handleSsoClick.

* Added SSO email persistence to v1 component.

---------

Co-authored-by: Bernd Schoolmann <mail@quexten.com>
This commit is contained in:
Todd Martin
2025-02-21 17:09:50 -05:00
committed by GitHub
parent 9dd2033081
commit 077e0f89cc
26 changed files with 534 additions and 245 deletions

View File

@@ -1,11 +1,18 @@
import { TestBed } from "@angular/core/testing";
import { MockProxy, mock } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
import { DefaultLoginComponentService } from "@bitwarden/auth/angular";
import { SsoUrlService } from "@bitwarden/auth/common";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { ClientType } from "@bitwarden/common/enums";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import {
Environment,
EnvironmentService,
} from "@bitwarden/common/platform/abstractions/environment.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { BrowserPlatformUtilsService } from "../../../platform/services/platform-utils/browser-platform-utils.service";
@@ -18,6 +25,7 @@ jest.mock("../../../platform/flags", () => ({
}));
describe("ExtensionLoginComponentService", () => {
const baseUrl = "https://webvault.bitwarden.com";
let service: ExtensionLoginComponentService;
let cryptoFunctionService: MockProxy<CryptoFunctionService>;
let environmentService: MockProxy<EnvironmentService>;
@@ -25,13 +33,20 @@ describe("ExtensionLoginComponentService", () => {
let platformUtilsService: MockProxy<BrowserPlatformUtilsService>;
let ssoLoginService: MockProxy<SsoLoginServiceAbstraction>;
let extensionAnonLayoutWrapperDataService: MockProxy<ExtensionAnonLayoutWrapperDataService>;
let ssoUrlService: MockProxy<SsoUrlService>;
beforeEach(() => {
cryptoFunctionService = mock<CryptoFunctionService>();
environmentService = mock<EnvironmentService>();
passwordGenerationService = mock<PasswordGenerationServiceAbstraction>();
platformUtilsService = mock<BrowserPlatformUtilsService>();
ssoLoginService = mock<SsoLoginServiceAbstraction>();
ssoUrlService = mock<SsoUrlService>();
extensionAnonLayoutWrapperDataService = mock<ExtensionAnonLayoutWrapperDataService>();
environmentService.environment$ = new BehaviorSubject<Environment>({
getWebVaultUrl: () => baseUrl,
} as Environment);
platformUtilsService.getClientType.mockReturnValue(ClientType.Browser);
TestBed.configureTestingModule({
providers: [
{
@@ -44,6 +59,7 @@ describe("ExtensionLoginComponentService", () => {
platformUtilsService,
ssoLoginService,
extensionAnonLayoutWrapperDataService,
ssoUrlService,
),
},
{ provide: DefaultLoginComponentService, useExisting: ExtensionLoginComponentService },
@@ -52,6 +68,11 @@ describe("ExtensionLoginComponentService", () => {
{ provide: PasswordGenerationServiceAbstraction, useValue: passwordGenerationService },
{ provide: PlatformUtilsService, useValue: platformUtilsService },
{ provide: SsoLoginServiceAbstraction, useValue: ssoLoginService },
{
provide: ExtensionAnonLayoutWrapperDataService,
useValue: extensionAnonLayoutWrapperDataService,
},
{ provide: SsoUrlService, useValue: ssoUrlService },
],
});
service = TestBed.inject(ExtensionLoginComponentService);
@@ -61,6 +82,26 @@ describe("ExtensionLoginComponentService", () => {
expect(service).toBeTruthy();
});
describe("redirectToSso", () => {
it("launches SSO browser window", async () => {
const email = "test@bitwarden.com";
const state = "testState";
const expectedState = "testState:clientId=browser";
const codeVerifier = "testCodeVerifier";
const codeChallenge = "testCodeChallenge";
passwordGenerationService.generatePassword.mockResolvedValueOnce(state);
passwordGenerationService.generatePassword.mockResolvedValueOnce(codeVerifier);
jest.spyOn(Utils, "fromBufferToUrlB64").mockReturnValue(codeChallenge);
await service.redirectToSsoLogin(email);
expect(ssoLoginService.setSsoState).toHaveBeenCalledWith(expectedState);
expect(ssoLoginService.setCodeVerifier).toHaveBeenCalledWith(codeVerifier);
expect(platformUtilsService.launchUri).toHaveBeenCalled();
});
});
describe("showBackButton", () => {
it("sets showBackButton in extensionAnonLayoutWrapperDataService", () => {
service.showBackButton(true);

View File

@@ -1,8 +1,10 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Injectable } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { DefaultLoginComponentService, LoginComponentService } from "@bitwarden/auth/angular";
import { SsoUrlService } from "@bitwarden/auth/common";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
@@ -23,6 +25,7 @@ export class ExtensionLoginComponentService
platformUtilsService: PlatformUtilsService,
ssoLoginService: SsoLoginServiceAbstraction,
private extensionAnonLayoutWrapperDataService: ExtensionAnonLayoutWrapperDataService,
private ssoUrlService: SsoUrlService,
) {
super(
cryptoFunctionService,
@@ -31,7 +34,35 @@ export class ExtensionLoginComponentService
platformUtilsService,
ssoLoginService,
);
this.clientType = this.platformUtilsService.getClientType();
}
/**
* On the extension, redirecting to the SSO login page is done via a new browser window, opened
* to the SSO component on the web client.
* @param email the email of the user trying to log in, used to look up the org SSO identifier
* @param state the state that will be used to verify the SSO login, which needs to be passed to the IdP
* @param codeChallenge the challenge that will be verified after the code is returned from the IdP, which needs to be passed to the IdP
*/
protected override async redirectToSso(
email: string,
state: string,
codeChallenge: string,
): Promise<void> {
const env = await firstValueFrom(this.environmentService.environment$);
const webVaultUrl = env.getWebVaultUrl();
const redirectUri = webVaultUrl + "/sso-connector.html";
const webAppSsoUrl = this.ssoUrlService.buildSsoUrl(
webVaultUrl,
this.clientType,
redirectUri,
state,
codeChallenge,
email,
);
this.platformUtilsService.launchUri(webAppSsoUrl);
}
showBackButton(showBackButton: boolean): void {

View File

@@ -27,7 +27,12 @@ import {
LoginDecryptionOptionsService,
SsoComponentService,
} from "@bitwarden/auth/angular";
import { LockService, LoginEmailService, PinServiceAbstraction } from "@bitwarden/auth/common";
import {
LockService,
LoginEmailService,
PinServiceAbstraction,
SsoUrlService,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
@@ -550,6 +555,11 @@ const safeProviders: SafeProvider[] = [
useExisting: ExtensionAnonLayoutWrapperDataService,
deps: [],
}),
safeProvider({
provide: SsoUrlService,
useClass: SsoUrlService,
deps: [],
}),
safeProvider({
provide: LoginComponentService,
useClass: ExtensionLoginComponentService,
@@ -560,6 +570,7 @@ const safeProviders: SafeProvider[] = [
PlatformUtilsService,
SsoLoginServiceAbstraction,
ExtensionAnonLayoutWrapperDataService,
SsoUrlService,
],
}),
safeProvider({