PM- 2060 Update Two Factor Yubikey dialog (#9010)

* PM-2060 Update Two Factor Yubikey Dialog

* PM-2060 Removed old code

* PM-2060 Added event emitter to capture enabled status

* PM-2060 Addressed review comments

* PM-2060 Change in html file for existing key options

* PM-2060 Addressed the latest comments

* PM-2060 Updated remove method as per comments

* PM-2060 Added throw error to enable and disbale in base component

* tailwind updates to yubikey two factor settings

* fixing imports

* remove disable dialog when keys are null to use the error toast

* PM-2060 Addressed the review comments and fixed conflicts

* Removed super.enable removed extra emitter from component class.

* fixing adding multiple keys in one session of a dialog.

* removed thrown error

---------

Co-authored-by: Ike Kottlowski <ikottlowski@bitwarden.com>
Co-authored-by: Ike <137194738+ike-kottlowski@users.noreply.github.com>
This commit is contained in:
KiruthigaManivannan 2024-07-10 23:26:52 +05:30 committed by GitHub
parent e4c7efba12
commit 3f0f5af26a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 183 additions and 149 deletions

View File

@ -48,7 +48,6 @@ export abstract class TwoFactorBaseComponent {
this.onUpdated.emit(true); this.onUpdated.emit(true);
} catch (e) { } catch (e) {
this.logService.error(e); this.logService.error(e);
throw e;
} }
} }

View File

@ -160,10 +160,12 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
if (!result) { if (!result) {
return; return;
} }
const yubiComp = await this.openModal(this.yubikeyModalRef, TwoFactorYubiKeyComponent); const yubiComp: DialogRef<boolean, any> = TwoFactorYubiKeyComponent.open(
yubiComp.auth(result); this.dialogService,
this.twoFactorSetupSubscription = yubiComp.onUpdated { data: result },
.pipe(first(), takeUntil(this.destroy$)) );
yubiComp.componentInstance.onUpdated
.pipe(takeUntil(this.destroy$))
.subscribe((enabled: boolean) => { .subscribe((enabled: boolean) => {
this.updateStatus(enabled, TwoFactorProviderType.Yubikey); this.updateStatus(enabled, TwoFactorProviderType.Yubikey);
}); });
@ -266,7 +268,7 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
this.modal.close(); this.modal.close();
} }
this.providers.forEach((p) => { this.providers.forEach((p) => {
if (p.type === type) { if (p.type === type && enabled !== undefined) {
p.enabled = enabled; p.enabled = enabled;
} }
}); });

View File

@ -1,118 +1,84 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="2faYubiKeyTitle"> <form *ngIf="authed" [formGroup]="formGroup" [bitSubmit]="submit">
<div class="modal-dialog modal-lg" role="document"> <bit-dialog dialogSize="large">
<div class="modal-content"> <span bitDialogTitle>
<div class="modal-header"> {{ "twoStepLogin" | i18n }}
<h1 class="modal-title" id="2faYubiKeyTitle"> <span bitTypography="body1">YubiKey</span>
{{ "twoStepLogin" | i18n }} </span>
<small>YubiKey</small> <ng-container bitDialogContent>
</h1> <app-callout
<button *ngIf="enabled"
type="button" type="success"
class="close" title="{{ 'enabled' | i18n }}"
data-dismiss="modal" icon="bwi bwi-check-circle"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<form
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
*ngIf="authed"
autocomplete="off"
> >
<div class="modal-body"> {{ "twoStepLoginProviderEnabled" | i18n }}
<app-callout </app-callout>
type="success" <app-callout type="warning">
title="{{ 'enabled' | i18n }}" <p bitTypography="body1">{{ "twoFactorYubikeyWarning" | i18n }}</p>
icon="bwi bwi-check-circle" <ul class="tw-mb-0" bitTypography="body1">
*ngIf="enabled" <li>{{ "twoFactorYubikeySupportUsb" | i18n }}</li>
> <li>{{ "twoFactorYubikeySupportMobile" | i18n }}</li>
{{ "twoStepLoginProviderEnabled" | i18n }} </ul>
</app-callout> </app-callout>
<app-callout type="warning"> <img class="tw-float-right mfaType3" alt="YubiKey OTP security key logo" />
<p>{{ "twoFactorYubikeyWarning" | i18n }}</p> <p bitTypography="body1">{{ "twoFactorYubikeyAdd" | i18n }}:</p>
<ul class="mb-0"> <ol bitTypography="body1">
<li>{{ "twoFactorYubikeySupportUsb" | i18n }}</li> <li>{{ "twoFactorYubikeyPlugIn" | i18n }}</li>
<li>{{ "twoFactorYubikeySupportMobile" | i18n }}</li> <li>{{ "twoFactorYubikeySelectKey" | i18n }}</li>
</ul> <li>{{ "twoFactorYubikeyTouchButton" | i18n }}</li>
</app-callout> <li>{{ "twoFactorYubikeySaveForm" | i18n }}</li>
<img class="float-right mfaType3" alt="Yubico OTP security key logo" /> </ol>
<p>{{ "twoFactorYubikeyAdd" | i18n }}:</p> <hr />
<ol> <div class="tw-grid tw-grid-cols-12 tw-gap-4" formArrayName="formKeys">
<li>{{ "twoFactorYubikeyPlugIn" | i18n }}</li> <div class="tw-col-span-6" *ngFor="let k of keys; let i = index">
<li>{{ "twoFactorYubikeySelectKey" | i18n }}</li> <div [formGroupName]="i">
<li>{{ "twoFactorYubikeyTouchButton" | i18n }}</li> <bit-label>{{ "yubikeyX" | i18n: i + 1 }}</bit-label>
<li>{{ "twoFactorYubikeySaveForm" | i18n }}</li> <bit-form-field *ngIf="!keys[i].existingKey">
</ol> <input bitInput type="password" formControlName="key" appInputVerbatim />
<hr /> </bit-form-field>
<div class="row"> <div class="tw-flex tw-justify-between tw-mb-6" *ngIf="keys[i].existingKey">
<div class="form-group col-6" *ngFor="let k of keys; let i = index"> <span class="tw-mr-2 tw-self-center">{{ keys[i].existingKey }}</span>
<label for="key{{ i + 1 }}">{{ "yubikeyX" | i18n: i + 1 }}</label> <button
<input bitIconButton="bwi-minus-circle"
id="key{{ i + 1 }}" type="button"
type="password" buttonType="danger"
name="Key{{ i + 1 }}" (click)="remove(i)"
class="form-control" appA11yTitle="{{ 'remove' | i18n }}"
[(ngModel)]="k.key" ></button>
*ngIf="!k.existingKey"
appInputVerbatim
autocomplete="new-password"
/>
<div class="d-flex" *ngIf="k.existingKey">
<span class="mr-2">{{ k.existingKey }}</span>
<button
type="button"
class="btn btn-link text-danger ml-auto"
(click)="remove(k)"
appA11yTitle="{{ 'remove' | i18n }}"
>
<i class="bwi bwi-minus-circle bwi-lg" aria-hidden="true"></i>
</button>
</div>
</div> </div>
</div> </div>
<strong class="d-block mb-2">{{ "nfcSupport" | i18n }}</strong>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="nfc" name="Nfc" [(ngModel)]="nfc" />
<label class="form-check-label" for="nfc">{{
"twoFactorYubikeySupportsNfc" | i18n
}}</label>
</div>
<small class="form-text text-muted">{{ "twoFactorYubikeySupportsNfcDesc" | i18n }}</small>
</div> </div>
<div class="modal-footer"> </div>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading"> <p bitTypography="body1" class="tw-font-bold tw-mb-2">{{ "nfcSupport" | i18n }}</p>
<i <bit-form-control [disableMargin]="true">
class="bwi bwi-spinner bwi-spin" <bit-label>{{ "twoFactorYubikeySupportsNfc" | i18n }}</bit-label>
title="{{ 'loading' | i18n }}" <input bitCheckbox type="checkbox" formControlName="anyKeyHasNfc" />
aria-hidden="true" <bit-hint class="tw-text-sm">{{ "twoFactorYubikeySupportsNfcDesc" | i18n }}</bit-hint>
></i> </bit-form-control>
<span>{{ "save" | i18n }}</span> </ng-container>
</button> <ng-container bitDialogFooter>
<button <button bitButton bitFormButton type="submit" buttonType="primary">
#disableBtn {{ "save" | i18n }}
type="button" </button>
class="btn btn-outline-secondary btn-submit" <button
[appApiAction]="disablePromise" *ngIf="enabled"
[disabled]="$any(disableBtn).loading" bitButton
(click)="disable()" bitFormButton
*ngIf="enabled" type="button"
> buttonType="secondary"
<i [bitAction]="disable"
class="bwi bwi-spinner bwi-spin" >
title="{{ 'loading' | i18n }}" {{ "disableAllKeys" | i18n }}
aria-hidden="true" </button>
></i> <button
<span>{{ "disableAllKeys" | i18n }}</span> bitButton
</button> bitFormButton
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal"> type="button"
{{ "close" | i18n }} buttonType="secondary"
</button> [bitDialogClose]="this.enabled"
</div> >
</form> {{ "close" | i18n }}
</div> </button>
</div> </ng-container>
</div> </bit-dialog>
</form>

View File

@ -1,15 +1,17 @@
import { Component } from "@angular/core"; import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core";
import { FormArray, FormBuilder, FormControl, FormGroup } from "@angular/forms";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { UpdateTwoFactorYubioOtpRequest } from "@bitwarden/common/auth/models/request/update-two-factor-yubio-otp.request"; import { UpdateTwoFactorYubikeyOtpRequest } from "@bitwarden/common/auth/models/request/update-two-factor-yubikey-otp.request";
import { TwoFactorYubiKeyResponse } from "@bitwarden/common/auth/models/response/two-factor-yubi-key.response"; import { TwoFactorYubiKeyResponse } from "@bitwarden/common/auth/models/response/two-factor-yubi-key.response";
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response"; import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogService } from "@bitwarden/components"; import { DialogService, ToastService } from "@bitwarden/components";
import { TwoFactorBaseComponent } from "./two-factor-base.component"; import { TwoFactorBaseComponent } from "./two-factor-base.component";
@ -25,20 +27,35 @@ interface Key {
export class TwoFactorYubiKeyComponent extends TwoFactorBaseComponent { export class TwoFactorYubiKeyComponent extends TwoFactorBaseComponent {
type = TwoFactorProviderType.Yubikey; type = TwoFactorProviderType.Yubikey;
keys: Key[]; keys: Key[];
nfc = false; anyKeyHasNfc = false;
formPromise: Promise<TwoFactorYubiKeyResponse>; formPromise: Promise<TwoFactorYubiKeyResponse>;
disablePromise: Promise<unknown>; disablePromise: Promise<unknown>;
override componentName = "app-two-factor-yubikey"; override componentName = "app-two-factor-yubikey";
formGroup: FormGroup<{
formKeys: FormArray<FormControl<Key>>;
anyKeyHasNfc: FormControl<boolean>;
}>;
get keysFormControl() {
return this.formGroup.controls.formKeys.controls;
}
get anyKeyHasNfcFormControl() {
return this.formGroup.controls.anyKeyHasNfc;
}
constructor( constructor(
@Inject(DIALOG_DATA) protected data: AuthResponse<TwoFactorYubiKeyResponse>,
apiService: ApiService, apiService: ApiService,
i18nService: I18nService, i18nService: I18nService,
platformUtilsService: PlatformUtilsService, platformUtilsService: PlatformUtilsService,
logService: LogService, logService: LogService,
userVerificationService: UserVerificationService, userVerificationService: UserVerificationService,
dialogService: DialogService, dialogService: DialogService,
private formBuilder: FormBuilder,
private toastService: ToastService,
) { ) {
super( super(
apiService, apiService,
@ -50,39 +67,83 @@ export class TwoFactorYubiKeyComponent extends TwoFactorBaseComponent {
); );
} }
ngOnInit() {
this.auth(this.data);
this.formGroup = this.formBuilder.group({
formKeys: this.formBuilder.array<Key>([]),
anyKeyHasNfc: this.formBuilder.control(this.anyKeyHasNfc),
});
this.refreshFormArrayData();
}
refreshFormArrayData() {
const formKeys = <FormArray>this.formGroup.get("formKeys");
formKeys.clear();
this.keys.forEach((val) => {
const fb = this.formBuilder.group({
key: val.key,
existingKey: val.existingKey,
});
formKeys.push(fb);
});
}
auth(authResponse: AuthResponse<TwoFactorYubiKeyResponse>) { auth(authResponse: AuthResponse<TwoFactorYubiKeyResponse>) {
super.auth(authResponse); super.auth(authResponse);
this.processResponse(authResponse.response); this.processResponse(authResponse.response);
} }
async submit() { submit = async () => {
const request = await this.buildRequestModel(UpdateTwoFactorYubioOtpRequest); this.formGroup.markAllAsTouched();
request.key1 = this.keys != null && this.keys.length > 0 ? this.keys[0].key : null; if (this.formGroup.invalid) {
request.key2 = this.keys != null && this.keys.length > 1 ? this.keys[1].key : null; return;
request.key3 = this.keys != null && this.keys.length > 2 ? this.keys[2].key : null; }
request.key4 = this.keys != null && this.keys.length > 3 ? this.keys[3].key : null; await this.enable();
request.key5 = this.keys != null && this.keys.length > 4 ? this.keys[4].key : null; };
request.nfc = this.nfc;
return super.enable(async () => { disable = async () => {
this.formPromise = this.apiService.putTwoFactorYubiKey(request); await this.disableMethod();
const response = await this.formPromise;
await this.processResponse(response); if (!this.enabled) {
this.platformUtilsService.showToast("success", null, this.i18nService.t("yubikeysUpdated")); for (let i = 0; i < this.keys.length; i++) {
this.remove(i);
}
}
};
protected async enable() {
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;
this.processResponse(await this.apiService.putTwoFactorYubiKey(request));
this.refreshFormArrayData();
this.toastService.showToast({
title: this.i18nService.t("success"),
message: this.i18nService.t("yubikeysUpdated"),
variant: "success",
}); });
this.onUpdated.emit(this.enabled);
} }
disable() { remove(pos: number) {
return super.disable(this.disablePromise); this.keys[pos].key = null;
} this.keys[pos].existingKey = null;
remove(key: Key) { this.keysFormControl[pos].setValue({
key.existingKey = null; existingKey: null,
key.key = null; key: null,
});
} }
private processResponse(response: TwoFactorYubiKeyResponse) { private processResponse(response: TwoFactorYubiKeyResponse) {
this.enabled = response.enabled; this.enabled = response.enabled;
this.anyKeyHasNfc = response.nfc || !response.enabled;
this.keys = [ this.keys = [
{ key: response.key1, existingKey: this.padRight(response.key1) }, { key: response.key1, existingKey: this.padRight(response.key1) },
{ key: response.key2, existingKey: this.padRight(response.key2) }, { key: response.key2, existingKey: this.padRight(response.key2) },
@ -90,7 +151,6 @@ export class TwoFactorYubiKeyComponent extends TwoFactorBaseComponent {
{ key: response.key4, existingKey: this.padRight(response.key4) }, { key: response.key4, existingKey: this.padRight(response.key4) },
{ key: response.key5, existingKey: this.padRight(response.key5) }, { key: response.key5, existingKey: this.padRight(response.key5) },
]; ];
this.nfc = response.nfc || !response.enabled;
} }
private padRight(str: string, character = "•", size = 44) { private padRight(str: string, character = "•", size = 44) {
@ -103,4 +163,11 @@ export class TwoFactorYubiKeyComponent extends TwoFactorBaseComponent {
} }
return str; return str;
} }
static open(
dialogService: DialogService,
config: DialogConfig<AuthResponse<TwoFactorYubiKeyResponse>>,
) {
return dialogService.open<boolean>(TwoFactorYubiKeyComponent, config);
}
} }

View File

@ -53,7 +53,7 @@ import { UpdateTwoFactorDuoRequest } from "../auth/models/request/update-two-fac
import { UpdateTwoFactorEmailRequest } from "../auth/models/request/update-two-factor-email.request"; import { UpdateTwoFactorEmailRequest } from "../auth/models/request/update-two-factor-email.request";
import { UpdateTwoFactorWebAuthnDeleteRequest } from "../auth/models/request/update-two-factor-web-authn-delete.request"; import { UpdateTwoFactorWebAuthnDeleteRequest } from "../auth/models/request/update-two-factor-web-authn-delete.request";
import { UpdateTwoFactorWebAuthnRequest } from "../auth/models/request/update-two-factor-web-authn.request"; import { UpdateTwoFactorWebAuthnRequest } from "../auth/models/request/update-two-factor-web-authn.request";
import { UpdateTwoFactorYubioOtpRequest } from "../auth/models/request/update-two-factor-yubio-otp.request"; import { UpdateTwoFactorYubikeyOtpRequest } from "../auth/models/request/update-two-factor-yubikey-otp.request";
import { ApiKeyResponse } from "../auth/models/response/api-key.response"; import { ApiKeyResponse } from "../auth/models/response/api-key.response";
import { AuthRequestResponse } from "../auth/models/response/auth-request.response"; import { AuthRequestResponse } from "../auth/models/response/auth-request.response";
import { DeviceVerificationResponse } from "../auth/models/response/device-verification.response"; import { DeviceVerificationResponse } from "../auth/models/response/device-verification.response";
@ -330,7 +330,7 @@ export abstract class ApiService {
request: UpdateTwoFactorDuoRequest, request: UpdateTwoFactorDuoRequest,
) => Promise<TwoFactorDuoResponse>; ) => Promise<TwoFactorDuoResponse>;
putTwoFactorYubiKey: ( putTwoFactorYubiKey: (
request: UpdateTwoFactorYubioOtpRequest, request: UpdateTwoFactorYubikeyOtpRequest,
) => Promise<TwoFactorYubiKeyResponse>; ) => Promise<TwoFactorYubiKeyResponse>;
putTwoFactorWebAuthn: ( putTwoFactorWebAuthn: (
request: UpdateTwoFactorWebAuthnRequest, request: UpdateTwoFactorWebAuthnRequest,

View File

@ -1,6 +1,6 @@
import { SecretVerificationRequest } from "./secret-verification.request"; import { SecretVerificationRequest } from "./secret-verification.request";
export class UpdateTwoFactorYubioOtpRequest extends SecretVerificationRequest { export class UpdateTwoFactorYubikeyOtpRequest extends SecretVerificationRequest {
key1: string; key1: string;
key2: string; key2: string;
key3: string; key3: string;

View File

@ -62,7 +62,7 @@ import { UpdateTwoFactorDuoRequest } from "../auth/models/request/update-two-fac
import { UpdateTwoFactorEmailRequest } from "../auth/models/request/update-two-factor-email.request"; import { UpdateTwoFactorEmailRequest } from "../auth/models/request/update-two-factor-email.request";
import { UpdateTwoFactorWebAuthnDeleteRequest } from "../auth/models/request/update-two-factor-web-authn-delete.request"; import { UpdateTwoFactorWebAuthnDeleteRequest } from "../auth/models/request/update-two-factor-web-authn-delete.request";
import { UpdateTwoFactorWebAuthnRequest } from "../auth/models/request/update-two-factor-web-authn.request"; import { UpdateTwoFactorWebAuthnRequest } from "../auth/models/request/update-two-factor-web-authn.request";
import { UpdateTwoFactorYubioOtpRequest } from "../auth/models/request/update-two-factor-yubio-otp.request"; import { UpdateTwoFactorYubikeyOtpRequest } from "../auth/models/request/update-two-factor-yubikey-otp.request";
import { ApiKeyResponse } from "../auth/models/response/api-key.response"; import { ApiKeyResponse } from "../auth/models/response/api-key.response";
import { AuthRequestResponse } from "../auth/models/response/auth-request.response"; import { AuthRequestResponse } from "../auth/models/response/auth-request.response";
import { DeviceVerificationResponse } from "../auth/models/response/device-verification.response"; import { DeviceVerificationResponse } from "../auth/models/response/device-verification.response";
@ -1023,7 +1023,7 @@ export class ApiService implements ApiServiceAbstraction {
} }
async putTwoFactorYubiKey( async putTwoFactorYubiKey(
request: UpdateTwoFactorYubioOtpRequest, request: UpdateTwoFactorYubikeyOtpRequest,
): Promise<TwoFactorYubiKeyResponse> { ): Promise<TwoFactorYubiKeyResponse> {
const r = await this.send("PUT", "/two-factor/yubikey", request, true, true); const r = await this.send("PUT", "/two-factor/yubikey", request, true, true);
return new TwoFactorYubiKeyResponse(r); return new TwoFactorYubiKeyResponse(r);