[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 { Component } from "@angular/core";
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from "@angular/router";
import { concatMap, takeUntil, map } from "rxjs"; import { concatMap, takeUntil, map, lastValueFrom } from "rxjs";
import { tap } from "rxjs/operators"; import { tap } from "rxjs/operators";
import { ModalService } from "@bitwarden/angular/services/modal.service"; 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 { TwoFactorDuoComponent } from "../../../auth/settings/two-factor-duo.component";
import { TwoFactorSetupComponent as BaseTwoFactorSetupComponent } from "../../../auth/settings/two-factor-setup.component"; import { TwoFactorSetupComponent as BaseTwoFactorSetupComponent } from "../../../auth/settings/two-factor-setup.component";
import { TwoFactorVerifyComponent } from "../../../auth/settings/two-factor-verify.component";
@Component({ @Component({
selector: "app-two-factor-setup", selector: "app-two-factor-setup",
@ -65,21 +66,19 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent {
async manage(type: TwoFactorProviderType) { async manage(type: TwoFactorProviderType) {
switch (type) { switch (type) {
case TwoFactorProviderType.OrganizationDuo: { case TwoFactorProviderType.OrganizationDuo: {
const result: AuthResponse<TwoFactorDuoResponse> = await this.callTwoFactorVerifyDialog( const twoFactorVerifyDialogRef = TwoFactorVerifyComponent.open(this.dialogService, {
TwoFactorProviderType.OrganizationDuo, data: { type: type, organizationId: this.organizationId },
});
const result: AuthResponse<TwoFactorDuoResponse> = await lastValueFrom(
twoFactorVerifyDialogRef.closed,
); );
if (!result) { if (!result) {
return; 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; break;
} }
default: default:

View File

@ -1,98 +1,58 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="2faDuoTitle"> <form [formGroup]="formGroup" [bitSubmit]="submit" *ngIf="authed" autocomplete="off">
<div class="modal-dialog" role="document"> <bit-dialog>
<div class="modal-content"> <span bitDialogTitle>
<div class="modal-header"> {{ "twoStepLogin" | i18n }}
<h1 class="modal-title" id="2faDuoTitle"> <span bitTypography="body1">Duo</span>
{{ "twoStepLogin" | i18n }} </span>
<small>Duo</small> <ng-container bitDialogContent>
</h1> <ng-container *ngIf="enabled">
<button <app-callout type="success" title="{{ 'enabled' | i18n }}" icon="bwi bwi-check-circle">
type="button" {{ "twoStepLoginProviderEnabled" | i18n }}
class="close" </app-callout>
data-dismiss="modal" <img class="tw-float-right tw-ml-3 mfaType2" alt="Duo logo" />
appA11yTitle="{{ 'close' | i18n }}" <strong>{{ "twoFactorDuoClientId" | i18n }}:</strong> {{ clientId }}
> <br />
<span aria-hidden="true">&times;</span> <strong>{{ "twoFactorDuoClientSecret" | i18n }}:</strong> {{ clientSecret }}
</button> <br />
</div> <strong>{{ "twoFactorDuoApiHostname" | i18n }}:</strong> {{ host }}
<form </ng-container>
#form <ng-container *ngIf="!enabled">
(ngSubmit)="submit()" <img class="tw-float-right tw-ml-3 mfaType2" alt="Duo logo" />
[appApiAction]="formPromise" <p bitTypography="body1">{{ "twoFactorDuoDesc" | i18n }}</p>
ngNativeValidate <bit-form-field>
*ngIf="authed" <bit-label>{{ "twoFactorDuoClientId" | i18n }}</bit-label>
autocomplete="off" <input bitInput type="text" formControlName="clientId" appInputVerbatim />
> </bit-form-field>
<div class="modal-body"> <bit-form-field>
<ng-container *ngIf="enabled"> <bit-label>{{ "twoFactorDuoClientSecret" | i18n }}</bit-label>
<app-callout type="success" title="{{ 'enabled' | i18n }}" icon="bwi bwi-check-circle"> <input
{{ "twoStepLoginProviderEnabled" | i18n }} bitInput
</app-callout> type="password"
<img class="float-right ml-3 mfaType2" alt="Duo logo" /> formControlName="clientSecret"
<strong>{{ "twoFactorDuoClientId" | i18n }}:</strong> {{ clientId }} appInputVerbatim
<br /> autocomplete="new-password"
<strong>{{ "twoFactorDuoClientSecret" | i18n }}:</strong> {{ clientSecret }} />
<br /> </bit-form-field>
<strong>{{ "twoFactorDuoApiHostname" | i18n }}:</strong> {{ host }} <bit-form-field>
</ng-container> <bit-label>{{ "twoFactorDuoApiHostname" | i18n }}</bit-label>
<ng-container *ngIf="!enabled"> <input
<img class="float-right ml-3 mfaType2" alt="Duo logo" /> bitInput
<p>{{ "twoFactorDuoDesc" | i18n }}</p> type="text"
<div class="form-group"> formControlName="host"
<label for="ClientId">{{ "twoFactorDuoClientId" | i18n }}</label> placeholder="{{ 'ex' | i18n }} api-xxxxxxxx.duosecurity.com"
<input appInputVerbatim
id="ClientId" />
type="text" </bit-form-field>
name="ClientId" </ng-container>
class="form-control" </ng-container>
[(ngModel)]="clientId" <ng-container bitDialogFooter>
required <button bitButton bitFormButton type="submit" buttonType="primary">
appInputVerbatim <span *ngIf="!enabled">{{ "enable" | i18n }}</span>
/> <span *ngIf="enabled">{{ "disable" | i18n }}</span>
</div> </button>
<div class="form-group"> <button bitButton bitFormButton type="button" buttonType="secondary" bitDialogClose>
<label for="ClientSecret">{{ "twoFactorDuoClientSecret" | i18n }}</label> {{ "close" | i18n }}
<input </button>
id="ClientSecret" </ng-container>
type="password" </bit-dialog>
name="ClientSecret" </form>
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>

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 { 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";
@ -18,21 +20,26 @@ import { TwoFactorBaseComponent } from "./two-factor-base.component";
templateUrl: "two-factor-duo.component.html", templateUrl: "two-factor-duo.component.html",
}) })
export class TwoFactorDuoComponent extends TwoFactorBaseComponent { export class TwoFactorDuoComponent extends TwoFactorBaseComponent {
type = TwoFactorProviderType.Duo; @Output() onChangeStatus: EventEmitter<boolean> = new EventEmitter();
clientId: string;
clientSecret: string;
host: string;
formPromise: Promise<TwoFactorDuoResponse>;
type = TwoFactorProviderType.Duo;
formGroup = this.formBuilder.group({
clientId: ["", [Validators.required]],
clientSecret: ["", [Validators.required]],
host: ["", [Validators.required]],
});
override componentName = "app-two-factor-duo"; override componentName = "app-two-factor-duo";
constructor( constructor(
@Inject(DIALOG_DATA) protected data: AuthResponse<TwoFactorDuoResponse>,
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 dialogRef: DialogRef,
) { ) {
super( super(
apiService, apiService,
@ -44,43 +51,81 @@ export class TwoFactorDuoComponent extends TwoFactorBaseComponent {
); );
} }
auth(authResponse: AuthResponse<TwoFactorDuoResponse>) { get clientId() {
super.auth(authResponse); return this.formGroup.get("clientId").value;
this.processResponse(authResponse.response); }
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() { async ngOnInit() {
if (this.enabled) { super.auth(this.data);
return super.disable(this.formPromise); this.processResponse(this.data.response);
} else {
return this.enable();
}
} }
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() { protected async enable() {
const request = await this.buildRequestModel(UpdateTwoFactorDuoRequest); const request = await this.buildRequestModel(UpdateTwoFactorDuoRequest);
request.clientId = this.clientId; request.clientId = this.clientId;
request.clientSecret = this.clientSecret; request.clientSecret = this.clientSecret;
request.host = this.host; request.host = this.host;
return super.enable(async () => { let response: TwoFactorDuoResponse;
if (this.organizationId != null) {
this.formPromise = this.apiService.putTwoFactorOrganizationDuo( if (this.organizationId != null) {
this.organizationId, response = await this.apiService.putTwoFactorOrganizationDuo(this.organizationId, request);
request, } else {
); response = await this.apiService.putTwoFactorDuo(request);
} else { }
this.formPromise = this.apiService.putTwoFactorDuo(request);
} this.processResponse(response);
const response = await this.formPromise; this.onUpdated.emit(true);
await this.processResponse(response);
});
} }
onClose = () => {
this.dialogRef.close(this.enabled);
};
private processResponse(response: TwoFactorDuoResponse) { private processResponse(response: TwoFactorDuoResponse) {
this.clientId = response.clientId; this.clientId = response.clientId;
this.clientSecret = response.clientSecret; this.clientSecret = response.clientSecret;
this.host = response.host; this.host = response.host;
this.enabled = response.enabled; 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) { if (!result) {
return; return;
} }
const duoComp = await this.openModal(this.duoModalRef, TwoFactorDuoComponent); const duoComp: DialogRef<boolean, any> = TwoFactorDuoComponent.open(this.dialogService, {
duoComp.auth(result); data: result,
duoComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => { });
duoComp.componentInstance.onChangeStatus.subscribe((enabled: boolean) => {
this.updateStatus(enabled, TwoFactorProviderType.Duo); this.updateStatus(enabled, TwoFactorProviderType.Duo);
}); });
break; break;