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:
parent
b6c46745a5
commit
7fffbc7938
|
@ -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;
|
||||||
|
|
|
@ -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">×</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>
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue