1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-17 08:43:33 +00:00

feat(sso): [PM-8114] implement SSO component UI refresh

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 <116684653+JaredSnider-Bitwarden@users.noreply.github.com>
Co-authored-by: Jared Snider <jsnider@bitwarden.com>
This commit is contained in:
Alec Rippberger
2024-12-12 10:28:30 -06:00
committed by GitHub
parent bfa9cf3623
commit 0df7b53bb4
33 changed files with 1005 additions and 45 deletions

View File

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

View File

@@ -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`;
}
}

View File

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

View File

@@ -32,6 +32,7 @@ import {
LoginComponentService,
LockComponentService,
SetPasswordJitService,
SsoComponentService,
LoginDecryptionOptionsService,
} from "@bitwarden/auth/angular";
import {
@@ -101,6 +102,7 @@ import {
WebLockComponentService,
WebLoginDecryptionOptionsService,
} from "../auth";
import { WebSsoComponentService } from "../auth/core/services/login/web-sso-component.service";
import { AcceptOrganizationInviteService } from "../auth/organization-invite/accept-organization.service";
import { HtmlStorageService } from "../core/html-storage.service";
import { I18nService } from "../core/i18n.service";
@@ -301,6 +303,11 @@ const safeProviders: SafeProvider[] = [
useClass: LoginEmailService,
deps: [AccountService, AuthService, StateProvider],
}),
safeProvider({
provide: SsoComponentService,
useClass: WebSsoComponentService,
deps: [I18nServiceAbstraction],
}),
safeProvider({
provide: LoginDecryptionOptionsService,
useClass: WebLoginDecryptionOptionsService,

View File

@@ -29,11 +29,13 @@ import {
LockIcon,
TwoFactorTimeoutIcon,
UserLockIcon,
SsoKeyIcon,
LoginViaAuthRequestComponent,
DevicesIcon,
RegistrationUserAddIcon,
RegistrationLockAltIcon,
RegistrationExpiredLinkIcon,
SsoComponent,
VaultIcon,
LoginDecryptionOptionsComponent,
} from "@bitwarden/auth/angular";
@@ -62,7 +64,7 @@ import { AccountComponent } from "./auth/settings/account/account.component";
import { EmergencyAccessComponent } from "./auth/settings/emergency-access/emergency-access.component";
import { EmergencyAccessViewComponent } from "./auth/settings/emergency-access/view/emergency-access-view.component";
import { SecurityRoutingModule } from "./auth/settings/security/security-routing.module";
import { SsoComponent } from "./auth/sso.component";
import { SsoComponentV1 } from "./auth/sso-v1.component";
import { CompleteTrialInitiationComponent } from "./auth/trial-initiation/complete-trial-initiation/complete-trial-initiation.component";
import { freeTrialTextResolver } from "./auth/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver";
import { TrialInitiationComponent } from "./auth/trial-initiation/trial-initiation.component";
@@ -430,27 +432,57 @@ const routes: Routes = [
},
],
},
{
path: "sso",
canActivate: [unauthGuardFn()],
data: {
pageTitle: {
key: "enterpriseSingleSignOn",
},
titleId: "enterpriseSingleSignOn",
} satisfies RouteDataProperties & AnonLayoutWrapperData,
children: [
{
path: "",
component: SsoComponent,
},
{
path: "",
component: EnvironmentSelectorComponent,
outlet: "environment-selector",
},
],
},
...unauthUiRefreshSwap(
SsoComponentV1,
SsoComponent,
{
path: "sso",
canActivate: [unauthGuardFn()],
data: {
pageTitle: {
key: "enterpriseSingleSignOn",
},
titleId: "enterpriseSingleSignOn",
} satisfies RouteDataProperties & AnonLayoutWrapperData,
children: [
{
path: "",
component: SsoComponentV1,
},
{
path: "",
component: EnvironmentSelectorComponent,
outlet: "environment-selector",
},
],
},
{
path: "sso",
canActivate: [unauthGuardFn()],
data: {
pageTitle: {
key: "singleSignOn",
},
titleId: "enterpriseSingleSignOn",
pageSubtitle: {
key: "singleSignOnEnterOrgIdentifierText",
},
titleAreaMaxWidth: "md",
pageIcon: SsoKeyIcon,
} satisfies RouteDataProperties & AnonLayoutWrapperData,
children: [
{
path: "",
component: SsoComponent,
},
{
path: "",
component: EnvironmentSelectorComponent,
outlet: "environment-selector",
},
],
},
),
{
path: "login",
canActivate: [unauthGuardFn()],

View File

@@ -50,7 +50,7 @@ import { TwoFactorSetupYubiKeyComponent } from "../auth/settings/two-factor/two-
import { TwoFactorSetupComponent } from "../auth/settings/two-factor/two-factor-setup.component";
import { TwoFactorVerifyComponent } from "../auth/settings/two-factor/two-factor-verify.component";
import { UserVerificationModule } from "../auth/shared/components/user-verification";
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 { UpdatePasswordComponent } from "../auth/update-password.component";
@@ -158,7 +158,7 @@ import { SharedModule } from "./shared.module";
SetPasswordComponent,
SponsoredFamiliesComponent,
SponsoringOrgRowComponent,
SsoComponent,
SsoComponentV1,
TwoFactorSetupAuthenticatorComponent,
TwoFactorComponent,
TwoFactorSetupDuoComponent,
@@ -225,7 +225,7 @@ import { SharedModule } from "./shared.module";
SetPasswordComponent,
SponsoredFamiliesComponent,
SponsoringOrgRowComponent,
SsoComponent,
SsoComponentV1,
TwoFactorSetupAuthenticatorComponent,
TwoFactorComponent,
TwoFactorSetupDuoComponent,

View File

@@ -4739,6 +4739,12 @@
"ssoLogInWithOrgIdentifier": {
"message": "Log in using your organization's single sign-on portal. Please enter your organization's SSO identifier to begin."
},
"singleSignOnEnterOrgIdentifier": {
"message": "Enter your organization's SSO identifier to begin"
},
"singleSignOnEnterOrgIdentifierText": {
"message": "To log in with your SSO provider, enter your organization's SSO identifier to begin. You may need to enter this SSO identifier when you log in from a new device."
},
"enterpriseSingleSignOn": {
"message": "Enterprise single sign-on"
},