[PM-2056] update two factor duo dialog (#8976)

* migrating two factor duo component

* migrating two factor duo component

* two factor duo component migration

* two factor duo component migration

* removed null check from two-factor-setup

* cleanup duo changes

* remove ikey and skey references

* clean up

---------

Co-authored-by: Jake Fink <jfink@bitwarden.com>
This commit is contained in:
vinith-kovan 2024-06-26 23:25:42 +05:30 committed by GitHub
parent b38629b4c9
commit ab83e822f7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 144 additions and 139 deletions

View File

@ -1,6 +1,6 @@
import { Component } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { concatMap, takeUntil, map } from "rxjs";
import { concatMap, takeUntil, map, lastValueFrom } from "rxjs";
import { tap } from "rxjs/operators";
import { ModalService } from "@bitwarden/angular/services/modal.service";
@ -16,6 +16,7 @@ import { DialogService } from "@bitwarden/components";
import { TwoFactorDuoComponent } from "../../../auth/settings/two-factor-duo.component";
import { TwoFactorSetupComponent as BaseTwoFactorSetupComponent } from "../../../auth/settings/two-factor-setup.component";
import { TwoFactorVerifyComponent } from "../../../auth/settings/two-factor-verify.component";
@Component({
selector: "app-two-factor-setup",
@ -65,21 +66,19 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent {
async manage(type: TwoFactorProviderType) {
switch (type) {
case TwoFactorProviderType.OrganizationDuo: {
const result: AuthResponse<TwoFactorDuoResponse> = await this.callTwoFactorVerifyDialog(
TwoFactorProviderType.OrganizationDuo,
const twoFactorVerifyDialogRef = TwoFactorVerifyComponent.open(this.dialogService, {
data: { type: type, organizationId: this.organizationId },
});
const result: AuthResponse<TwoFactorDuoResponse> = await lastValueFrom(
twoFactorVerifyDialogRef.closed,
);
if (!result) {
return;
}
const duoComp = TwoFactorDuoComponent.open(this.dialogService, { data: result });
const enabled: boolean = await lastValueFrom(duoComp.closed);
this.updateStatus(enabled, TwoFactorProviderType.Duo);
const duoComp = await this.openModal(this.duoModalRef, TwoFactorDuoComponent);
duoComp.type = TwoFactorProviderType.OrganizationDuo;
duoComp.organizationId = this.organizationId;
duoComp.auth(result);
duoComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => {
this.updateStatus(enabled, TwoFactorProviderType.OrganizationDuo);
});
break;
}
default:

View File

@ -1,98 +1,58 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="2faDuoTitle">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title" id="2faDuoTitle">
{{ "twoStepLogin" | i18n }}
<small>Duo</small>
</h1>
<button
type="button"
class="close"
data-dismiss="modal"
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">
<ng-container *ngIf="enabled">
<app-callout type="success" title="{{ 'enabled' | i18n }}" icon="bwi bwi-check-circle">
{{ "twoStepLoginProviderEnabled" | i18n }}
</app-callout>
<img class="float-right ml-3 mfaType2" alt="Duo logo" />
<strong>{{ "twoFactorDuoClientId" | i18n }}:</strong> {{ clientId }}
<br />
<strong>{{ "twoFactorDuoClientSecret" | i18n }}:</strong> {{ clientSecret }}
<br />
<strong>{{ "twoFactorDuoApiHostname" | i18n }}:</strong> {{ host }}
</ng-container>
<ng-container *ngIf="!enabled">
<img class="float-right ml-3 mfaType2" alt="Duo logo" />
<p>{{ "twoFactorDuoDesc" | i18n }}</p>
<div class="form-group">
<label for="ClientId">{{ "twoFactorDuoClientId" | i18n }}</label>
<input
id="ClientId"
type="text"
name="ClientId"
class="form-control"
[(ngModel)]="clientId"
required
appInputVerbatim
/>
</div>
<div class="form-group">
<label for="ClientSecret">{{ "twoFactorDuoClientSecret" | i18n }}</label>
<input
id="ClientSecret"
type="password"
name="ClientSecret"
class="form-control"
[(ngModel)]="clientSecret"
required
appInputVerbatim
autocomplete="new-password"
/>
</div>
<div class="form-group">
<label for="host">{{ "twoFactorDuoApiHostname" | i18n }}</label>
<input
id="host"
type="text"
name="Host"
class="form-control"
[(ngModel)]="host"
placeholder="{{ 'ex' | i18n }} api-xxxxxxxx.duosecurity.com"
required
appInputVerbatim
/>
</div>
</ng-container>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i
class="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span *ngIf="!enabled">{{ "enable" | i18n }}</span>
<span *ngIf="enabled">{{ "disable" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "close" | i18n }}
</button>
</div>
</form>
</div>
</div>
</div>
<form [formGroup]="formGroup" [bitSubmit]="submit" *ngIf="authed" autocomplete="off">
<bit-dialog>
<span bitDialogTitle>
{{ "twoStepLogin" | i18n }}
<span bitTypography="body1">Duo</span>
</span>
<ng-container bitDialogContent>
<ng-container *ngIf="enabled">
<app-callout type="success" title="{{ 'enabled' | i18n }}" icon="bwi bwi-check-circle">
{{ "twoStepLoginProviderEnabled" | i18n }}
</app-callout>
<img class="tw-float-right tw-ml-3 mfaType2" alt="Duo logo" />
<strong>{{ "twoFactorDuoClientId" | i18n }}:</strong> {{ clientId }}
<br />
<strong>{{ "twoFactorDuoClientSecret" | i18n }}:</strong> {{ clientSecret }}
<br />
<strong>{{ "twoFactorDuoApiHostname" | i18n }}:</strong> {{ host }}
</ng-container>
<ng-container *ngIf="!enabled">
<img class="tw-float-right tw-ml-3 mfaType2" alt="Duo logo" />
<p bitTypography="body1">{{ "twoFactorDuoDesc" | i18n }}</p>
<bit-form-field>
<bit-label>{{ "twoFactorDuoClientId" | i18n }}</bit-label>
<input bitInput type="text" formControlName="clientId" appInputVerbatim />
</bit-form-field>
<bit-form-field>
<bit-label>{{ "twoFactorDuoClientSecret" | i18n }}</bit-label>
<input
bitInput
type="password"
formControlName="clientSecret"
appInputVerbatim
autocomplete="new-password"
/>
</bit-form-field>
<bit-form-field>
<bit-label>{{ "twoFactorDuoApiHostname" | i18n }}</bit-label>
<input
bitInput
type="text"
formControlName="host"
placeholder="{{ 'ex' | i18n }} api-xxxxxxxx.duosecurity.com"
appInputVerbatim
/>
</bit-form-field>
</ng-container>
</ng-container>
<ng-container bitDialogFooter>
<button bitButton bitFormButton type="submit" buttonType="primary">
<span *ngIf="!enabled">{{ "enable" | i18n }}</span>
<span *ngIf="enabled">{{ "disable" | i18n }}</span>
</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 } from "@angular/core";
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
import { Component, EventEmitter, Inject, Output } from "@angular/core";
import { FormBuilder, 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";
@ -18,21 +20,26 @@ import { TwoFactorBaseComponent } from "./two-factor-base.component";
templateUrl: "two-factor-duo.component.html",
})
export class TwoFactorDuoComponent extends TwoFactorBaseComponent {
type = TwoFactorProviderType.Duo;
clientId: string;
clientSecret: string;
host: string;
formPromise: Promise<TwoFactorDuoResponse>;
@Output() onChangeStatus: EventEmitter<boolean> = new EventEmitter();
type = TwoFactorProviderType.Duo;
formGroup = this.formBuilder.group({
clientId: ["", [Validators.required]],
clientSecret: ["", [Validators.required]],
host: ["", [Validators.required]],
});
override componentName = "app-two-factor-duo";
constructor(
@Inject(DIALOG_DATA) protected data: AuthResponse<TwoFactorDuoResponse>,
apiService: ApiService,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
logService: LogService,
userVerificationService: UserVerificationService,
dialogService: DialogService,
private formBuilder: FormBuilder,
private dialogRef: DialogRef,
) {
super(
apiService,
@ -44,43 +51,81 @@ export class TwoFactorDuoComponent extends TwoFactorBaseComponent {
);
}
auth(authResponse: AuthResponse<TwoFactorDuoResponse>) {
super.auth(authResponse);
this.processResponse(authResponse.response);
get clientId() {
return this.formGroup.get("clientId").value;
}
get clientSecret() {
return this.formGroup.get("clientSecret").value;
}
get host() {
return this.formGroup.get("host").value;
}
set clientId(value: string) {
this.formGroup.get("clientId").setValue(value);
}
set clientSecret(value: string) {
this.formGroup.get("clientSecret").setValue(value);
}
set host(value: string) {
this.formGroup.get("host").setValue(value);
}
submit() {
if (this.enabled) {
return super.disable(this.formPromise);
} else {
return this.enable();
}
async ngOnInit() {
super.auth(this.data);
this.processResponse(this.data.response);
}
submit = async () => {
this.formGroup.markAllAsTouched();
if (this.formGroup.invalid) {
return;
}
if (this.enabled) {
await this.disableMethod();
} else {
await this.enable();
}
this.onChangeStatus.emit(this.enabled);
};
protected async enable() {
const request = await this.buildRequestModel(UpdateTwoFactorDuoRequest);
request.clientId = this.clientId;
request.clientSecret = this.clientSecret;
request.host = this.host;
return super.enable(async () => {
if (this.organizationId != null) {
this.formPromise = this.apiService.putTwoFactorOrganizationDuo(
this.organizationId,
request,
);
} else {
this.formPromise = this.apiService.putTwoFactorDuo(request);
}
const response = await this.formPromise;
await this.processResponse(response);
});
let response: TwoFactorDuoResponse;
if (this.organizationId != null) {
response = await this.apiService.putTwoFactorOrganizationDuo(this.organizationId, request);
} else {
response = await this.apiService.putTwoFactorDuo(request);
}
this.processResponse(response);
this.onUpdated.emit(true);
}
onClose = () => {
this.dialogRef.close(this.enabled);
};
private processResponse(response: TwoFactorDuoResponse) {
this.clientId = response.clientId;
this.clientSecret = response.clientSecret;
this.host = response.host;
this.enabled = response.enabled;
}
/**
* Strongly typed helper to open a TwoFactorDuoComponentComponent
* @param dialogService Instance of the dialog service that will be used to open the dialog
* @param config Configuration for the dialog
*/
static open = (
dialogService: DialogService,
config: DialogConfig<AuthResponse<TwoFactorDuoResponse>>,
) => {
return dialogService.open<boolean>(TwoFactorDuoComponent, config);
};
}

View File

@ -161,9 +161,10 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
if (!result) {
return;
}
const duoComp = await this.openModal(this.duoModalRef, TwoFactorDuoComponent);
duoComp.auth(result);
duoComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => {
const duoComp: DialogRef<boolean, any> = TwoFactorDuoComponent.open(this.dialogService, {
data: result,
});
duoComp.componentInstance.onChangeStatus.subscribe((enabled: boolean) => {
this.updateStatus(enabled, TwoFactorProviderType.Duo);
});
break;