Skip to content

Commit

Permalink
feat(sso): [PM-8114] implement SSO component UI refresh
Browse files Browse the repository at this point in the history
Consolidates existing SSO components into a single unified component in
libs/auth, matching the new design system. This implementation:

- Creates a new shared SsoComponent with extracted business logic
- Adds feature flag support for unauth-ui-refresh
- Updates page styling including new icons and typography
- Preserves web client claimed domain logic
- Maintains backwards compatibility with legacy views

PM-8114

---------

Co-authored-by: Jared Snider <[email protected]>
Co-authored-by: Jared Snider <[email protected]>
  • Loading branch information
3 people authored Dec 12, 2024
1 parent bfa9cf3 commit 0df7b53
Show file tree
Hide file tree
Showing 33 changed files with 1,005 additions and 45 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { TestBed } from "@angular/core/testing";
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";

import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import {
EnvironmentService,
Environment,
} from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";

import { BrowserApi } from "../../../platform/browser/browser-api";

import { ExtensionSsoComponentService } from "./extension-sso-component.service";

describe("ExtensionSsoComponentService", () => {
let service: ExtensionSsoComponentService;
const baseUrl = "https://vault.bitwarden.com";

let syncService: MockProxy<SyncService>;
let authService: MockProxy<AuthService>;
let environmentService: MockProxy<EnvironmentService>;
let i18nService: MockProxy<I18nService>;
let logService: MockProxy<LogService>;

beforeEach(() => {
syncService = mock<SyncService>();
authService = mock<AuthService>();
environmentService = mock<EnvironmentService>();
i18nService = mock<I18nService>();
logService = mock<LogService>();
environmentService.environment$ = new BehaviorSubject<Environment>({
getWebVaultUrl: () => baseUrl,
} as Environment);

TestBed.configureTestingModule({
providers: [
{ provide: SyncService, useValue: syncService },
{ provide: AuthService, useValue: authService },
{ provide: EnvironmentService, useValue: environmentService },
{ provide: I18nService, useValue: i18nService },
{ provide: LogService, useValue: logService },
ExtensionSsoComponentService,
],
});

service = TestBed.inject(ExtensionSsoComponentService);

jest.spyOn(BrowserApi, "reloadOpenWindows").mockImplementation();
});

it("creates the service", () => {
expect(service).toBeTruthy();
});

describe("closeWindow", () => {
it("closes window", async () => {
const windowSpy = jest.spyOn(window, "close").mockImplementation();

await service.closeWindow?.();

expect(windowSpy).toHaveBeenCalled();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Injectable } from "@angular/core";

import { DefaultSsoComponentService, SsoComponentService } from "@bitwarden/auth/angular";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";

/**
* This service is used to handle the SSO login process for the browser extension.
*/
@Injectable()
export class ExtensionSsoComponentService
extends DefaultSsoComponentService
implements SsoComponentService
{
constructor(
protected syncService: SyncService,
protected authService: AuthService,
protected environmentService: EnvironmentService,
protected i18nService: I18nService,
protected logService: LogService,
) {
super();
}

/**
* Closes the popup window after a successful login.
*/
async closeWindow() {
window.close();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ import { BrowserApi } from "../../platform/browser/browser-api";

@Component({
selector: "app-sso",
templateUrl: "sso.component.html",
templateUrl: "sso-v1.component.html",
})
export class SsoComponent extends BaseSsoComponent {
export class SsoComponentV1 extends BaseSsoComponent {
constructor(
ssoLoginService: SsoLoginServiceAbstraction,
loginStrategyService: LoginStrategyServiceAbstraction,
Expand Down
43 changes: 36 additions & 7 deletions apps/browser/src/popup/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
VaultIcon,
LoginDecryptionOptionsComponent,
DevicesIcon,
SsoComponent,
TwoFactorTimeoutIcon,
} from "@bitwarden/auth/angular";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
Expand All @@ -62,7 +63,7 @@ import { RemovePasswordComponent } from "../auth/popup/remove-password.component
import { SetPasswordComponent } from "../auth/popup/set-password.component";
import { AccountSecurityComponent as AccountSecurityV1Component } from "../auth/popup/settings/account-security-v1.component";
import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
import { SsoComponent } from "../auth/popup/sso.component";
import { SsoComponentV1 } from "../auth/popup/sso-v1.component";
import { TwoFactorAuthComponent } from "../auth/popup/two-factor-auth.component";
import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component";
import { TwoFactorComponent } from "../auth/popup/two-factor.component";
Expand Down Expand Up @@ -230,12 +231,40 @@ const routes: Routes = [
canActivate: [unauthGuardFn(unauthRouteOverrides)],
data: { elevation: 1 } satisfies RouteDataProperties,
},
{
path: "sso",
component: SsoComponent,
canActivate: [unauthGuardFn(unauthRouteOverrides)],
data: { elevation: 1 } satisfies RouteDataProperties,
},
...unauthUiRefreshSwap(
SsoComponentV1,
ExtensionAnonLayoutWrapperComponent,
{
path: "sso",
canActivate: [unauthGuardFn(unauthRouteOverrides)],
data: { elevation: 1 } satisfies RouteDataProperties,
},
{
path: "sso",
canActivate: [unauthGuardFn(unauthRouteOverrides)],
data: {
pageIcon: VaultIcon,
pageTitle: {
key: "enterpriseSingleSignOn",
},
pageSubtitle: {
key: "singleSignOnEnterOrgIdentifierText",
},
elevation: 1,
} satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData,
children: [
{ path: "", component: SsoComponent },
{
path: "",
component: EnvironmentSelectorComponent,
outlet: "environment-selector",
data: {
overlayPosition: ExtensionDefaultOverlayPosition,
} satisfies EnvironmentSelectorRouteData,
},
],
},
),
{
path: "set-password",
component: SetPasswordComponent,
Expand Down
4 changes: 2 additions & 2 deletions apps/browser/src/popup/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { SetPasswordComponent } from "../auth/popup/set-password.component";
import { AccountSecurityComponent as AccountSecurityComponentV1 } from "../auth/popup/settings/account-security-v1.component";
import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
import { VaultTimeoutInputComponent } from "../auth/popup/settings/vault-timeout-input.component";
import { SsoComponent } from "../auth/popup/sso.component";
import { SsoComponentV1 } from "../auth/popup/sso-v1.component";
import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component";
import { TwoFactorComponent } from "../auth/popup/two-factor.component";
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
Expand Down Expand Up @@ -177,7 +177,7 @@ import "../platform/popup/locales";
SettingsComponent,
VaultSettingsComponent,
ShareComponent,
SsoComponent,
SsoComponentV1,
SyncComponent,
TabsComponent,
TabsV2Component,
Expand Down
7 changes: 7 additions & 0 deletions apps/browser/src/popup/services/services.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
AnonLayoutWrapperDataService,
LoginComponentService,
LockComponentService,
SsoComponentService,
LoginDecryptionOptionsService,
} from "@bitwarden/auth/angular";
import { LockService, LoginEmailService, PinServiceAbstraction } from "@bitwarden/auth/common";
Expand Down Expand Up @@ -119,6 +120,7 @@ import { PasswordRepromptService } from "@bitwarden/vault";
import { ForegroundLockService } from "../../auth/popup/accounts/foreground-lock.service";
import { ExtensionAnonLayoutWrapperDataService } from "../../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service";
import { ExtensionLoginComponentService } from "../../auth/popup/login/extension-login-component.service";
import { ExtensionSsoComponentService } from "../../auth/popup/login/extension-sso-component.service";
import { ExtensionLoginDecryptionOptionsService } from "../../auth/popup/login-decryption-options/extension-login-decryption-options.service";
import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service";
import AutofillService from "../../autofill/services/autofill.service";
Expand Down Expand Up @@ -597,6 +599,11 @@ const safeProviders: SafeProvider[] = [
useExisting: PopupCompactModeService,
deps: [],
}),
safeProvider({
provide: SsoComponentService,
useClass: ExtensionSsoComponentService,
deps: [SyncService, AuthService, EnvironmentService, I18nServiceAbstraction, LogService],
}),
safeProvider({
provide: LoginDecryptionOptionsService,
useClass: ExtensionLoginDecryptionOptionsService,
Expand Down
31 changes: 29 additions & 2 deletions apps/desktop/src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
VaultIcon,
LoginDecryptionOptionsComponent,
DevicesIcon,
SsoComponent,
TwoFactorTimeoutIcon,
} from "@bitwarden/auth/angular";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
Expand All @@ -51,7 +52,7 @@ import { LoginViaAuthRequestComponentV1 } from "../auth/login/login-via-auth-req
import { RegisterComponent } from "../auth/register.component";
import { RemovePasswordComponent } from "../auth/remove-password.component";
import { SetPasswordComponent } from "../auth/set-password.component";
import { SsoComponent } from "../auth/sso.component";
import { SsoComponentV1 } from "../auth/sso-v1.component";
import { TwoFactorAuthComponent } from "../auth/two-factor-auth.component";
import { TwoFactorComponent } from "../auth/two-factor.component";
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component";
Expand Down Expand Up @@ -122,7 +123,33 @@ const routes: Routes = [
},
{ path: "accessibility-cookie", component: AccessibilityCookieComponent },
{ path: "set-password", component: SetPasswordComponent },
{ path: "sso", component: SsoComponent },
...unauthUiRefreshSwap(
SsoComponentV1,
AnonLayoutWrapperComponent,
{
path: "sso",
},
{
path: "sso",
data: {
pageIcon: VaultIcon,
pageTitle: {
key: "enterpriseSingleSignOn",
},
pageSubtitle: {
key: "singleSignOnEnterOrgIdentifierText",
},
} satisfies AnonLayoutWrapperData,
children: [
{ path: "", component: SsoComponent },
{
path: "",
component: EnvironmentSelectorComponent,
outlet: "environment-selector",
},
],
},
),
{
path: "send",
component: SendComponent,
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { LoginModule } from "../auth/login/login.module";
import { RegisterComponent } from "../auth/register.component";
import { RemovePasswordComponent } from "../auth/remove-password.component";
import { SetPasswordComponent } from "../auth/set-password.component";
import { SsoComponent } from "../auth/sso.component";
import { SsoComponentV1 } from "../auth/sso-v1.component";
import { TwoFactorOptionsComponent } from "../auth/two-factor-options.component";
import { TwoFactorComponent } from "../auth/two-factor.component";
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component";
Expand Down Expand Up @@ -92,7 +92,7 @@ import { SendComponent } from "./tools/send/send.component";
SetPasswordComponent,
SettingsComponent,
ShareComponent,
SsoComponent,
SsoComponentV1,
TwoFactorComponent,
TwoFactorOptionsComponent,
UpdateTempPasswordComponent,
Expand Down
7 changes: 7 additions & 0 deletions apps/desktop/src/app/services/services.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import {
LoginComponentService,
SetPasswordJitService,
LockComponentService,
SsoComponentService,
DefaultSsoComponentService,
} from "@bitwarden/auth/angular";
import {
InternalUserDecryptionOptionsServiceAbstraction,
Expand Down Expand Up @@ -361,6 +363,11 @@ const safeProviders: SafeProvider[] = [
useClass: LoginEmailService,
deps: [AccountService, AuthService, StateProvider],
}),
safeProvider({
provide: SsoComponentService,
useClass: DefaultSsoComponentService,
deps: [],
}),
safeProvider({
provide: LoginApprovalComponentServiceAbstraction,
useClass: DesktopLoginApprovalComponentService,
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legac

@Component({
selector: "app-sso",
templateUrl: "sso.component.html",
templateUrl: "sso-v1.component.html",
})
export class SsoComponent extends BaseSsoComponent {
export class SsoComponentV1 extends BaseSsoComponent {
constructor(
ssoLoginService: SsoLoginServiceAbstraction,
loginStrategyService: LoginStrategyServiceAbstraction,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { TestBed } from "@angular/core/testing";
import { mock, MockProxy } from "jest-mock-extended";

import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";

import { WebSsoComponentService } from "./web-sso-component.service";

describe("WebSsoComponentService", () => {
let service: WebSsoComponentService;
let i18nService: MockProxy<I18nService>;

beforeEach(() => {
i18nService = mock<I18nService>();

TestBed.configureTestingModule({
providers: [WebSsoComponentService, { provide: I18nService, useValue: i18nService }],
});
service = TestBed.inject(WebSsoComponentService);
});

it("creates the service", () => {
expect(service).toBeTruthy();
});

describe("setDocumentCookies", () => {
it("sets ssoHandOffMessage cookie with translated message", () => {
const mockMessage = "Test SSO Message";
i18nService.t.mockReturnValue(mockMessage);

service.setDocumentCookies?.();

expect(document.cookie).toContain(`ssoHandOffMessage=${mockMessage}`);
expect(i18nService.t).toHaveBeenCalledWith("ssoHandOff");
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Injectable } from "@angular/core";

import { DefaultSsoComponentService, SsoComponentService } from "@bitwarden/auth/angular";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";

/**
* This service is used to handle the SSO login process for the web client.
*/
@Injectable()
export class WebSsoComponentService
extends DefaultSsoComponentService
implements SsoComponentService
{
constructor(private i18nService: I18nService) {
super();
}

setDocumentCookies() {
document.cookie = `ssoHandOffMessage=${this.i18nService.t("ssoHandOff")};SameSite=strict`;
}
}
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legac

@Component({
selector: "app-sso",
templateUrl: "sso.component.html",
templateUrl: "sso-v1.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class SsoComponent extends BaseSsoComponent implements OnInit {
export class SsoComponentV1 extends BaseSsoComponent implements OnInit {
protected formGroup = new FormGroup({
identifier: new FormControl(null, [Validators.required]),
});
Expand Down
Loading

0 comments on commit 0df7b53

Please sign in to comment.