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

refactor(auth): [PM-8976] migrate two-factor setup component to Tailwind and standalone

- Remove Bootstrap styles from two-factor-setup component and replace with Tailwind equivalents
- Convert two factor components to standalone components to move away from LooseComponents
- Replace ul/li list with bit-item-group and bit-item components
- Integrate with the bit design system

---------

Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>
Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com>
This commit is contained in:
Alec Rippberger
2025-04-10 14:13:11 -05:00
committed by GitHub
parent f24a4d139d
commit 4772362928
26 changed files with 374 additions and 194 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -21,11 +21,3 @@
max-width: 100px;
}
}
.recovery-code-img {
@include themify($themes) {
content: url("../images/two-factor/rc" + themed("mfaLogoSuffix"));
max-width: 100px;
max-height: 45px;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -21,11 +21,3 @@
max-width: 100px;
}
}
.recovery-code-img {
@include themify($themes) {
content: url("../images/two-factor/rc" + themed("mfaLogoSuffix"));
max-width: 100px;
max-height: 45px;
}
}

View File

@@ -1,5 +1,7 @@
import { NgModule } from "@angular/core";
import { ItemModule } from "@bitwarden/components";
import { LooseComponentsModule, SharedModule } from "../../../shared";
import { AccountFingerprintComponent } from "../../../shared/components/account-fingerprint/account-fingerprint.component";
import { PoliciesModule } from "../../organizations/policies";
@@ -15,6 +17,7 @@ import { TwoFactorSetupComponent } from "./two-factor-setup.component";
PoliciesModule,
OrganizationSettingsRoutingModule,
AccountFingerprintComponent,
ItemModule,
],
declarations: [AccountComponent, TwoFactorSetupComponent],
})

View File

@@ -1,36 +1,50 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CommonModule } from "@angular/common";
import { Component, Inject } from "@angular/core";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { TwoFactorRecoverResponse } from "@bitwarden/common/auth/models/response/two-factor-recover.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { DIALOG_DATA, DialogConfig, DialogService } from "@bitwarden/components";
import {
ButtonModule,
DIALOG_DATA,
DialogConfig,
DialogModule,
DialogRef,
DialogService,
TypographyModule,
} from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
@Component({
selector: "app-two-factor-recovery",
templateUrl: "two-factor-recovery.component.html",
standalone: true,
imports: [CommonModule, DialogModule, ButtonModule, TypographyModule, I18nPipe],
})
export class TwoFactorRecoveryComponent {
type = -1;
code: string;
authed: boolean;
code: string = "";
authed: boolean = false;
twoFactorProviderType = TwoFactorProviderType;
constructor(
@Inject(DIALOG_DATA) protected data: any,
@Inject(DIALOG_DATA) protected data: { response: { response: TwoFactorRecoverResponse } },
private i18nService: I18nService,
) {
this.auth(data.response);
}
auth(authResponse: any) {
auth(authResponse: { response: TwoFactorRecoverResponse }) {
this.authed = true;
this.processResponse(authResponse.response);
}
print() {
const w = window.open();
if (!w) {
// return early if the window is not open
return;
}
w.document.write(
'<div style="font-size: 18px; text-align: center;">' +
"<p>" +
@@ -47,9 +61,9 @@ export class TwoFactorRecoveryComponent {
w.print();
}
private formatString(s: string) {
private formatString(s: string): string {
if (s == null) {
return null;
return "";
}
return s
.replace(/(.{4})/g, "$1 ")
@@ -61,7 +75,13 @@ export class TwoFactorRecoveryComponent {
this.code = this.formatString(response.code);
}
static open(dialogService: DialogService, config: DialogConfig<any>) {
static open(
dialogService: DialogService,
config: DialogConfig<
{ response: { response: TwoFactorRecoverResponse } },
DialogRef<unknown, TwoFactorRecoveryComponent>
>,
) {
return dialogService.open(TwoFactorRecoveryComponent, config);
}
}

View File

@@ -41,7 +41,7 @@
{{ "twoStepAuthenticatorInstructionSuffix" | i18n }}
</p>
<p class="text-center">
<p class="tw-text-center">
<a
href="https://apps.apple.com/ca/app/bitwarden-authenticator/id6497335175"
target="_blank"

View File

@@ -1,9 +1,11 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CommonModule } from "@angular/common";
import { Component, EventEmitter, Inject, OnDestroy, OnInit, Output } from "@angular/core";
import { FormBuilder, FormControl, Validators } from "@angular/forms";
import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from "@angular/forms";
import { firstValueFrom, map } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
@@ -18,12 +20,22 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import {
AsyncActionsModule,
ButtonModule,
CalloutModule,
DIALOG_DATA,
DialogConfig,
DialogModule,
DialogRef,
DialogService,
FormFieldModule,
IconModule,
InputModule,
LinkModule,
ToastService,
TypographyModule,
} from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
import { TwoFactorSetupMethodBaseComponent } from "./two-factor-setup-method-base.component";
@@ -44,6 +56,22 @@ declare global {
@Component({
selector: "app-two-factor-setup-authenticator",
templateUrl: "two-factor-setup-authenticator.component.html",
standalone: true,
imports: [
CommonModule,
ReactiveFormsModule,
DialogModule,
FormFieldModule,
InputModule,
LinkModule,
TypographyModule,
CalloutModule,
ButtonModule,
IconModule,
I18nPipe,
AsyncActionsModule,
JslibModule,
],
})
export class TwoFactorSetupAuthenticatorComponent
extends TwoFactorSetupMethodBaseComponent

View File

@@ -2,9 +2,9 @@
<bit-dialog [title]="'twoStepLogin' | i18n" [subtitle]="'Duo'">
<ng-container bitDialogContent>
<ng-container *ngIf="enabled">
<app-callout type="success" title="{{ 'enabled' | i18n }}" icon="bwi bwi-check-circle">
<bit-callout type="success" [title]="'enabled' | i18n" icon="bwi bwi-check-circle">
{{ "twoStepLoginProviderEnabled" | i18n }}
</app-callout>
</bit-callout>
<img class="tw-float-right tw-ml-3 mfaType2" alt="Duo logo" />
<strong>{{ "twoFactorDuoClientId" | i18n }}:</strong> {{ clientId }}
<br />

View File

@@ -1,7 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CommonModule } from "@angular/common";
import { Component, EventEmitter, Inject, OnInit, Output } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
@@ -13,18 +12,41 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import {
AsyncActionsModule,
ButtonModule,
CalloutModule,
DIALOG_DATA,
DialogConfig,
DialogModule,
DialogRef,
DialogService,
FormFieldModule,
IconModule,
InputModule,
ToastService,
TypographyModule,
} from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
import { TwoFactorSetupMethodBaseComponent } from "./two-factor-setup-method-base.component";
@Component({
selector: "app-two-factor-setup-duo",
templateUrl: "two-factor-setup-duo.component.html",
standalone: true,
imports: [
CommonModule,
DialogModule,
FormFieldModule,
InputModule,
TypographyModule,
ButtonModule,
IconModule,
I18nPipe,
ReactiveFormsModule,
AsyncActionsModule,
CalloutModule,
],
})
export class TwoFactorSetupDuoComponent
extends TwoFactorSetupMethodBaseComponent
@@ -63,23 +85,23 @@ export class TwoFactorSetupDuoComponent
);
}
get clientId() {
return this.formGroup.get("clientId").value;
get clientId(): string {
return this.formGroup.get("clientId")?.value || "";
}
get clientSecret() {
return this.formGroup.get("clientSecret").value;
get clientSecret(): string {
return this.formGroup.get("clientSecret")?.value || "";
}
get host() {
return this.formGroup.get("host").value;
get host(): string {
return this.formGroup.get("host")?.value || "";
}
set clientId(value: string) {
this.formGroup.get("clientId").setValue(value);
this.formGroup.get("clientId")?.setValue(value);
}
set clientSecret(value: string) {
this.formGroup.get("clientSecret").setValue(value);
this.formGroup.get("clientSecret")?.setValue(value);
}
set host(value: string) {
this.formGroup.get("host").setValue(value);
this.formGroup.get("host")?.setValue(value);
}
async ngOnInit() {
@@ -147,7 +169,10 @@ export class TwoFactorSetupDuoComponent
dialogService: DialogService,
config: DialogConfig<TwoFactorDuoComponentConfig>,
) => {
return dialogService.open<boolean>(TwoFactorSetupDuoComponent, config);
return dialogService.open<boolean, TwoFactorDuoComponentConfig>(
TwoFactorSetupDuoComponent,
config as DialogConfig<TwoFactorDuoComponentConfig, DialogRef<boolean>>,
);
};
}

View File

@@ -1,7 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CommonModule } from "@angular/common";
import { Component, EventEmitter, Inject, OnInit, Output } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
import { firstValueFrom, map } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
@@ -16,19 +15,41 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import {
AsyncActionsModule,
ButtonModule,
CalloutModule,
DIALOG_DATA,
DialogConfig,
DialogModule,
DialogRef,
DialogService,
FormFieldModule,
IconModule,
InputModule,
ToastService,
TypographyModule,
} from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
import { TwoFactorSetupMethodBaseComponent } from "./two-factor-setup-method-base.component";
@Component({
selector: "app-two-factor-setup-email",
templateUrl: "two-factor-setup-email.component.html",
outputs: ["onUpdated"],
standalone: true,
imports: [
AsyncActionsModule,
ButtonModule,
CalloutModule,
CommonModule,
DialogModule,
FormFieldModule,
IconModule,
I18nPipe,
InputModule,
ReactiveFormsModule,
TypographyModule,
],
})
export class TwoFactorSetupEmailComponent
extends TwoFactorSetupMethodBaseComponent
@@ -36,8 +57,8 @@ export class TwoFactorSetupEmailComponent
{
@Output() onChangeStatus: EventEmitter<boolean> = new EventEmitter();
type = TwoFactorProviderType.Email;
sentEmail: string;
emailPromise: Promise<unknown>;
sentEmail: string = "";
emailPromise: Promise<unknown> | undefined;
override componentName = "app-two-factor-email";
formGroup = this.formBuilder.group({
token: ["", [Validators.required]],
@@ -67,17 +88,17 @@ export class TwoFactorSetupEmailComponent
toastService,
);
}
get token() {
return this.formGroup.get("token").value;
get token(): string {
return this.formGroup.get("token")?.value || "";
}
set token(value: string) {
this.formGroup.get("token").setValue(value);
set token(value: string | null) {
this.formGroup.get("token")?.setValue(value || "");
}
get email() {
return this.formGroup.get("email").value;
get email(): string {
return this.formGroup.get("email")?.value || "";
}
set email(value: string) {
this.formGroup.get("email").setValue(value);
set email(value: string | null | undefined) {
this.formGroup.get("email")?.setValue(value || "");
}
async ngOnInit() {
@@ -149,6 +170,9 @@ export class TwoFactorSetupEmailComponent
dialogService: DialogService,
config: DialogConfig<AuthResponse<TwoFactorEmailResponse>>,
) {
return dialogService.open<boolean>(TwoFactorSetupEmailComponent, config);
return dialogService.open<boolean, AuthResponse<TwoFactorEmailResponse>>(
TwoFactorSetupEmailComponent,
config as DialogConfig<AuthResponse<TwoFactorEmailResponse>, DialogRef<boolean>>,
);
}
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive, EventEmitter, Output } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
@@ -17,18 +15,20 @@ import { DialogService, ToastService } from "@bitwarden/components";
/**
* Base class for two-factor setup components (ex: email, yubikey, webauthn, duo).
*/
@Directive()
@Directive({
standalone: true,
})
export abstract class TwoFactorSetupMethodBaseComponent {
@Output() onUpdated = new EventEmitter<boolean>();
type: TwoFactorProviderType;
organizationId: string;
type: TwoFactorProviderType | undefined;
organizationId: string | null = null;
twoFactorProviderType = TwoFactorProviderType;
enabled = false;
authed = false;
protected hashedSecret: string;
protected verificationType: VerificationType;
protected hashedSecret: string | undefined;
protected verificationType: VerificationType | undefined;
protected componentName = "";
constructor(
@@ -74,6 +74,9 @@ export abstract class TwoFactorSetupMethodBaseComponent {
try {
const request = await this.buildRequestModel(TwoFactorProviderRequest);
if (this.type === undefined) {
throw new Error("Two-factor provider type is required");
}
request.type = this.type;
if (this.organizationId != null) {
promise = this.apiService.putTwoFactorOrganizationDisable(this.organizationId, request);
@@ -84,7 +87,7 @@ export abstract class TwoFactorSetupMethodBaseComponent {
this.enabled = false;
this.toastService.showToast({
variant: "success",
title: null,
title: "",
message: this.i18nService.t("twoStepDisabled"),
});
this.onUpdated.emit(false);
@@ -105,6 +108,9 @@ export abstract class TwoFactorSetupMethodBaseComponent {
}
const request = await this.buildRequestModel(TwoFactorProviderRequest);
if (this.type === undefined) {
throw new Error("Two-factor provider type is required");
}
request.type = this.type;
if (this.organizationId != null) {
await this.apiService.putTwoFactorOrganizationDisable(this.organizationId, request);
@@ -114,7 +120,7 @@ export abstract class TwoFactorSetupMethodBaseComponent {
this.enabled = false;
this.toastService.showToast({
variant: "success",
title: null,
title: "",
message: this.i18nService.t("twoStepDisabled"),
});
this.onUpdated.emit(false);
@@ -123,6 +129,9 @@ export abstract class TwoFactorSetupMethodBaseComponent {
protected async buildRequestModel<T extends SecretVerificationRequest>(
requestClass: new () => T,
) {
if (this.hashedSecret === undefined || this.verificationType === undefined) {
throw new Error("User verification data is missing");
}
return this.userVerificationService.buildRequest(
{
secret: this.hashedSecret,

View File

@@ -5,23 +5,23 @@
[subtitle]="'webAuthnTitle' | i18n"
>
<ng-container bitDialogContent>
<app-callout
<bit-callout
type="success"
title="{{ 'enabled' | i18n }}"
icon="bwi bwi-check-circle"
*ngIf="enabled"
>
{{ "twoStepLoginProviderEnabled" | i18n }}
</app-callout>
<app-callout type="warning">
</bit-callout>
<bit-callout type="warning">
<p bitTypography="body1">{{ "twoFactorWebAuthnWarning1" | i18n }}</p>
</app-callout>
</bit-callout>
<img class="tw-float-right tw-ml-5 mfaType7" alt="FIDO2 WebAuthn logo" />
<ul class="bwi-ul">
<li *ngFor="let k of keys; let i = index" #removeKeyBtn [appApiAction]="k.removePromise">
<i class="bwi bwi-li bwi-key"></i>
<span *ngIf="!k.configured || !k.name" bitTypography="body1" class="tw-font-bold">
{{ "webAuthnkeyX" | i18n: i + 1 }}
{{ "webAuthnkeyX" | i18n: (i + 1).toString() }}
</span>
<span *ngIf="k.configured && k.name" bitTypography="body1" class="tw-font-bold">
{{ k.name }}

View File

@@ -1,8 +1,8 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CommonModule } from "@angular/common";
import { Component, Inject, NgZone } from "@angular/core";
import { FormControl, FormGroup } from "@angular/forms";
import { FormControl, FormGroup, ReactiveFormsModule } from "@angular/forms";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
@@ -18,12 +18,20 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import {
AsyncActionsModule,
ButtonModule,
CalloutModule,
DIALOG_DATA,
DialogConfig,
DialogModule,
DialogRef,
DialogService,
FormFieldModule,
LinkModule,
ToastService,
TypographyModule,
} from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
import { TwoFactorSetupMethodBaseComponent } from "./two-factor-setup-method-base.component";
@@ -38,24 +46,36 @@ interface Key {
@Component({
selector: "app-two-factor-setup-webauthn",
templateUrl: "two-factor-setup-webauthn.component.html",
standalone: true,
imports: [
AsyncActionsModule,
ButtonModule,
CalloutModule,
CommonModule,
DialogModule,
FormFieldModule,
I18nPipe,
JslibModule,
LinkModule,
ReactiveFormsModule,
TypographyModule,
],
})
export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseComponent {
type = TwoFactorProviderType.WebAuthn;
name: string;
keys: Key[];
keyIdAvailable: number = null;
name: string = "";
keys: Key[] = [];
keyIdAvailable: number | null = null;
keysConfiguredCount = 0;
webAuthnError: boolean;
webAuthnListening: boolean;
webAuthnResponse: PublicKeyCredential;
challengePromise: Promise<ChallengeResponse>;
formPromise: Promise<TwoFactorWebAuthnResponse>;
webAuthnError: boolean = false;
webAuthnListening: boolean = false;
webAuthnResponse: PublicKeyCredential | null = null;
challengePromise: Promise<ChallengeResponse> | undefined;
formPromise: Promise<TwoFactorWebAuthnResponse> | undefined;
override componentName = "app-two-factor-webauthn";
protected formGroup = new FormGroup({
name: new FormControl({ value: "", disabled: !this.keyIdAvailable }),
});
protected formGroup: FormGroup;
constructor(
@Inject(DIALOG_DATA) protected data: AuthResponse<TwoFactorWebAuthnResponse>,
@@ -78,6 +98,9 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom
dialogService,
toastService,
);
this.formGroup = new FormGroup({
name: new FormControl({ value: "", disabled: false }),
});
this.auth(data);
}
@@ -96,9 +119,14 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom
protected async enable() {
const request = await this.buildRequestModel(UpdateTwoFactorWebAuthnRequest);
if (this.webAuthnResponse == undefined || this.keyIdAvailable == undefined) {
throw new Error("WebAuthn response or key ID is missing");
}
request.deviceResponse = this.webAuthnResponse;
request.id = this.keyIdAvailable;
request.name = this.formGroup.value.name;
request.name = this.formGroup.value.name || "";
const response = await this.apiService.putTwoFactorWebAuthn(request);
this.processResponse(response);
@@ -164,10 +192,10 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom
.create({
publicKey: webAuthnChallenge,
})
.then((data: PublicKeyCredential) => {
.then((data) => {
this.ngZone.run(() => {
this.webAuthnListening = false;
this.webAuthnResponse = data;
this.webAuthnResponse = data as PublicKeyCredential;
});
})
.catch((err) => {
@@ -189,8 +217,11 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom
this.resetWebAuthn();
this.keys = [];
this.keyIdAvailable = null;
this.formGroup.get("name").enable();
this.formGroup.get("name").setValue(null);
const nameControl = this.formGroup.get("name");
if (nameControl) {
nameControl.enable();
nameControl.setValue("");
}
this.keysConfiguredCount = 0;
for (let i = 1; i <= 5; i++) {
if (response.keys != null) {
@@ -207,7 +238,7 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom
continue;
}
}
this.keys.push({ id: i, name: null, configured: false, removePromise: null });
this.keys.push({ id: i, name: "", configured: false, removePromise: null });
if (this.keyIdAvailable == null) {
this.keyIdAvailable = i;
}
@@ -220,6 +251,9 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom
dialogService: DialogService,
config: DialogConfig<AuthResponse<TwoFactorWebAuthnResponse>>,
) {
return dialogService.open<boolean>(TwoFactorSetupWebAuthnComponent, config);
return dialogService.open<boolean, AuthResponse<TwoFactorWebAuthnResponse>>(
TwoFactorSetupWebAuthnComponent,
config as DialogConfig<AuthResponse<TwoFactorWebAuthnResponse>, DialogRef<boolean>>,
);
}
}

View File

@@ -1,21 +1,21 @@
<form *ngIf="authed" [formGroup]="formGroup" [bitSubmit]="submit">
<bit-dialog dialogSize="large" [title]="'twoStepLogin' | i18n" [subtitle]="'YubiKey'">
<ng-container bitDialogContent>
<app-callout
<bit-callout
*ngIf="enabled"
type="success"
title="{{ 'enabled' | i18n }}"
icon="bwi bwi-check-circle"
>
{{ "twoStepLoginProviderEnabled" | i18n }}
</app-callout>
<app-callout type="warning">
</bit-callout>
<bit-callout type="warning">
<p bitTypography="body1">{{ "twoFactorYubikeyWarning" | i18n }}</p>
<ul class="tw-mb-0" bitTypography="body1">
<li>{{ "twoFactorYubikeySupportUsb" | i18n }}</li>
<li>{{ "twoFactorYubikeySupportMobile" | i18n }}</li>
</ul>
</app-callout>
</bit-callout>
<img class="tw-float-right mfaType3" alt="YubiKey OTP security key logo" />
<p bitTypography="body1">{{ "twoFactorYubikeyAdd" | i18n }}:</p>
<ol bitTypography="body1">
@@ -28,7 +28,7 @@
<div class="tw-grid tw-grid-cols-12 tw-gap-4" formArrayName="formKeys">
<div class="tw-col-span-6" *ngFor="let k of keys; let i = index">
<div [formGroupName]="i">
<bit-label>{{ "yubikeyX" | i18n: i + 1 }}</bit-label>
<bit-label>{{ "yubikeyX" | i18n: (i + 1).toString() }}</bit-label>
<bit-form-field *ngIf="!keys[i].existingKey">
<input bitInput type="password" formControlName="key" appInputVerbatim />
</bit-form-field>

View File

@@ -1,8 +1,14 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CommonModule } from "@angular/common";
import { Component, Inject, OnInit } from "@angular/core";
import { FormArray, FormBuilder, FormControl, FormGroup } from "@angular/forms";
import {
FormArray,
FormBuilder,
FormControl,
FormGroup,
ReactiveFormsModule,
} from "@angular/forms";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
@@ -12,7 +18,24 @@ import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DIALOG_DATA, DialogConfig, DialogService, ToastService } from "@bitwarden/components";
import {
AsyncActionsModule,
ButtonModule,
CalloutModule,
CheckboxModule,
DIALOG_DATA,
DialogConfig,
DialogModule,
DialogRef,
DialogService,
FormFieldModule,
IconButtonModule,
InputModule,
LinkModule,
ToastService,
TypographyModule,
} from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
import { TwoFactorSetupMethodBaseComponent } from "./two-factor-setup-method-base.component";
@@ -24,30 +47,49 @@ interface Key {
@Component({
selector: "app-two-factor-setup-yubikey",
templateUrl: "two-factor-setup-yubikey.component.html",
standalone: true,
imports: [
CommonModule,
ReactiveFormsModule,
JslibModule,
DialogModule,
FormFieldModule,
ButtonModule,
IconButtonModule,
CalloutModule,
CheckboxModule,
LinkModule,
TypographyModule,
InputModule,
AsyncActionsModule,
I18nPipe,
],
})
export class TwoFactorSetupYubiKeyComponent
extends TwoFactorSetupMethodBaseComponent
implements OnInit
{
type = TwoFactorProviderType.Yubikey;
keys: Key[];
keys: Key[] = [];
anyKeyHasNfc = false;
formPromise: Promise<TwoFactorYubiKeyResponse>;
disablePromise: Promise<unknown>;
formPromise: Promise<TwoFactorYubiKeyResponse> | undefined;
disablePromise: Promise<unknown> | undefined;
override componentName = "app-two-factor-yubikey";
formGroup: FormGroup<{
formKeys: FormArray<FormControl<Key>>;
anyKeyHasNfc: FormControl<boolean>;
}>;
formGroup:
| FormGroup<{
formKeys: FormArray<FormControl<Key | null>>;
anyKeyHasNfc: FormControl<boolean | null>;
}>
| undefined;
get keysFormControl() {
return this.formGroup.controls.formKeys.controls;
return this.formGroup?.controls.formKeys.controls;
}
get anyKeyHasNfcFormControl() {
return this.formGroup.controls.anyKeyHasNfc;
return this.formGroup?.controls.anyKeyHasNfc;
}
constructor(
@@ -82,6 +124,9 @@ export class TwoFactorSetupYubiKeyComponent
}
refreshFormArrayData() {
if (!this.formGroup) {
return;
}
const formKeys = <FormArray>this.formGroup.get("formKeys");
formKeys.clear();
this.keys.forEach((val) => {
@@ -99,6 +144,9 @@ export class TwoFactorSetupYubiKeyComponent
}
submit = async () => {
if (!this.formGroup) {
return;
}
this.formGroup.markAllAsTouched();
if (this.formGroup.invalid) {
return;
@@ -117,14 +165,17 @@ export class TwoFactorSetupYubiKeyComponent
};
protected async enable() {
if (!this.formGroup) {
return;
}
const keys = this.formGroup.controls.formKeys.value;
const request = await this.buildRequestModel(UpdateTwoFactorYubikeyOtpRequest);
request.key1 = keys != null && keys.length > 0 ? keys[0].key : null;
request.key2 = keys != null && keys.length > 1 ? keys[1].key : null;
request.key3 = keys != null && keys.length > 2 ? keys[2].key : null;
request.key4 = keys != null && keys.length > 3 ? keys[3].key : null;
request.key5 = keys != null && keys.length > 4 ? keys[4].key : null;
request.nfc = this.formGroup.value.anyKeyHasNfc;
request.key1 = keys != null && keys.length > 0 ? (keys[0]?.key ?? "") : "";
request.key2 = keys != null && keys.length > 1 ? (keys[1]?.key ?? "") : "";
request.key3 = keys != null && keys.length > 2 ? (keys[2]?.key ?? "") : "";
request.key4 = keys != null && keys.length > 3 ? (keys[3]?.key ?? "") : "";
request.key5 = keys != null && keys.length > 4 ? (keys[4]?.key ?? "") : "";
request.nfc = this.formGroup.value.anyKeyHasNfc ?? false;
this.processResponse(await this.apiService.putTwoFactorYubiKey(request));
this.refreshFormArrayData();
@@ -137,12 +188,16 @@ export class TwoFactorSetupYubiKeyComponent
}
remove(pos: number) {
this.keys[pos].key = null;
this.keys[pos].existingKey = null;
this.keys[pos].key = "";
this.keys[pos].existingKey = "";
if (!this.keysFormControl || !this.keysFormControl[pos]) {
return;
}
this.keysFormControl[pos].setValue({
existingKey: null,
key: null,
existingKey: "",
key: "",
});
}
@@ -173,6 +228,9 @@ export class TwoFactorSetupYubiKeyComponent
dialogService: DialogService,
config: DialogConfig<AuthResponse<TwoFactorYubiKeyResponse>>,
) {
return dialogService.open<boolean>(TwoFactorSetupYubiKeyComponent, config);
return dialogService.open<boolean, AuthResponse<TwoFactorYubiKeyResponse>>(
TwoFactorSetupYubiKeyComponent,
config as DialogConfig<AuthResponse<TwoFactorYubiKeyResponse>, DialogRef<boolean>>,
);
}
}

View File

@@ -1,7 +1,7 @@
<app-header *ngIf="organizationId != null"></app-header>
<bit-container>
<div class="tabbed-header" *ngIf="organizationId == null">
<div class="tw-mt-6 tw-mb-2 tw-pb-2.5" *ngIf="organizationId == null">
<h1 *ngIf="!organizationId || !isEnterpriseOrg">{{ "twoStepLogin" | i18n }}</h1>
<h1 *ngIf="organizationId && isEnterpriseOrg">{{ "twoStepLoginEnforcement" | i18n }}</h1>
</div>
@@ -35,7 +35,7 @@
{{ "providers" | i18n }}
<small *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin bwi-fw text-muted"
class="bwi bwi-spinner bwi-spin bwi-fw tw-text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
@@ -45,32 +45,32 @@
<bit-callout type="warning" *ngIf="showPolicyWarning">
{{ "twoStepLoginPolicyUserWarning" | i18n }}
</bit-callout>
<ul class="list-group list-group-2fa">
<li *ngFor="let p of providers" class="list-group-item d-flex align-items-center">
<div class="logo-2fa d-flex justify-content-center">
<auth-two-factor-icon [provider]="p.type" [name]="p.name" />
<bit-item-group [attr.aria-label]="'providers' | i18n">
<bit-item *ngFor="let p of providers" class="tw-py-4">
<div slot="start" class="tw-min-w-[120px] tw-flex tw-justify-center">
<auth-two-factor-icon class="tw-flex tw-items-center" [provider]="p.type" [name]="p.name" />
</div>
<div class="mx-4">
<h3 class="mb-0">
<div bit-item-content class="tw-px-4">
<h3 class="tw-mb-0">
<div
class="font-weight-semibold tw-text-base"
class="tw-font-semibold tw-text-base"
[style]="p.enabled || p.premium ? 'display:inline-block' : ''"
>
{{ p.name }}
</div>
<ng-container *ngIf="p.enabled">
<i
class="bwi bwi-check text-success bwi-fw"
class="bwi bwi-check tw-text-success-600 bwi-fw tw-ml-2"
title="{{ 'enabled' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "enabled" | i18n }}</span>
</ng-container>
<app-premium-badge *ngIf="p.premium"></app-premium-badge>
<app-premium-badge class="tw-ml-2" *ngIf="p.premium"></app-premium-badge>
</h3>
{{ p.description }}
<div class="tw-mt-2 tw-text-wrap">{{ p.description }}</div>
</div>
<div class="ml-auto">
<bit-item-action slot="end">
<button
type="button"
bitButton
@@ -80,9 +80,9 @@
>
{{ "manage" | i18n }}
</button>
</div>
</li>
</ul>
</bit-item-action>
</bit-item>
</bit-item-group>
</bit-container>
<ng-template #duoTemplate></ng-template>

View File

@@ -32,7 +32,10 @@ import { ProductTierType } from "@bitwarden/common/billing/enums";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { DialogRef, DialogService } from "@bitwarden/components";
import { DialogRef, DialogService, ItemModule } from "@bitwarden/components";
import { LooseComponentsModule } from "../../../shared/loose-components.module";
import { SharedModule } from "../../../shared/shared.module";
import { TwoFactorRecoveryComponent } from "./two-factor-recovery.component";
import { TwoFactorSetupAuthenticatorComponent } from "./two-factor-setup-authenticator.component";
@@ -45,6 +48,8 @@ import { TwoFactorVerifyComponent } from "./two-factor-verify.component";
@Component({
selector: "app-two-factor-setup",
templateUrl: "two-factor-setup.component.html",
standalone: true,
imports: [ItemModule, LooseComponentsModule, SharedModule],
})
export class TwoFactorSetupComponent implements OnInit, OnDestroy {
@ViewChild("yubikeyTemplate", { read: ViewContainerRef, static: true })

View File

@@ -1,8 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, EventEmitter, Inject, Output } from "@angular/core";
import { FormControl, FormGroup } from "@angular/forms";
import { FormControl, FormGroup, ReactiveFormsModule } from "@angular/forms";
import { UserVerificationFormInputComponent } from "@bitwarden/auth/angular";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
@@ -13,7 +12,16 @@ import { TwoFactorResponse } from "@bitwarden/common/auth/types/two-factor-respo
import { Verification } from "@bitwarden/common/auth/types/verification";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components";
import {
AsyncActionsModule,
ButtonModule,
DIALOG_DATA,
DialogConfig,
DialogModule,
DialogRef,
DialogService,
} from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
type TwoFactorVerifyDialogData = {
type: TwoFactorProviderType;
@@ -23,13 +31,22 @@ type TwoFactorVerifyDialogData = {
@Component({
selector: "app-two-factor-verify",
templateUrl: "two-factor-verify.component.html",
standalone: true,
imports: [
AsyncActionsModule,
ButtonModule,
DialogModule,
I18nPipe,
ReactiveFormsModule,
UserVerificationFormInputComponent,
],
})
export class TwoFactorVerifyComponent {
type: TwoFactorProviderType;
organizationId: string;
@Output() onAuthed = new EventEmitter<AuthResponse<TwoFactorResponse>>();
formPromise: Promise<TwoFactorResponse>;
formPromise: Promise<TwoFactorResponse> | undefined;
protected formGroup = new FormGroup({
secret: new FormControl<Verification | null>(null),
@@ -49,22 +66,25 @@ export class TwoFactorVerifyComponent {
submit = async () => {
try {
let hashedSecret: string;
this.formPromise = this.userVerificationService
.buildRequest(this.formGroup.value.secret)
.then((request) => {
hashedSecret =
this.formGroup.value.secret.type === VerificationType.MasterPassword
? request.masterPasswordHash
: request.otp;
return this.apiCall(request);
});
let hashedSecret = "";
if (!this.formGroup.value.secret) {
throw new Error("Secret is required");
}
const secret = this.formGroup.value.secret!;
this.formPromise = this.userVerificationService.buildRequest(secret).then((request) => {
hashedSecret =
secret.type === VerificationType.MasterPassword
? request.masterPasswordHash
: request.otp;
return this.apiCall(request);
});
const response = await this.formPromise;
this.dialogRef.close({
response: response,
secret: hashedSecret,
verificationType: this.formGroup.value.secret.type,
verificationType: secret.type,
});
} catch (e) {
if (e instanceof ErrorResponse && e.statusCode === 400) {
@@ -88,6 +108,8 @@ export class TwoFactorVerifyComponent {
return this.i18nService.t("authenticatorAppTitle");
case TwoFactorProviderType.Yubikey:
return "Yubikey";
default:
throw new Error(`Unknown two-factor type: ${this.type}`);
}
}
@@ -110,10 +132,15 @@ export class TwoFactorVerifyComponent {
return this.apiService.getTwoFactorAuthenticator(request);
case TwoFactorProviderType.Yubikey:
return this.apiService.getTwoFactorYubiKey(request);
default:
throw new Error(`Unknown two-factor type: ${this.type}`);
}
}
static open(dialogService: DialogService, config: DialogConfig<TwoFactorVerifyDialogData>) {
return dialogService.open<AuthResponse<any>>(TwoFactorVerifyComponent, config);
return dialogService.open<AuthResponse<any>, TwoFactorVerifyDialogData>(
TwoFactorVerifyComponent,
config as DialogConfig<TwoFactorVerifyDialogData, DialogRef<AuthResponse<any>>>,
);
}
}

View File

@@ -33,14 +33,6 @@ import { ApiKeyComponent } from "../auth/settings/security/api-key.component";
import { ChangeKdfModule } from "../auth/settings/security/change-kdf/change-kdf.module";
import { SecurityKeysComponent } from "../auth/settings/security/security-keys.component";
import { SecurityComponent } from "../auth/settings/security/security.component";
import { TwoFactorRecoveryComponent } from "../auth/settings/two-factor/two-factor-recovery.component";
import { TwoFactorSetupAuthenticatorComponent } from "../auth/settings/two-factor/two-factor-setup-authenticator.component";
import { TwoFactorSetupDuoComponent } from "../auth/settings/two-factor/two-factor-setup-duo.component";
import { TwoFactorSetupEmailComponent } from "../auth/settings/two-factor/two-factor-setup-email.component";
import { TwoFactorSetupWebAuthnComponent } from "../auth/settings/two-factor/two-factor-setup-webauthn.component";
import { TwoFactorSetupYubiKeyComponent } from "../auth/settings/two-factor/two-factor-setup-yubikey.component";
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 { UpdatePasswordComponent } from "../auth/update-password.component";
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component";
@@ -145,14 +137,6 @@ import { SharedModule } from "./shared.module";
SetPasswordComponent,
SponsoredFamiliesComponent,
SponsoringOrgRowComponent,
TwoFactorSetupAuthenticatorComponent,
TwoFactorSetupDuoComponent,
TwoFactorSetupEmailComponent,
TwoFactorRecoveryComponent,
TwoFactorSetupComponent,
TwoFactorVerifyComponent,
TwoFactorSetupWebAuthnComponent,
TwoFactorSetupYubiKeyComponent,
UpdatePasswordComponent,
UpdateTempPasswordComponent,
VerifyEmailTokenComponent,
@@ -204,13 +188,6 @@ import { SharedModule } from "./shared.module";
SetPasswordComponent,
SponsoredFamiliesComponent,
SponsoringOrgRowComponent,
TwoFactorSetupAuthenticatorComponent,
TwoFactorSetupDuoComponent,
TwoFactorSetupEmailComponent,
TwoFactorSetupComponent,
TwoFactorVerifyComponent,
TwoFactorSetupWebAuthnComponent,
TwoFactorSetupYubiKeyComponent,
UpdateTempPasswordComponent,
UpdatePasswordComponent,
UserLayoutComponent,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -30,12 +30,6 @@
width: 100%;
}
.list-group-2fa {
.logo-2fa {
min-width: 120px;
}
}
@each $mfaType in $mfaTypes {
.mfaType#{$mfaType} {
content: url("../images/two-factor/" + $mfaType + ".png");
@@ -66,14 +60,6 @@
}
}
.recovery-code-img {
@include themify($themes) {
content: url("../images/two-factor/rc" + themed("mfaLogoSuffix"));
max-width: 100px;
max-height: 45px;
}
}
.progress {
@include themify($themes) {
background-color: themed("pwStrengthBackground");

View File

@@ -66,7 +66,7 @@ describe("Menu", () => {
@Component({
selector: "test-app",
template: `
<button type="button" [bitMenuTriggerFor]="testMenu" class="testclass">Open menu</button>
<button type="button" [bitMenuTriggerFor]="testMenu">Open menu</button>
<bit-menu #testMenu>
<a id="item1" bitMenuItem>Item 1</a>