diff --git a/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts b/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts index b0dd6c63a77..0e00c31a69e 100644 --- a/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts +++ b/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts @@ -1,7 +1,8 @@ +import { DialogRef } from "@angular/cdk/dialog"; import { Component } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { concatMap, takeUntil, map, lastValueFrom } from "rxjs"; -import { tap } from "rxjs/operators"; +import { first, tap } from "rxjs/operators"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -64,6 +65,9 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent { } async manage(type: TwoFactorProviderType) { + // clear any existing subscriptions before creating a new one + this.twoFactorSetupSubscription?.unsubscribe(); + switch (type) { case TwoFactorProviderType.OrganizationDuo: { const twoFactorVerifyDialogRef = TwoFactorVerifyComponent.open(this.dialogService, { @@ -75,9 +79,18 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent { 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: DialogRef = TwoFactorDuoComponent.open(this.dialogService, { + data: { + authResponse: result, + organizationId: this.organizationId, + }, + }); + this.twoFactorSetupSubscription = duoComp.componentInstance.onChangeStatus + .pipe(first(), takeUntil(this.destroy$)) + .subscribe((enabled: boolean) => { + duoComp.close(); + this.updateStatus(enabled, TwoFactorProviderType.OrganizationDuo); + }); break; } diff --git a/apps/web/src/app/auth/settings/two-factor-duo.component.ts b/apps/web/src/app/auth/settings/two-factor-duo.component.ts index c3ed10ed87b..7505fe13b39 100644 --- a/apps/web/src/app/auth/settings/two-factor-duo.component.ts +++ b/apps/web/src/app/auth/settings/two-factor-duo.component.ts @@ -31,7 +31,7 @@ export class TwoFactorDuoComponent extends TwoFactorBaseComponent { override componentName = "app-two-factor-duo"; constructor( - @Inject(DIALOG_DATA) protected data: AuthResponse, + @Inject(DIALOG_DATA) protected data: TwoFactorDuoComponentConfig, apiService: ApiService, i18nService: I18nService, platformUtilsService: PlatformUtilsService, @@ -71,8 +71,17 @@ export class TwoFactorDuoComponent extends TwoFactorBaseComponent { } async ngOnInit() { - super.auth(this.data); - this.processResponse(this.data.response); + if (!this.data?.authResponse) { + throw Error("TwoFactorDuoComponent requires a TwoFactorDuoResponse to initialize"); + } + + super.auth(this.data.authResponse); + this.processResponse(this.data.authResponse.response); + + if (this.data.organizationId) { + this.type = TwoFactorProviderType.OrganizationDuo; + this.organizationId = this.data.organizationId; + } } submit = async () => { @@ -124,8 +133,13 @@ export class TwoFactorDuoComponent extends TwoFactorBaseComponent { */ static open = ( dialogService: DialogService, - config: DialogConfig>, + config: DialogConfig, ) => { return dialogService.open(TwoFactorDuoComponent, config); }; } + +type TwoFactorDuoComponentConfig = { + authResponse: AuthResponse; + organizationId?: string; +}; diff --git a/apps/web/src/app/auth/settings/two-factor-setup.component.ts b/apps/web/src/app/auth/settings/two-factor-setup.component.ts index d60153230b0..6f80f17bd24 100644 --- a/apps/web/src/app/auth/settings/two-factor-setup.component.ts +++ b/apps/web/src/app/auth/settings/two-factor-setup.component.ts @@ -1,6 +1,14 @@ import { DialogRef } from "@angular/cdk/dialog"; import { Component, OnDestroy, OnInit, Type, ViewChild, ViewContainerRef } from "@angular/core"; -import { firstValueFrom, lastValueFrom, Observable, Subject, takeUntil } from "rxjs"; +import { + first, + firstValueFrom, + lastValueFrom, + Observable, + Subject, + Subscription, + takeUntil, +} from "rxjs"; import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; import { ModalService } from "@bitwarden/angular/services/modal.service"; @@ -36,9 +44,6 @@ import { TwoFactorYubiKeyComponent } from "./two-factor-yubikey.component"; export class TwoFactorSetupComponent implements OnInit, OnDestroy { @ViewChild("yubikeyTemplate", { read: ViewContainerRef, static: true }) yubikeyModalRef: ViewContainerRef; - @ViewChild("duoTemplate", { read: ViewContainerRef, static: true }) duoModalRef: ViewContainerRef; - @ViewChild("emailTemplate", { read: ViewContainerRef, static: true }) - emailModalRef: ViewContainerRef; organizationId: string; organization: Organization; @@ -53,6 +58,7 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { protected destroy$ = new Subject(); private twoFactorAuthPolicyAppliesToActiveUser: boolean; + protected twoFactorSetupSubscription: Subscription; constructor( protected dialogService: DialogService, @@ -126,6 +132,9 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { } async manage(type: TwoFactorProviderType) { + // clear any existing subscriptions before creating a new one + this.twoFactorSetupSubscription?.unsubscribe(); + switch (type) { case TwoFactorProviderType.Authenticator: { const result: AuthResponse = @@ -137,9 +146,12 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { this.dialogService, { data: result }, ); - authComp.componentInstance.onChangeStatus.subscribe((enabled: boolean) => { - this.updateStatus(enabled, TwoFactorProviderType.Authenticator); - }); + this.twoFactorSetupSubscription = authComp.componentInstance.onChangeStatus + .pipe(first(), takeUntil(this.destroy$)) + .subscribe((enabled: boolean) => { + authComp.close(); + this.updateStatus(enabled, TwoFactorProviderType.Authenticator); + }); break; } case TwoFactorProviderType.Yubikey: { @@ -150,9 +162,11 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { } const yubiComp = await this.openModal(this.yubikeyModalRef, TwoFactorYubiKeyComponent); yubiComp.auth(result); - yubiComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => { - this.updateStatus(enabled, TwoFactorProviderType.Yubikey); - }); + this.twoFactorSetupSubscription = yubiComp.onUpdated + .pipe(first(), takeUntil(this.destroy$)) + .subscribe((enabled: boolean) => { + this.updateStatus(enabled, TwoFactorProviderType.Yubikey); + }); break; } case TwoFactorProviderType.Duo: { @@ -162,11 +176,16 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { return; } const duoComp: DialogRef = TwoFactorDuoComponent.open(this.dialogService, { - data: result, - }); - duoComp.componentInstance.onChangeStatus.subscribe((enabled: boolean) => { - this.updateStatus(enabled, TwoFactorProviderType.Duo); + data: { + authResponse: result, + }, }); + this.twoFactorSetupSubscription = duoComp.componentInstance.onChangeStatus + .pipe(first(), takeUntil(this.destroy$)) + .subscribe((enabled: boolean) => { + duoComp.close(); + this.updateStatus(enabled, TwoFactorProviderType.Duo); + }); break; } case TwoFactorProviderType.Email: { @@ -175,12 +194,16 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { if (!result) { return; } - const authComp: DialogRef = TwoFactorEmailComponent.open(this.dialogService, { - data: result, - }); - authComp.componentInstance.onChangeStatus - .pipe(takeUntil(this.destroy$)) + const emailComp: DialogRef = TwoFactorEmailComponent.open( + this.dialogService, + { + data: result, + }, + ); + this.twoFactorSetupSubscription = emailComp.componentInstance.onChangeStatus + .pipe(first(), takeUntil(this.destroy$)) .subscribe((enabled: boolean) => { + emailComp.close(); this.updateStatus(enabled, TwoFactorProviderType.Email); }); break; @@ -195,9 +218,12 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { this.dialogService, { data: result }, ); - webAuthnComp.componentInstance.onChangeStatus.subscribe((enabled: boolean) => { - this.updateStatus(enabled, TwoFactorProviderType.WebAuthn); - }); + this.twoFactorSetupSubscription = webAuthnComp.componentInstance.onChangeStatus + .pipe(first(), takeUntil(this.destroy$)) + .subscribe((enabled: boolean) => { + webAuthnComp.close(); + this.updateStatus(enabled, TwoFactorProviderType.WebAuthn); + }); break; } default: