PM- 2059 Update Two factor webauthn dialog (#9009)

* PM-2059 Update Two Factor Webauth Dialog

* PM-2059 Added event emitter for enabled status

* PM-2059 Addressed review comments

* convert to arrow function

* PM-2059 Latest comments addressed

* PM-2059 Updated disable method by adding a condition to capture simple dialog in base component

---------

Co-authored-by: rr-bw <102181210+rr-bw@users.noreply.github.com>
This commit is contained in:
KiruthigaManivannan 2024-06-25 20:33:48 +05:30 committed by GitHub
parent b6c46745a5
commit 7fffbc7938
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 157 additions and 167 deletions

View File

@ -39,8 +39,6 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
@ViewChild("duoTemplate", { read: ViewContainerRef, static: true }) duoModalRef: ViewContainerRef; @ViewChild("duoTemplate", { read: ViewContainerRef, static: true }) duoModalRef: ViewContainerRef;
@ViewChild("emailTemplate", { read: ViewContainerRef, static: true }) @ViewChild("emailTemplate", { read: ViewContainerRef, static: true })
emailModalRef: ViewContainerRef; emailModalRef: ViewContainerRef;
@ViewChild("webAuthnTemplate", { read: ViewContainerRef, static: true })
webAuthnModalRef: ViewContainerRef;
organizationId: string; organizationId: string;
organization: Organization; organization: Organization;
@ -192,12 +190,11 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
if (!result) { if (!result) {
return; return;
} }
const webAuthnComp = await this.openModal( const webAuthnComp: DialogRef<boolean, any> = TwoFactorWebAuthnComponent.open(
this.webAuthnModalRef, this.dialogService,
TwoFactorWebAuthnComponent, { data: result },
); );
webAuthnComp.auth(result); webAuthnComp.componentInstance.onChangeStatus.subscribe((enabled: boolean) => {
webAuthnComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => {
this.updateStatus(enabled, TwoFactorProviderType.WebAuthn); this.updateStatus(enabled, TwoFactorProviderType.WebAuthn);
}); });
break; break;

View File

@ -1,152 +1,118 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="2faU2fTitle"> <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="2faU2fTitle"> <span bitTypography="body1">{{ "webAuthnTitle" | i18n }}</span>
{{ "twoStepLogin" | i18n }} </span>
<small>{{ "webAuthnTitle" | i18n }}</small> <ng-container bitDialogContent>
</h1> <app-callout
<button type="success"
type="button" title="{{ 'enabled' | i18n }}"
class="close" icon="bwi bwi-check-circle"
data-dismiss="modal" *ngIf="enabled"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<form
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
*ngIf="authed"
> >
<div class="modal-body"> {{ "twoStepLoginProviderEnabled" | i18n }}
<app-callout </app-callout>
type="success" <app-callout type="warning">
title="{{ 'enabled' | i18n }}" <p bitTypography="body1">{{ "twoFactorWebAuthnWarning" | i18n }}</p>
icon="bwi bwi-check-circle" <ul class="tw-mb-0">
*ngIf="enabled" <li>{{ "twoFactorWebAuthnSupportWeb" | i18n }}</li>
> </ul>
{{ "twoStepLoginProviderEnabled" | i18n }} </app-callout>
</app-callout> <img class="tw-float-right tw-ml-5 mfaType7" alt="FIDO2 WebAuthn logo" />
<app-callout type="warning"> <ul class="bwi-ul">
<p>{{ "twoFactorWebAuthnWarning" | i18n }}</p> <li *ngFor="let k of keys; let i = index" #removeKeyBtn [appApiAction]="k.removePromise">
<ul class="mb-0"> <i class="bwi bwi-li bwi-key"></i>
<li>{{ "twoFactorWebAuthnSupportWeb" | i18n }}</li> <span *ngIf="!k.configured || !k.name" bitTypography="body1" class="tw-font-bold">
</ul> {{ "webAuthnkeyX" | i18n: i + 1 }}
</app-callout> </span>
<img class="float-right ml-5 mfaType7" alt="FIDO2 WebAuthn logo'" /> <span *ngIf="k.configured && k.name" bitTypography="body1" class="tw-font-bold">
<ul class="bwi-ul"> {{ k.name }}
<li </span>
*ngFor="let k of keys; let i = index" <ng-container *ngIf="k.configured && !$any(removeKeyBtn).loading">
#removeKeyBtn <ng-container *ngIf="k.migrated">
[appApiAction]="k.removePromise" <span>{{ "webAuthnMigrated" | i18n }}</span>
>
<i class="bwi bwi-li bwi-key"></i>
<strong *ngIf="!k.configured || !k.name">{{ "webAuthnkeyX" | i18n: i + 1 }}</strong>
<strong *ngIf="k.configured && k.name">{{ k.name }}</strong>
<ng-container *ngIf="k.configured && !$any(removeKeyBtn).loading">
<ng-container *ngIf="k.migrated">
<span>{{ "webAuthnMigrated" | i18n }}</span>
</ng-container>
</ng-container>
<ng-container *ngIf="keysConfiguredCount > 1 && k.configured">
<i
class="bwi bwi-spin bwi-spinner text-muted bwi-fw"
title="{{ 'loading' | i18n }}"
*ngIf="$any(removeKeyBtn).loading"
aria-hidden="true"
></i>
-
<a href="#" appStopClick (click)="remove(k)">{{ "remove" | i18n }}</a>
</ng-container>
</li>
</ul>
<hr />
<p>{{ "twoFactorWebAuthnAdd" | i18n }}:</p>
<ol>
<li>{{ "twoFactorU2fGiveName" | i18n }}</li>
<li>{{ "twoFactorU2fPlugInReadKey" | i18n }}</li>
<li>{{ "twoFactorU2fTouchButton" | i18n }}</li>
<li>{{ "twoFactorU2fSaveForm" | i18n }}</li>
</ol>
<div class="row">
<div class="form-group col-6">
<label for="name">{{ "name" | i18n }}</label>
<input
id="name"
type="text"
name="Name"
class="form-control"
[(ngModel)]="name"
[disabled]="!keyIdAvailable"
/>
</div>
</div>
<button
type="button"
(click)="readKey()"
class="btn btn-outline-secondary mr-2"
[disabled]="$any(readKeyBtn).loading || webAuthnListening || !keyIdAvailable"
#readKeyBtn
[appApiAction]="challengePromise"
>
{{ "readKey" | i18n }}
</button>
<ng-container *ngIf="$any(readKeyBtn).loading">
<i class="bwi bwi-spinner bwi-spin text-muted" aria-hidden="true"></i>
</ng-container>
<ng-container *ngIf="!$any(readKeyBtn).loading">
<ng-container *ngIf="webAuthnListening">
<i class="bwi bwi-spinner bwi-spin text-muted" aria-hidden="true"></i>
{{ "twoFactorU2fWaiting" | i18n }}...
</ng-container>
<ng-container *ngIf="webAuthnResponse">
<i class="bwi bwi-check-circle text-success" aria-hidden="true"></i>
{{ "twoFactorU2fClickSave" | i18n }}
</ng-container>
<ng-container *ngIf="webAuthnError">
<i class="bwi bwi-exclamation-triangle text-danger" aria-hidden="true"></i>
{{ "twoFactorU2fProblemReadingTryAgain" | i18n }}
</ng-container> </ng-container>
</ng-container> </ng-container>
</div> <ng-container *ngIf="keysConfiguredCount > 1 && k.configured">
<div class="modal-footer">
<button
type="submit"
class="btn btn-primary"
[disabled]="form.loading || !webAuthnResponse"
>
<i <i
class="bwi bwi-spinner bwi-spin" class="bwi bwi-spin bwi-spinner tw-text-muted bwi-fw"
*ngIf="form.loading"
title="{{ 'loading' | i18n }}" title="{{ 'loading' | i18n }}"
*ngIf="$any(removeKeyBtn).loading"
aria-hidden="true" aria-hidden="true"
></i> ></i>
<span *ngIf="!form.loading">{{ "save" | i18n }}</span> -
</button> <a href="#" appStopClick (click)="remove(k)">{{ "remove" | i18n }}</a>
<button </ng-container>
#disableBtn </li>
type="button" </ul>
class="btn btn-outline-secondary btn-submit" <hr />
[disabled]="$any(disableBtn).loading" <p bitTypography="body1">{{ "twoFactorWebAuthnAdd" | i18n }}:</p>
(click)="disable()" <ol bitTypography="body1">
*ngIf="enabled" <li>{{ "twoFactorU2fGiveName" | i18n }}</li>
> <li>{{ "twoFactorU2fPlugInReadKey" | i18n }}</li>
<i <li>{{ "twoFactorU2fTouchButton" | i18n }}</li>
class="bwi bwi-spinner bwi-spin" <li>{{ "twoFactorU2fSaveForm" | i18n }}</li>
title="{{ 'loading' | i18n }}" </ol>
aria-hidden="true" <div class="tw-flex">
></i> <bit-form-field class="tw-basis-1/2">
<span>{{ "disableAllKeys" | i18n }}</span> <bit-label>{{ "name" | i18n }}</bit-label>
</button> <input bitInput type="text" formControlName="name" />
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal"> </bit-form-field>
{{ "close" | i18n }} </div>
</button> <button
</div> bitButton
</form> bitFormButton
</div> type="button"
</div> [bitAction]="readKey"
</div> buttonType="secondary"
[disabled]="$any(readKeyBtn).loading || webAuthnListening || !keyIdAvailable"
class="tw-mr-2"
#readKeyBtn
>
{{ "readKey" | i18n }}
</button>
<ng-container *ngIf="$any(readKeyBtn).loading">
<i class="bwi bwi-spinner bwi-spin tw-text-muted" aria-hidden="true"></i>
</ng-container>
<ng-container *ngIf="!$any(readKeyBtn).loading">
<ng-container *ngIf="webAuthnListening">
<i class="bwi bwi-spinner bwi-spin tw-text-muted" aria-hidden="true"></i>
{{ "twoFactorU2fWaiting" | i18n }}...
</ng-container>
<ng-container *ngIf="webAuthnResponse">
<i class="bwi bwi-check-circle tw-text-success" aria-hidden="true"></i>
{{ "twoFactorU2fClickSave" | i18n }}
</ng-container>
<ng-container *ngIf="webAuthnError">
<i class="bwi bwi-exclamation-triangle tw-text-danger" aria-hidden="true"></i>
{{ "twoFactorU2fProblemReadingTryAgain" | i18n }}
</ng-container>
</ng-container>
</ng-container>
<ng-container bitDialogFooter>
<button
bitButton
bitFormButton
type="submit"
buttonType="primary"
[disabled]="!webAuthnResponse"
>
{{ "save" | i18n }}
</button>
<button
bitButton
bitFormButton
*ngIf="enabled"
type="button"
buttonType="secondary"
[bitAction]="disable"
>
{{ "disableAllKeys" | i18n }}
</button>
<button bitButton bitFormButton type="button" buttonType="secondary" bitDialogClose>
{{ "close" | i18n }}
</button>
</ng-container>
</bit-dialog>
</form>

View File

@ -1,4 +1,6 @@
import { Component, NgZone } from "@angular/core"; import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
import { Component, EventEmitter, Inject, NgZone, Output } from "@angular/core";
import { 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";
@ -31,6 +33,7 @@ interface Key {
templateUrl: "two-factor-webauthn.component.html", templateUrl: "two-factor-webauthn.component.html",
}) })
export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent { export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent {
@Output() onChangeStatus = new EventEmitter<boolean>();
type = TwoFactorProviderType.WebAuthn; type = TwoFactorProviderType.WebAuthn;
name: string; name: string;
keys: Key[]; keys: Key[];
@ -44,7 +47,13 @@ export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent {
override componentName = "app-two-factor-webauthn"; override componentName = "app-two-factor-webauthn";
protected formGroup = new FormGroup({
name: new FormControl({ value: "", disabled: !this.keyIdAvailable }),
});
constructor( constructor(
@Inject(DIALOG_DATA) protected data: AuthResponse<TwoFactorWebAuthnResponse>,
private dialogRef: DialogRef,
apiService: ApiService, apiService: ApiService,
i18nService: I18nService, i18nService: I18nService,
platformUtilsService: PlatformUtilsService, platformUtilsService: PlatformUtilsService,
@ -61,6 +70,7 @@ export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent {
userVerificationService, userVerificationService,
dialogService, dialogService,
); );
this.auth(data);
} }
auth(authResponse: AuthResponse<TwoFactorWebAuthnResponse>) { auth(authResponse: AuthResponse<TwoFactorWebAuthnResponse>) {
@ -68,7 +78,7 @@ export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent {
this.processResponse(authResponse.response); this.processResponse(authResponse.response);
} }
async submit() { submit = async () => {
if (this.webAuthnResponse == null || this.keyIdAvailable == null) { if (this.webAuthnResponse == null || this.keyIdAvailable == null) {
// Should never happen. // Should never happen.
return Promise.reject(); return Promise.reject();
@ -76,16 +86,28 @@ export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent {
const request = await this.buildRequestModel(UpdateTwoFactorWebAuthnRequest); const request = await this.buildRequestModel(UpdateTwoFactorWebAuthnRequest);
request.deviceResponse = this.webAuthnResponse; request.deviceResponse = this.webAuthnResponse;
request.id = this.keyIdAvailable; request.id = this.keyIdAvailable;
request.name = this.name; request.name = this.formGroup.value.name;
return this.enableWebAuth(request);
};
private enableWebAuth(request: any) {
return super.enable(async () => { return super.enable(async () => {
this.formPromise = this.apiService.putTwoFactorWebAuthn(request); this.formPromise = this.apiService.putTwoFactorWebAuthn(request);
const response = await this.formPromise; const response = await this.formPromise;
await this.processResponse(response); this.processResponse(response);
}); });
} }
disable() { disable = async () => {
await this.disableWebAuth();
if (!this.enabled) {
this.onChangeStatus.emit(this.enabled);
this.dialogRef.close();
}
};
private async disableWebAuth() {
return super.disable(this.formPromise); return super.disable(this.formPromise);
} }
@ -116,19 +138,15 @@ export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent {
} }
} }
async readKey() { readKey = async () => {
if (this.keyIdAvailable == null) { if (this.keyIdAvailable == null) {
return; return;
} }
const request = await this.buildRequestModel(SecretVerificationRequest); const request = await this.buildRequestModel(SecretVerificationRequest);
try { this.challengePromise = this.apiService.getTwoFactorWebAuthnChallenge(request);
this.challengePromise = this.apiService.getTwoFactorWebAuthnChallenge(request); const challenge = await this.challengePromise;
const challenge = await this.challengePromise; this.readDevice(challenge);
this.readDevice(challenge); };
} catch (e) {
this.logService.error(e);
}
}
private readDevice(webAuthnChallenge: ChallengeResponse) { private readDevice(webAuthnChallenge: ChallengeResponse) {
// eslint-disable-next-line // eslint-disable-next-line
@ -164,7 +182,8 @@ export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent {
this.resetWebAuthn(); this.resetWebAuthn();
this.keys = []; this.keys = [];
this.keyIdAvailable = null; this.keyIdAvailable = null;
this.name = null; this.formGroup.get("name").enable();
this.formGroup.get("name").setValue(null);
this.keysConfiguredCount = 0; this.keysConfiguredCount = 0;
for (let i = 1; i <= 5; i++) { for (let i = 1; i <= 5; i++) {
if (response.keys != null) { if (response.keys != null) {
@ -187,5 +206,13 @@ export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent {
} }
} }
this.enabled = response.enabled; this.enabled = response.enabled;
this.onChangeStatus.emit(this.enabled);
}
static open(
dialogService: DialogService,
config: DialogConfig<AuthResponse<TwoFactorWebAuthnResponse>>,
) {
return dialogService.open<boolean>(TwoFactorWebAuthnComponent, config);
} }
} }