diff --git a/apps/browser/src/auth/popup/two-factor-auth-duo.component.ts b/apps/browser/src/auth/popup/two-factor-auth-duo.component.ts deleted file mode 100644 index 53aedc7a5f3..00000000000 --- a/apps/browser/src/auth/popup/two-factor-auth-duo.component.ts +++ /dev/null @@ -1,122 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { DialogModule } from "@angular/cdk/dialog"; -import { CommonModule } from "@angular/common"; -import { Component, OnDestroy, OnInit } from "@angular/core"; -import { ReactiveFormsModule, FormsModule } from "@angular/forms"; -import { Subject, Subscription, filter, firstValueFrom, takeUntil } from "rxjs"; - -import { TwoFactorAuthDuoComponent as TwoFactorAuthDuoBaseComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth-duo.component"; -import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { ToastService } from "@bitwarden/components"; - -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { AsyncActionsModule } from "../../../../../libs/components/src/async-actions"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { ButtonModule } from "../../../../../libs/components/src/button"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { FormFieldModule } from "../../../../../libs/components/src/form-field"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { LinkModule } from "../../../../../libs/components/src/link"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { I18nPipe } from "../../../../../libs/components/src/shared/i18n.pipe"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { TypographyModule } from "../../../../../libs/components/src/typography"; -import { ZonedMessageListenerService } from "../../platform/browser/zoned-message-listener.service"; - -@Component({ - standalone: true, - selector: "app-two-factor-auth-duo", - templateUrl: - "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-duo.component.html", - imports: [ - CommonModule, - JslibModule, - DialogModule, - ButtonModule, - LinkModule, - TypographyModule, - ReactiveFormsModule, - FormFieldModule, - AsyncActionsModule, - FormsModule, - ], - providers: [I18nPipe], -}) -export class TwoFactorAuthDuoComponent - extends TwoFactorAuthDuoBaseComponent - implements OnInit, OnDestroy -{ - private destroy$ = new Subject(); - duoResultSubscription: Subscription; - - constructor( - protected i18nService: I18nService, - protected platformUtilsService: PlatformUtilsService, - private browserMessagingApi: ZonedMessageListenerService, - private environmentService: EnvironmentService, - toastService: ToastService, - ) { - super(i18nService, platformUtilsService, toastService); - } - - async ngOnInit(): Promise { - await super.ngOnInit(); - } - - async ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); - } - - protected override setupDuoResultListener() { - if (!this.duoResultSubscription) { - this.duoResultSubscription = this.browserMessagingApi - .messageListener$() - .pipe( - filter((msg: any) => msg.command === "duoResult"), - takeUntil(this.destroy$), - ) - .subscribe((msg: { command: string; code: string; state: string }) => { - this.token.emit(msg.code + "|" + msg.state); - }); - } - } - - override async launchDuoFrameless() { - if (this.duoFramelessUrl === null) { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t("duoHealthCheckResultsInNullAuthUrlError"), - }); - return; - } - const duoHandOffMessage = { - title: this.i18nService.t("youSuccessfullyLoggedIn"), - message: this.i18nService.t("youMayCloseThisWindow"), - isCountdown: false, - }; - - // we're using the connector here as a way to set a cookie with translations - // before continuing to the duo frameless url - const env = await firstValueFrom(this.environmentService.environment$); - const launchUrl = - env.getWebVaultUrl() + - "/duo-redirect-connector.html" + - "?duoFramelessUrl=" + - encodeURIComponent(this.duoFramelessUrl) + - "&handOffMessage=" + - encodeURIComponent(JSON.stringify(duoHandOffMessage)); - this.platformUtilsService.launchUri(launchUrl); - } -} diff --git a/apps/browser/src/auth/popup/two-factor-auth-email.component.ts b/apps/browser/src/auth/popup/two-factor-auth-email.component.ts deleted file mode 100644 index 723152adfab..00000000000 --- a/apps/browser/src/auth/popup/two-factor-auth-email.component.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { DialogModule } from "@angular/cdk/dialog"; -import { CommonModule } from "@angular/common"; -import { Component, OnInit, inject } from "@angular/core"; -import { ReactiveFormsModule, FormsModule } from "@angular/forms"; - -import { TwoFactorAuthEmailComponent as TwoFactorAuthEmailBaseComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth-email.component"; -import { JslibModule } from "@bitwarden/angular/jslib.module"; - -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { AsyncActionsModule } from "../../../../../libs/components/src/async-actions"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { ButtonModule } from "../../../../../libs/components/src/button"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { DialogService } from "../../../../../libs/components/src/dialog"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { FormFieldModule } from "../../../../../libs/components/src/form-field"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { LinkModule } from "../../../../../libs/components/src/link"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { I18nPipe } from "../../../../../libs/components/src/shared/i18n.pipe"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { TypographyModule } from "../../../../../libs/components/src/typography"; -import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; - -@Component({ - standalone: true, - selector: "app-two-factor-auth-email", - templateUrl: - "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component.html", - imports: [ - CommonModule, - JslibModule, - DialogModule, - ButtonModule, - LinkModule, - TypographyModule, - ReactiveFormsModule, - FormFieldModule, - AsyncActionsModule, - FormsModule, - ], - providers: [I18nPipe], -}) -export class TwoFactorAuthEmailComponent extends TwoFactorAuthEmailBaseComponent implements OnInit { - private dialogService = inject(DialogService); - - async ngOnInit(): Promise { - if (BrowserPopupUtils.inPopup(window)) { - const confirmed = await this.dialogService.openSimpleDialog({ - title: { key: "warning" }, - content: { key: "popup2faCloseMessage" }, - type: "warning", - }); - if (confirmed) { - await BrowserPopupUtils.openCurrentPagePopout(window); - return; - } - } - - await super.ngOnInit(); - } -} diff --git a/apps/browser/src/auth/popup/two-factor-auth.component.ts b/apps/browser/src/auth/popup/two-factor-auth.component.ts deleted file mode 100644 index f22bbbe202c..00000000000 --- a/apps/browser/src/auth/popup/two-factor-auth.component.ts +++ /dev/null @@ -1,170 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { CommonModule } from "@angular/common"; -import { Component, Inject, OnDestroy, OnInit } from "@angular/core"; -import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; -import { ActivatedRoute, Router, RouterLink } from "@angular/router"; - -import { TwoFactorAuthAuthenticatorComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth-authenticator.component"; -import { TwoFactorAuthWebAuthnComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth-webauthn.component"; -import { TwoFactorAuthYubikeyComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth-yubikey.component"; -import { TwoFactorAuthComponent as BaseTwoFactorAuthComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth.component"; -import { TwoFactorOptionsComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-options.component"; -import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; -import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; -import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; -import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; -import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { SyncService } from "@bitwarden/common/platform/sync"; -import { - ButtonModule, - FormFieldModule, - AsyncActionsModule, - CheckboxModule, - DialogModule, - LinkModule, - TypographyModule, - DialogService, - ToastService, -} from "@bitwarden/components"; - -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { - LoginStrategyServiceAbstraction, - LoginEmailServiceAbstraction, - UserDecryptionOptionsServiceAbstraction, -} from "../../../../../libs/auth/src/common/abstractions"; -import { BrowserApi } from "../../platform/browser/browser-api"; -import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; - -import { TwoFactorAuthDuoComponent } from "./two-factor-auth-duo.component"; -import { TwoFactorAuthEmailComponent } from "./two-factor-auth-email.component"; - -@Component({ - standalone: true, - templateUrl: - "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.html", - selector: "app-two-factor-auth", - imports: [ - CommonModule, - JslibModule, - DialogModule, - ButtonModule, - LinkModule, - TypographyModule, - ReactiveFormsModule, - FormFieldModule, - AsyncActionsModule, - RouterLink, - CheckboxModule, - TwoFactorOptionsComponent, - TwoFactorAuthEmailComponent, - TwoFactorAuthAuthenticatorComponent, - TwoFactorAuthYubikeyComponent, - TwoFactorAuthDuoComponent, - TwoFactorAuthWebAuthnComponent, - ], - providers: [I18nPipe], -}) -export class TwoFactorAuthComponent - extends BaseTwoFactorAuthComponent - implements OnInit, OnDestroy -{ - constructor( - protected loginStrategyService: LoginStrategyServiceAbstraction, - protected router: Router, - i18nService: I18nService, - platformUtilsService: PlatformUtilsService, - environmentService: EnvironmentService, - dialogService: DialogService, - protected route: ActivatedRoute, - logService: LogService, - protected twoFactorService: TwoFactorService, - loginEmailService: LoginEmailServiceAbstraction, - userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, - protected ssoLoginService: SsoLoginServiceAbstraction, - protected configService: ConfigService, - masterPasswordService: InternalMasterPasswordServiceAbstraction, - accountService: AccountService, - formBuilder: FormBuilder, - @Inject(WINDOW) protected win: Window, - private syncService: SyncService, - private messagingService: MessagingService, - toastService: ToastService, - ) { - super( - loginStrategyService, - router, - i18nService, - platformUtilsService, - environmentService, - dialogService, - route, - logService, - twoFactorService, - loginEmailService, - userDecryptionOptionsService, - ssoLoginService, - configService, - masterPasswordService, - accountService, - formBuilder, - win, - toastService, - ); - this.onSuccessfulLoginTdeNavigate = async () => { - this.win.close(); - }; - this.onSuccessfulLoginNavigate = this.goAfterLogIn; - } - - async ngOnInit(): Promise { - await super.ngOnInit(); - - if (this.route.snapshot.paramMap.has("webAuthnResponse")) { - // WebAuthn fallback response - this.selectedProviderType = TwoFactorProviderType.WebAuthn; - this.token = this.route.snapshot.paramMap.get("webAuthnResponse"); - this.onSuccessfulLogin = async () => { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.syncService.fullSync(true); - this.messagingService.send("reloadPopup"); - window.close(); - }; - this.remember = this.route.snapshot.paramMap.get("remember") === "true"; - await this.submit(); - return; - } - - if (await BrowserPopupUtils.inPopout(this.win)) { - this.selectedProviderType = TwoFactorProviderType.Email; - } - - // WebAuthn prompt appears inside the popup on linux, and requires a larger popup width - // than usual to avoid cutting off the dialog. - if (this.selectedProviderType === TwoFactorProviderType.WebAuthn && (await this.isLinux())) { - document.body.classList.add("linux-webauthn"); - } - } - - async ngOnDestroy() { - if (this.selectedProviderType === TwoFactorProviderType.WebAuthn && (await this.isLinux())) { - document.body.classList.remove("linux-webauthn"); - } - } - - async isLinux() { - return (await BrowserApi.getPlatformInfo()).os === "linux"; - } -} diff --git a/apps/browser/src/auth/popup/two-factor.component.html b/apps/browser/src/auth/popup/two-factor-v1.component.html similarity index 100% rename from apps/browser/src/auth/popup/two-factor.component.html rename to apps/browser/src/auth/popup/two-factor-v1.component.html diff --git a/apps/browser/src/auth/popup/two-factor.component.ts b/apps/browser/src/auth/popup/two-factor-v1.component.ts similarity index 97% rename from apps/browser/src/auth/popup/two-factor.component.ts rename to apps/browser/src/auth/popup/two-factor-v1.component.ts index a2f9cd9d0fc..1e7af489626 100644 --- a/apps/browser/src/auth/popup/two-factor.component.ts +++ b/apps/browser/src/auth/popup/two-factor-v1.component.ts @@ -5,7 +5,7 @@ import { ActivatedRoute, Router } from "@angular/router"; import { Subject, Subscription, firstValueFrom } from "rxjs"; import { filter, first, takeUntil } from "rxjs/operators"; -import { TwoFactorComponent as BaseTwoFactorComponent } from "@bitwarden/angular/auth/components/two-factor.component"; +import { TwoFactorComponentV1 as BaseTwoFactorComponent } from "@bitwarden/angular/auth/components/two-factor-v1.component"; import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; import { LoginStrategyServiceAbstraction, @@ -37,9 +37,9 @@ import { closeTwoFactorAuthPopout } from "./utils/auth-popout-window"; @Component({ selector: "app-two-factor", - templateUrl: "two-factor.component.html", + templateUrl: "two-factor-v1.component.html", }) -export class TwoFactorComponent extends BaseTwoFactorComponent implements OnInit, OnDestroy { +export class TwoFactorComponentV1 extends BaseTwoFactorComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); inPopout = BrowserPopupUtils.inPopout(window); diff --git a/apps/browser/src/auth/services/extension-two-factor-auth-component.service.ts b/apps/browser/src/auth/services/extension-two-factor-auth-component.service.ts new file mode 100644 index 00000000000..9faa845ad42 --- /dev/null +++ b/apps/browser/src/auth/services/extension-two-factor-auth-component.service.ts @@ -0,0 +1,56 @@ +import { + DefaultTwoFactorAuthComponentService, + TwoFactorAuthComponentService, +} from "@bitwarden/auth/angular"; +import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; + +import { BrowserApi } from "../../platform/browser/browser-api"; +import { closeTwoFactorAuthPopout } from "../popup/utils/auth-popout-window"; + +export class ExtensionTwoFactorAuthComponentService + extends DefaultTwoFactorAuthComponentService + implements TwoFactorAuthComponentService +{ + constructor(private window: Window) { + super(); + } + + shouldCheckForWebauthnResponseOnInit(): boolean { + return true; + } + + async extendPopupWidthIfRequired(selected2faProviderType: TwoFactorProviderType): Promise { + // WebAuthn prompt appears inside the popup on linux, and requires a larger popup width + // than usual to avoid cutting off the dialog. + const isLinux = await this.isLinux(); + if (selected2faProviderType === TwoFactorProviderType.WebAuthn && isLinux) { + document.body.classList.add("linux-webauthn"); + } + } + + removePopupWidthExtension(): void { + document.body.classList.remove("linux-webauthn"); + } + + closeWindow(): void { + this.window.close(); + } + + async handleSso2faFlowSuccess(): Promise { + // Force sidebars (FF && Opera) to reload while exempting current window + // because we are just going to close the current window. + BrowserApi.reloadOpenWindows(true); + + // We don't need this window anymore because the intent is for the user to be left + // on the web vault screen which tells them to continue in the browser extension (sidebar or popup) + // We don't want the user to be left with a floating, popped out extension which could be lost behind + // another window or minimized. Currently, the popped out window thinks it is active and wouldn't time out + // which leads to the security concern. So, we close the popped out extension to avoid this. + await closeTwoFactorAuthPopout(); + } + + private async isLinux(): Promise { + const platformInfo = await BrowserApi.getPlatformInfo(); + return platformInfo.os === "linux"; + } +} diff --git a/apps/browser/src/auth/services/extension-two-factor-auth-duo-component.service.ts b/apps/browser/src/auth/services/extension-two-factor-auth-duo-component.service.ts new file mode 100644 index 00000000000..87925185874 --- /dev/null +++ b/apps/browser/src/auth/services/extension-two-factor-auth-duo-component.service.ts @@ -0,0 +1,57 @@ +import { filter, firstValueFrom, map, Observable } from "rxjs"; + +import { Duo2faResult, TwoFactorAuthDuoComponentService } from "@bitwarden/auth/angular"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; + +import { ZonedMessageListenerService } from "../../platform/browser/zoned-message-listener.service"; + +interface Message { + command: string; + code: string; + state: string; +} + +export class ExtensionTwoFactorAuthDuoComponentService implements TwoFactorAuthDuoComponentService { + constructor( + private browserMessagingApi: ZonedMessageListenerService, + private environmentService: EnvironmentService, + private i18nService: I18nService, + private platformUtilsService: PlatformUtilsService, + ) {} + listenForDuo2faResult$(): Observable { + return this.browserMessagingApi.messageListener$().pipe( + filter((msg): msg is Message => { + return (msg as Message).command === "duoResult"; + }), + map((msg: Message) => { + return { + code: msg.code, + state: msg.state, + token: `${msg.code}|${msg.state}`, + } as Duo2faResult; + }), + ); + } + + async launchDuoFrameless(duoFramelessUrl: string): Promise { + const duoHandOffMessage = { + title: this.i18nService.t("youSuccessfullyLoggedIn"), + message: this.i18nService.t("youMayCloseThisWindow"), + isCountdown: false, + }; + + // we're using the connector here as a way to set a cookie with translations + // before continuing to the duo frameless url + const env = await firstValueFrom(this.environmentService.environment$); + const launchUrl = + env.getWebVaultUrl() + + "/duo-redirect-connector.html" + + "?duoFramelessUrl=" + + encodeURIComponent(duoFramelessUrl) + + "&handOffMessage=" + + encodeURIComponent(JSON.stringify(duoHandOffMessage)); + this.platformUtilsService.launchUri(launchUrl); + } +} diff --git a/apps/browser/src/auth/services/extension-two-factor-auth-email-component.service.ts b/apps/browser/src/auth/services/extension-two-factor-auth-email-component.service.ts new file mode 100644 index 00000000000..10d203d3a84 --- /dev/null +++ b/apps/browser/src/auth/services/extension-two-factor-auth-email-component.service.ts @@ -0,0 +1,33 @@ +import { + DefaultTwoFactorAuthEmailComponentService, + TwoFactorAuthEmailComponentService, +} from "@bitwarden/auth/angular"; +import { DialogService } from "@bitwarden/components"; + +import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; + +// TODO: popup state persistence should eventually remove the need for this service +export class ExtensionTwoFactorAuthEmailComponentService + extends DefaultTwoFactorAuthEmailComponentService + implements TwoFactorAuthEmailComponentService +{ + constructor( + private dialogService: DialogService, + private window: Window, + ) { + super(); + } + + async openPopoutIfApprovedForEmail2fa(): Promise { + if (BrowserPopupUtils.inPopup(this.window)) { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "warning" }, + content: { key: "popup2faCloseMessage" }, + type: "warning", + }); + if (confirmed) { + await BrowserPopupUtils.openCurrentPagePopout(this.window); + } + } + } +} diff --git a/apps/browser/src/auth/services/extension-two-factor-auth-webauthn-component.service.ts b/apps/browser/src/auth/services/extension-two-factor-auth-webauthn-component.service.ts new file mode 100644 index 00000000000..14a949fb086 --- /dev/null +++ b/apps/browser/src/auth/services/extension-two-factor-auth-webauthn-component.service.ts @@ -0,0 +1,24 @@ +import { + DefaultTwoFactorAuthWebAuthnComponentService, + TwoFactorAuthWebAuthnComponentService, +} from "@bitwarden/auth/angular"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; + +export class ExtensionTwoFactorAuthWebAuthnComponentService + extends DefaultTwoFactorAuthWebAuthnComponentService + implements TwoFactorAuthWebAuthnComponentService +{ + constructor(private platformUtilsService: PlatformUtilsService) { + super(); + } + + shouldOpenWebAuthnInNewTab(): boolean { + const isChrome = this.platformUtilsService.isChrome(); + if (isChrome) { + // Chrome now supports WebAuthn in the iframe in the extension now. + return false; + } + + return true; + } +} diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index d55bebfa0c3..eee0af9df6e 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -6,7 +6,6 @@ import { EnvironmentSelectorRouteData, ExtensionDefaultOverlayPosition, } from "@bitwarden/angular/auth/components/environment-selector.component"; -import { TwoFactorTimeoutComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth-expired.component"; import { unauthUiRefreshRedirect } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-redirect"; import { unauthUiRefreshSwap } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-route-swap"; import { @@ -17,7 +16,6 @@ import { unauthGuardFn, } from "@bitwarden/angular/auth/guards"; import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; -import { twofactorRefactorSwap } from "@bitwarden/angular/utils/two-factor-component-refactor-route-swap"; import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards"; import { AnonLayoutWrapperComponent, @@ -40,6 +38,9 @@ import { DevicesIcon, SsoComponent, TwoFactorTimeoutIcon, + TwoFactorAuthComponent, + TwoFactorTimeoutComponent, + TwoFactorAuthGuard, } from "@bitwarden/auth/angular"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { LockComponent } from "@bitwarden/key-management/angular"; @@ -66,9 +67,8 @@ import { RemovePasswordComponent } from "../auth/popup/remove-password.component import { SetPasswordComponent } from "../auth/popup/set-password.component"; import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component"; import { SsoComponentV1 } from "../auth/popup/sso-v1.component"; -import { TwoFactorAuthComponent } from "../auth/popup/two-factor-auth.component"; import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component"; -import { TwoFactorComponent } from "../auth/popup/two-factor.component"; +import { TwoFactorComponentV1 } from "../auth/popup/two-factor-v1.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; import { Fido2Component } from "../autofill/popup/fido2/fido2.component"; import { AutofillComponent } from "../autofill/popup/settings/autofill.component"; @@ -149,9 +149,9 @@ const routes: Routes = [ canActivate: [fido2AuthGuard], data: { elevation: 1 } satisfies RouteDataProperties, }, - ...twofactorRefactorSwap( - TwoFactorComponent, - AnonLayoutWrapperComponent, + ...unauthUiRefreshSwap( + TwoFactorComponentV1, + ExtensionAnonLayoutWrapperComponent, { path: "2fa", canActivate: [unauthGuardFn(unauthRouteOverrides)], @@ -159,7 +159,7 @@ const routes: Routes = [ }, { path: "2fa", - canActivate: [unauthGuardFn(unauthRouteOverrides)], + canActivate: [unauthGuardFn(unauthRouteOverrides), TwoFactorAuthGuard], data: { elevation: 1 } satisfies RouteDataProperties, children: [ { diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index 15a898aef53..ba90396bd5c 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -33,7 +33,7 @@ import { AccountSecurityComponent } from "../auth/popup/settings/account-securit import { VaultTimeoutInputComponent } from "../auth/popup/settings/vault-timeout-input.component"; import { SsoComponentV1 } from "../auth/popup/sso-v1.component"; import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component"; -import { TwoFactorComponent } from "../auth/popup/two-factor.component"; +import { TwoFactorComponentV1 } from "../auth/popup/two-factor-v1.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; import { AutofillComponent } from "../autofill/popup/settings/autofill.component"; import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component"; @@ -107,7 +107,7 @@ import "../platform/popup/locales"; SetPasswordComponent, SsoComponentV1, TabsV2Component, - TwoFactorComponent, + TwoFactorComponentV1, TwoFactorOptionsComponent, UpdateTempPasswordComponent, UserVerificationComponent, diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 24d82ab8b67..165e64d87c0 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -19,6 +19,7 @@ import { SYSTEM_THEME_OBSERVABLE, SafeInjectionToken, ENV_ADDITIONAL_REGIONS, + WINDOW, } from "@bitwarden/angular/services/injection-tokens"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; import { @@ -26,6 +27,10 @@ import { LoginComponentService, SsoComponentService, LoginDecryptionOptionsService, + TwoFactorAuthComponentService, + TwoFactorAuthEmailComponentService, + TwoFactorAuthDuoComponentService, + TwoFactorAuthWebAuthnComponentService, } from "@bitwarden/auth/angular"; import { LockService, LoginEmailService, PinServiceAbstraction } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -122,6 +127,10 @@ import { ExtensionAnonLayoutWrapperDataService } from "../../auth/popup/extensio import { ExtensionLoginComponentService } from "../../auth/popup/login/extension-login-component.service"; import { ExtensionSsoComponentService } from "../../auth/popup/login/extension-sso-component.service"; import { ExtensionLoginDecryptionOptionsService } from "../../auth/popup/login-decryption-options/extension-login-decryption-options.service"; +import { ExtensionTwoFactorAuthComponentService } from "../../auth/services/extension-two-factor-auth-component.service"; +import { ExtensionTwoFactorAuthDuoComponentService } from "../../auth/services/extension-two-factor-auth-duo-component.service"; +import { ExtensionTwoFactorAuthEmailComponentService } from "../../auth/services/extension-two-factor-auth-email-component.service"; +import { ExtensionTwoFactorAuthWebAuthnComponentService } from "../../auth/services/extension-two-factor-auth-webauthn-component.service"; import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service"; import AutofillService from "../../autofill/services/autofill.service"; import { InlineMenuFieldQualificationService } from "../../autofill/services/inline-menu-field-qualification.service"; @@ -130,6 +139,7 @@ import { ExtensionLockComponentService } from "../../key-management/lock/service import { BrowserApi } from "../../platform/browser/browser-api"; import { runInsideAngular } from "../../platform/browser/run-inside-angular.operator"; /* eslint-disable no-restricted-imports */ +import { ZonedMessageListenerService } from "../../platform/browser/zoned-message-listener.service"; import { ChromeMessageSender } from "../../platform/messaging/chrome-message.sender"; /* eslint-enable no-restricted-imports */ import { OffscreenDocumentService } from "../../platform/offscreen-document/abstractions/offscreen-document"; @@ -524,6 +534,31 @@ const safeProviders: SafeProvider[] = [ useClass: ExtensionLockComponentService, deps: [], }), + safeProvider({ + provide: TwoFactorAuthComponentService, + useClass: ExtensionTwoFactorAuthComponentService, + deps: [WINDOW], + }), + safeProvider({ + provide: TwoFactorAuthEmailComponentService, + useClass: ExtensionTwoFactorAuthEmailComponentService, + deps: [DialogService, WINDOW], + }), + safeProvider({ + provide: TwoFactorAuthWebAuthnComponentService, + useClass: ExtensionTwoFactorAuthWebAuthnComponentService, + deps: [PlatformUtilsService], + }), + safeProvider({ + provide: TwoFactorAuthDuoComponentService, + useClass: ExtensionTwoFactorAuthDuoComponentService, + deps: [ + ZonedMessageListenerService, + EnvironmentService, + I18nServiceAbstraction, + PlatformUtilsService, + ], + }), safeProvider({ provide: Fido2UserVerificationService, useClass: Fido2UserVerificationService, diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index e565681de93..709d03bfdd6 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -5,7 +5,6 @@ import { DesktopDefaultOverlayPosition, EnvironmentSelectorComponent, } from "@bitwarden/angular/auth/components/environment-selector.component"; -import { TwoFactorTimeoutComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth-expired.component"; import { unauthUiRefreshSwap } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-route-swap"; import { authGuard, @@ -15,7 +14,6 @@ import { unauthGuardFn, } from "@bitwarden/angular/auth/guards"; import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; -import { twofactorRefactorSwap } from "@bitwarden/angular/utils/two-factor-component-refactor-route-swap"; import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards"; import { AnonLayoutWrapperComponent, @@ -38,6 +36,9 @@ import { DevicesIcon, SsoComponent, TwoFactorTimeoutIcon, + TwoFactorAuthComponent, + TwoFactorTimeoutComponent, + TwoFactorAuthGuard, } from "@bitwarden/auth/angular"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { LockComponent } from "@bitwarden/key-management/angular"; @@ -57,8 +58,7 @@ import { RegisterComponent } from "../auth/register.component"; import { RemovePasswordComponent } from "../auth/remove-password.component"; import { SetPasswordComponent } from "../auth/set-password.component"; import { SsoComponentV1 } from "../auth/sso-v1.component"; -import { TwoFactorAuthComponent } from "../auth/two-factor-auth.component"; -import { TwoFactorComponent } from "../auth/two-factor.component"; +import { TwoFactorComponentV1 } from "../auth/two-factor-v1.component"; import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component"; import { VaultComponent } from "../vault/app/vault/vault.component"; @@ -80,8 +80,8 @@ const routes: Routes = [ children: [], // Children lets us have an empty component. canActivate: [redirectGuard({ loggedIn: "/vault", loggedOut: "/login", locked: "/lock" })], }, - ...twofactorRefactorSwap( - TwoFactorComponent, + ...unauthUiRefreshSwap( + TwoFactorComponentV1, AnonLayoutWrapperComponent, { path: "2fa", @@ -89,11 +89,11 @@ const routes: Routes = [ { path: "2fa", component: AnonLayoutWrapperComponent, + canActivate: [unauthGuardFn(), TwoFactorAuthGuard], children: [ { path: "", component: TwoFactorAuthComponent, - canActivate: [unauthGuardFn()], }, ], }, diff --git a/apps/desktop/src/app/app.module.ts b/apps/desktop/src/app/app.module.ts index ea0a2c4c546..7b087556f0f 100644 --- a/apps/desktop/src/app/app.module.ts +++ b/apps/desktop/src/app/app.module.ts @@ -20,7 +20,7 @@ import { RemovePasswordComponent } from "../auth/remove-password.component"; import { SetPasswordComponent } from "../auth/set-password.component"; import { SsoComponentV1 } from "../auth/sso-v1.component"; import { TwoFactorOptionsComponent } from "../auth/two-factor-options.component"; -import { TwoFactorComponent } from "../auth/two-factor.component"; +import { TwoFactorComponentV1 } from "../auth/two-factor-v1.component"; import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component"; import { SshAgentService } from "../platform/services/ssh-agent.service"; import { PremiumComponent } from "../vault/app/accounts/premium.component"; @@ -92,8 +92,8 @@ import { SendComponent } from "./tools/send/send.component"; SetPasswordComponent, SettingsComponent, ShareComponent, + TwoFactorComponentV1, SsoComponentV1, - TwoFactorComponent, TwoFactorOptionsComponent, UpdateTempPasswordComponent, VaultComponent, diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 8b890032443..fcb417cf317 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -26,6 +26,7 @@ import { SetPasswordJitService, SsoComponentService, DefaultSsoComponentService, + TwoFactorAuthDuoComponentService, } from "@bitwarden/auth/angular"; import { InternalUserDecryptionOptionsServiceAbstraction, @@ -99,6 +100,7 @@ import { LockComponentService } from "@bitwarden/key-management/angular"; import { DesktopLoginApprovalComponentService } from "../../auth/login/desktop-login-approval-component.service"; import { DesktopLoginComponentService } from "../../auth/login/desktop-login-component.service"; +import { DesktopTwoFactorAuthDuoComponentService } from "../../auth/services/desktop-two-factor-auth-duo-component.service"; import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service"; import { DesktopAutofillService } from "../../autofill/services/desktop-autofill.service"; import { DesktopFido2UserInterfaceService } from "../../autofill/services/desktop-fido2-user-interface.service"; @@ -388,6 +390,16 @@ const safeProviders: SafeProvider[] = [ ToastService, ], }), + safeProvider({ + provide: TwoFactorAuthDuoComponentService, + useClass: DesktopTwoFactorAuthDuoComponentService, + deps: [ + MessageListener, + EnvironmentService, + I18nServiceAbstraction, + PlatformUtilsServiceAbstraction, + ], + }), safeProvider({ provide: SdkClientFactory, useClass: flagEnabled("sdk") ? DefaultSdkClientFactory : NoopSdkClientFactory, diff --git a/apps/desktop/src/auth/services/desktop-two-factor-auth-duo-component.service.ts b/apps/desktop/src/auth/services/desktop-two-factor-auth-duo-component.service.ts new file mode 100644 index 00000000000..eef03ca5b53 --- /dev/null +++ b/apps/desktop/src/auth/services/desktop-two-factor-auth-duo-component.service.ts @@ -0,0 +1,56 @@ +import { firstValueFrom, map, Observable } from "rxjs"; + +import { TwoFactorAuthDuoComponentService, Duo2faResult } from "@bitwarden/auth/angular"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { CommandDefinition, MessageListener } from "@bitwarden/common/platform/messaging"; + +// TODO: PM-16209 We should create a Duo2faMessageListenerService that listens for messages from duo +// and this command definition should move to that file. +// We should explore consolidating the messaging approach across clients - i.e., we +// should use the same command definition across all clients. We use duoResult on extension for no real +// benefit. +export const DUO_2FA_RESULT_COMMAND = new CommandDefinition<{ code: string; state: string }>( + "duoCallback", +); + +export class DesktopTwoFactorAuthDuoComponentService implements TwoFactorAuthDuoComponentService { + constructor( + private messageListener: MessageListener, + private environmentService: EnvironmentService, + private i18nService: I18nService, + private platformUtilsService: PlatformUtilsService, + ) {} + listenForDuo2faResult$(): Observable { + return this.messageListener.messages$(DUO_2FA_RESULT_COMMAND).pipe( + map((msg) => { + return { + code: msg.code, + state: msg.state, + token: `${msg.code}|${msg.state}`, + } as Duo2faResult; + }), + ); + } + + async launchDuoFrameless(duoFramelessUrl: string): Promise { + const duoHandOffMessage = { + title: this.i18nService.t("youSuccessfullyLoggedIn"), + message: this.i18nService.t("youMayCloseThisWindow"), + isCountdown: false, + }; + + // we're using the connector here as a way to set a cookie with translations + // before continuing to the duo frameless url + const env = await firstValueFrom(this.environmentService.environment$); + const launchUrl = + env.getWebVaultUrl() + + "/duo-redirect-connector.html" + + "?duoFramelessUrl=" + + encodeURIComponent(duoFramelessUrl) + + "&handOffMessage=" + + encodeURIComponent(JSON.stringify(duoHandOffMessage)); + this.platformUtilsService.launchUri(launchUrl); + } +} diff --git a/apps/desktop/src/auth/two-factor-auth-duo.component.ts b/apps/desktop/src/auth/two-factor-auth-duo.component.ts deleted file mode 100644 index c238b753b64..00000000000 --- a/apps/desktop/src/auth/two-factor-auth-duo.component.ts +++ /dev/null @@ -1,117 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { DialogModule } from "@angular/cdk/dialog"; -import { CommonModule } from "@angular/common"; -import { Component, NgZone, OnDestroy, OnInit } from "@angular/core"; -import { ReactiveFormsModule, FormsModule } from "@angular/forms"; -import { firstValueFrom } from "rxjs"; - -import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; -import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { - AsyncActionsModule, - ButtonModule, - FormFieldModule, - LinkModule, - ToastService, - TypographyModule, -} from "@bitwarden/components"; - -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { TwoFactorAuthDuoComponent as TwoFactorAuthDuoBaseComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-duo.component"; - -const BroadcasterSubscriptionId = "TwoFactorComponent"; - -@Component({ - standalone: true, - selector: "app-two-factor-auth-duo", - templateUrl: - "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-duo.component.html", - imports: [ - CommonModule, - JslibModule, - DialogModule, - ButtonModule, - LinkModule, - TypographyModule, - ReactiveFormsModule, - FormFieldModule, - AsyncActionsModule, - FormsModule, - ], - providers: [I18nPipe], -}) -export class TwoFactorAuthDuoComponent - extends TwoFactorAuthDuoBaseComponent - implements OnInit, OnDestroy -{ - constructor( - protected i18nService: I18nService, - protected platformUtilsService: PlatformUtilsService, - private broadcasterService: BroadcasterService, - private ngZone: NgZone, - private environmentService: EnvironmentService, - toastService: ToastService, - ) { - super(i18nService, platformUtilsService, toastService); - } - - async ngOnInit(): Promise { - await super.ngOnInit(); - } - - duoCallbackSubscriptionEnabled: boolean = false; - - protected override setupDuoResultListener() { - if (!this.duoCallbackSubscriptionEnabled) { - this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => { - await this.ngZone.run(async () => { - if (message.command === "duoCallback") { - this.token.emit(message.code + "|" + message.state); - } - }); - }); - this.duoCallbackSubscriptionEnabled = true; - } - } - - override async launchDuoFrameless() { - if (this.duoFramelessUrl === null) { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t("duoHealthCheckResultsInNullAuthUrlError"), - }); - return; - } - const duoHandOffMessage = { - title: this.i18nService.t("youSuccessfullyLoggedIn"), - message: this.i18nService.t("youMayCloseThisWindow"), - isCountdown: false, - }; - - // we're using the connector here as a way to set a cookie with translations - // before continuing to the duo frameless url - const env = await firstValueFrom(this.environmentService.environment$); - const launchUrl = - env.getWebVaultUrl() + - "/duo-redirect-connector.html" + - "?duoFramelessUrl=" + - encodeURIComponent(this.duoFramelessUrl) + - "&handOffMessage=" + - encodeURIComponent(JSON.stringify(duoHandOffMessage)); - this.platformUtilsService.launchUri(launchUrl); - } - - async ngOnDestroy() { - if (this.duoCallbackSubscriptionEnabled) { - this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); - this.duoCallbackSubscriptionEnabled = false; - } - } -} diff --git a/apps/desktop/src/auth/two-factor-auth.component.ts b/apps/desktop/src/auth/two-factor-auth.component.ts deleted file mode 100644 index 9e0898c39e2..00000000000 --- a/apps/desktop/src/auth/two-factor-auth.component.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { DialogModule } from "@angular/cdk/dialog"; -import { CommonModule } from "@angular/common"; -import { Component } from "@angular/core"; -import { ReactiveFormsModule } from "@angular/forms"; -import { RouterLink } from "@angular/router"; - -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { TwoFactorAuthAuthenticatorComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-authenticator.component"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { TwoFactorAuthEmailComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { TwoFactorAuthWebAuthnComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-webauthn.component"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { TwoFactorAuthYubikeyComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-yubikey.component"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { TwoFactorAuthComponent as BaseTwoFactorAuthComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { TwoFactorOptionsComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-options.component"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { JslibModule } from "../../../../libs/angular/src/jslib.module"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { AsyncActionsModule } from "../../../../libs/components/src/async-actions"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { ButtonModule } from "../../../../libs/components/src/button"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { CheckboxModule } from "../../../../libs/components/src/checkbox"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { FormFieldModule } from "../../../../libs/components/src/form-field"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { LinkModule } from "../../../../libs/components/src/link"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { I18nPipe } from "../../../../libs/components/src/shared/i18n.pipe"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { TypographyModule } from "../../../../libs/components/src/typography"; - -import { TwoFactorAuthDuoComponent } from "./two-factor-auth-duo.component"; - -@Component({ - standalone: true, - templateUrl: - "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.html", - selector: "app-two-factor-auth", - imports: [ - CommonModule, - JslibModule, - DialogModule, - ButtonModule, - LinkModule, - TypographyModule, - ReactiveFormsModule, - FormFieldModule, - AsyncActionsModule, - RouterLink, - CheckboxModule, - TwoFactorOptionsComponent, - TwoFactorAuthEmailComponent, - TwoFactorAuthAuthenticatorComponent, - TwoFactorAuthYubikeyComponent, - TwoFactorAuthDuoComponent, - TwoFactorAuthWebAuthnComponent, - ], - providers: [I18nPipe], -}) -export class TwoFactorAuthComponent extends BaseTwoFactorAuthComponent {} diff --git a/apps/desktop/src/auth/two-factor.component.html b/apps/desktop/src/auth/two-factor-v1.component.html similarity index 100% rename from apps/desktop/src/auth/two-factor.component.html rename to apps/desktop/src/auth/two-factor-v1.component.html diff --git a/apps/desktop/src/auth/two-factor.component.ts b/apps/desktop/src/auth/two-factor-v1.component.ts similarity index 96% rename from apps/desktop/src/auth/two-factor.component.ts rename to apps/desktop/src/auth/two-factor-v1.component.ts index 7f4525c5f14..00d12003a9f 100644 --- a/apps/desktop/src/auth/two-factor.component.ts +++ b/apps/desktop/src/auth/two-factor-v1.component.ts @@ -4,7 +4,7 @@ import { Component, Inject, NgZone, OnDestroy, ViewChild, ViewContainerRef } fro import { ActivatedRoute, Router } from "@angular/router"; import { firstValueFrom } from "rxjs"; -import { TwoFactorComponent as BaseTwoFactorComponent } from "@bitwarden/angular/auth/components/two-factor.component"; +import { TwoFactorComponentV1 as BaseTwoFactorComponent } from "@bitwarden/angular/auth/components/two-factor-v1.component"; import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { @@ -35,10 +35,10 @@ const BroadcasterSubscriptionId = "TwoFactorComponent"; @Component({ selector: "app-two-factor", - templateUrl: "two-factor.component.html", + templateUrl: "two-factor-v1.component.html", }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDestroy { +export class TwoFactorComponentV1 extends BaseTwoFactorComponent implements OnDestroy { @ViewChild("twoFactorOptions", { read: ViewContainerRef, static: true }) twoFactorOptionsModal: ViewContainerRef; diff --git a/apps/web/src/app/auth/core/services/index.ts b/apps/web/src/app/auth/core/services/index.ts index 6275ad4f4f3..1e8eec759b1 100644 --- a/apps/web/src/app/auth/core/services/index.ts +++ b/apps/web/src/app/auth/core/services/index.ts @@ -3,3 +3,4 @@ export * from "./login-decryption-options"; export * from "./webauthn-login"; export * from "./set-password-jit"; export * from "./registration"; +export * from "./two-factor-auth"; diff --git a/apps/web/src/app/auth/core/services/two-factor-auth/index.ts b/apps/web/src/app/auth/core/services/two-factor-auth/index.ts new file mode 100644 index 00000000000..ba2697fdee4 --- /dev/null +++ b/apps/web/src/app/auth/core/services/two-factor-auth/index.ts @@ -0,0 +1,2 @@ +export * from "./web-two-factor-auth-component.service"; +export * from "./web-two-factor-auth-duo-component.service"; diff --git a/apps/web/src/app/auth/core/services/two-factor-auth/web-two-factor-auth-component.service.ts b/apps/web/src/app/auth/core/services/two-factor-auth/web-two-factor-auth-component.service.ts new file mode 100644 index 00000000000..451cec57ddd --- /dev/null +++ b/apps/web/src/app/auth/core/services/two-factor-auth/web-two-factor-auth-component.service.ts @@ -0,0 +1,14 @@ +import { + DefaultTwoFactorAuthComponentService, + TwoFactorAuthComponentService, + LegacyKeyMigrationAction, +} from "@bitwarden/auth/angular"; + +export class WebTwoFactorAuthComponentService + extends DefaultTwoFactorAuthComponentService + implements TwoFactorAuthComponentService +{ + override determineLegacyKeyMigrationAction(): LegacyKeyMigrationAction { + return LegacyKeyMigrationAction.NAVIGATE_TO_MIGRATION_COMPONENT; + } +} diff --git a/apps/web/src/app/auth/core/services/two-factor-auth/web-two-factor-auth-duo-component.service.ts b/apps/web/src/app/auth/core/services/two-factor-auth/web-two-factor-auth-duo-component.service.ts new file mode 100644 index 00000000000..ac8eccb5198 --- /dev/null +++ b/apps/web/src/app/auth/core/services/two-factor-auth/web-two-factor-auth-duo-component.service.ts @@ -0,0 +1,31 @@ +import { fromEvent, map, Observable, share } from "rxjs"; + +import { TwoFactorAuthDuoComponentService, Duo2faResult } from "@bitwarden/auth/angular"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; + +export class WebTwoFactorAuthDuoComponentService implements TwoFactorAuthDuoComponentService { + private duo2faResult$: Observable; + + constructor(private platformUtilsService: PlatformUtilsService) { + const duoResultChannel: BroadcastChannel = new BroadcastChannel("duoResult"); + + this.duo2faResult$ = fromEvent(duoResultChannel, "message").pipe( + map((msg: MessageEvent) => { + return { + code: msg.data.code, + state: msg.data.state, + token: `${msg.data.code}|${msg.data.state}`, + } as Duo2faResult; + }), + // share the observable so that multiple subscribers can listen to the same event + share(), + ); + } + listenForDuo2faResult$(): Observable { + return this.duo2faResult$; + } + + async launchDuoFrameless(duoFramelessUrl: string): Promise { + this.platformUtilsService.launchUri(duoFramelessUrl); + } +} diff --git a/apps/web/src/app/auth/two-factor-auth-duo.component.ts b/apps/web/src/app/auth/two-factor-auth-duo.component.ts deleted file mode 100644 index b82632008bd..00000000000 --- a/apps/web/src/app/auth/two-factor-auth-duo.component.ts +++ /dev/null @@ -1,65 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { DialogModule } from "@angular/cdk/dialog"; -import { CommonModule } from "@angular/common"; -import { Component, OnDestroy, OnInit } from "@angular/core"; -import { ReactiveFormsModule, FormsModule } from "@angular/forms"; - -import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; - -import { TwoFactorAuthDuoComponent as TwoFactorAuthDuoBaseComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-duo.component"; -import { AsyncActionsModule } from "../../../../../libs/components/src/async-actions"; -import { ButtonModule } from "../../../../../libs/components/src/button"; -import { FormFieldModule } from "../../../../../libs/components/src/form-field"; -import { LinkModule } from "../../../../../libs/components/src/link"; -import { TypographyModule } from "../../../../../libs/components/src/typography"; - -@Component({ - standalone: true, - selector: "app-two-factor-auth-duo", - templateUrl: - "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-duo.component.html", - imports: [ - CommonModule, - JslibModule, - DialogModule, - ButtonModule, - LinkModule, - TypographyModule, - ReactiveFormsModule, - FormFieldModule, - AsyncActionsModule, - FormsModule, - ], - providers: [I18nPipe], -}) -export class TwoFactorAuthDuoComponent - extends TwoFactorAuthDuoBaseComponent - implements OnInit, OnDestroy -{ - async ngOnInit(): Promise { - await super.ngOnInit(); - } - - private duoResultChannel: BroadcastChannel; - - protected override setupDuoResultListener() { - if (!this.duoResultChannel) { - this.duoResultChannel = new BroadcastChannel("duoResult"); - this.duoResultChannel.addEventListener("message", this.handleDuoResultMessage); - } - } - - private handleDuoResultMessage = async (msg: { data: { code: string; state: string } }) => { - this.token.emit(msg.data.code + "|" + msg.data.state); - }; - - async ngOnDestroy() { - if (this.duoResultChannel) { - // clean up duo listener if it was initialized. - this.duoResultChannel.removeEventListener("message", this.handleDuoResultMessage); - this.duoResultChannel.close(); - } - } -} diff --git a/apps/web/src/app/auth/two-factor-auth.component.ts b/apps/web/src/app/auth/two-factor-auth.component.ts deleted file mode 100644 index 18660b2ca63..00000000000 --- a/apps/web/src/app/auth/two-factor-auth.component.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { DialogModule } from "@angular/cdk/dialog"; -import { CommonModule } from "@angular/common"; -import { Component, Inject } from "@angular/core"; -import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; -import { ActivatedRoute, Router, RouterLink } from "@angular/router"; - -import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; -import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; -import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; -import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; -import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { - LinkModule, - TypographyModule, - CheckboxModule, - DialogService, - ToastService, -} from "@bitwarden/components"; - -import { TwoFactorAuthAuthenticatorComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-authenticator.component"; -import { TwoFactorAuthEmailComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component"; -import { TwoFactorAuthWebAuthnComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-webauthn.component"; -import { TwoFactorAuthYubikeyComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-yubikey.component"; -import { TwoFactorAuthComponent as BaseTwoFactorAuthComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component"; -import { TwoFactorOptionsComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-options.component"; -import { - LoginStrategyServiceAbstraction, - LoginEmailServiceAbstraction, - UserDecryptionOptionsServiceAbstraction, -} from "../../../../../libs/auth/src/common/abstractions"; -import { AsyncActionsModule } from "../../../../../libs/components/src/async-actions"; -import { ButtonModule } from "../../../../../libs/components/src/button"; -import { FormFieldModule } from "../../../../../libs/components/src/form-field"; - -import { TwoFactorAuthDuoComponent } from "./two-factor-auth-duo.component"; - -@Component({ - standalone: true, - templateUrl: - "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.html", - selector: "app-two-factor-auth", - imports: [ - CommonModule, - JslibModule, - DialogModule, - ButtonModule, - LinkModule, - TypographyModule, - ReactiveFormsModule, - FormFieldModule, - AsyncActionsModule, - RouterLink, - CheckboxModule, - TwoFactorOptionsComponent, - TwoFactorAuthEmailComponent, - TwoFactorAuthAuthenticatorComponent, - TwoFactorAuthYubikeyComponent, - TwoFactorAuthDuoComponent, - TwoFactorAuthWebAuthnComponent, - ], - providers: [I18nPipe], -}) -export class TwoFactorAuthComponent extends BaseTwoFactorAuthComponent { - constructor( - protected loginStrategyService: LoginStrategyServiceAbstraction, - protected router: Router, - i18nService: I18nService, - platformUtilsService: PlatformUtilsService, - environmentService: EnvironmentService, - dialogService: DialogService, - protected route: ActivatedRoute, - logService: LogService, - protected twoFactorService: TwoFactorService, - loginEmailService: LoginEmailServiceAbstraction, - userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, - protected ssoLoginService: SsoLoginServiceAbstraction, - protected configService: ConfigService, - masterPasswordService: InternalMasterPasswordServiceAbstraction, - accountService: AccountService, - formBuilder: FormBuilder, - @Inject(WINDOW) protected win: Window, - toastService: ToastService, - ) { - super( - loginStrategyService, - router, - i18nService, - platformUtilsService, - environmentService, - dialogService, - route, - logService, - twoFactorService, - loginEmailService, - userDecryptionOptionsService, - ssoLoginService, - configService, - masterPasswordService, - accountService, - formBuilder, - win, - toastService, - ); - this.onSuccessfulLoginNavigate = this.goAfterLogIn; - } - - protected override handleMigrateEncryptionKey(result: AuthResult): boolean { - if (!result.requiresEncryptionKeyMigration) { - return false; - } - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["migrate-legacy-encryption"]); - return true; - } -} diff --git a/apps/web/src/app/auth/two-factor.component.html b/apps/web/src/app/auth/two-factor-v1.component.html similarity index 100% rename from apps/web/src/app/auth/two-factor.component.html rename to apps/web/src/app/auth/two-factor-v1.component.html diff --git a/apps/web/src/app/auth/two-factor.component.ts b/apps/web/src/app/auth/two-factor-v1.component.ts similarity index 95% rename from apps/web/src/app/auth/two-factor.component.ts rename to apps/web/src/app/auth/two-factor-v1.component.ts index eead66468fd..51e7382305e 100644 --- a/apps/web/src/app/auth/two-factor.component.ts +++ b/apps/web/src/app/auth/two-factor-v1.component.ts @@ -5,7 +5,7 @@ import { FormBuilder, Validators } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { Subject, takeUntil, lastValueFrom } from "rxjs"; -import { TwoFactorComponent as BaseTwoFactorComponent } from "@bitwarden/angular/auth/components/two-factor.component"; +import { TwoFactorComponentV1 as BaseTwoFactorComponent } from "@bitwarden/angular/auth/components/two-factor-v1.component"; import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; import { LoginStrategyServiceAbstraction, @@ -35,10 +35,10 @@ import { @Component({ selector: "app-two-factor", - templateUrl: "two-factor.component.html", + templateUrl: "two-factor-v1.component.html", }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class TwoFactorComponent extends BaseTwoFactorComponent implements OnInit, OnDestroy { +export class TwoFactorComponentV1 extends BaseTwoFactorComponent implements OnInit, OnDestroy { @ViewChild("twoFactorOptions", { read: ViewContainerRef, static: true }) twoFactorOptionsModal: ViewContainerRef; formGroup = this.formBuilder.group({ diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index 8f21dfa2c8b..2c581a06f67 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -33,6 +33,8 @@ import { SetPasswordJitService, SsoComponentService, LoginDecryptionOptionsService, + TwoFactorAuthComponentService, + TwoFactorAuthDuoComponentService, } from "@bitwarden/auth/angular"; import { InternalUserDecryptionOptionsServiceAbstraction, @@ -100,6 +102,8 @@ import { WebRegistrationFinishService, WebLoginComponentService, WebLoginDecryptionOptionsService, + WebTwoFactorAuthComponentService, + WebTwoFactorAuthDuoComponentService, } from "../auth"; import { WebSsoComponentService } from "../auth/core/services/login/web-sso-component.service"; import { AcceptOrganizationInviteService } from "../auth/organization-invite/accept-organization.service"; @@ -247,6 +251,11 @@ const safeProviders: SafeProvider[] = [ useClass: WebLockComponentService, deps: [], }), + safeProvider({ + provide: TwoFactorAuthComponentService, + useClass: WebTwoFactorAuthComponentService, + deps: [], + }), safeProvider({ provide: SetPasswordJitService, useClass: WebSetPasswordJitService, @@ -308,6 +317,11 @@ const safeProviders: SafeProvider[] = [ useClass: WebSsoComponentService, deps: [I18nServiceAbstraction], }), + safeProvider({ + provide: TwoFactorAuthDuoComponentService, + useClass: WebTwoFactorAuthDuoComponentService, + deps: [PlatformUtilsService], + }), safeProvider({ provide: LoginDecryptionOptionsService, useClass: WebLoginDecryptionOptionsService, diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index fadcc28f832..83c6897c41a 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -1,7 +1,6 @@ import { NgModule } from "@angular/core"; import { Route, RouterModule, Routes } from "@angular/router"; -import { TwoFactorTimeoutComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth-expired.component"; import { unauthUiRefreshSwap } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-route-swap"; import { authGuard, @@ -37,6 +36,9 @@ import { SsoComponent, VaultIcon, LoginDecryptionOptionsComponent, + TwoFactorAuthComponent, + TwoFactorTimeoutComponent, + TwoFactorAuthGuard, } from "@bitwarden/auth/angular"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { LockComponent } from "@bitwarden/key-management/angular"; @@ -46,7 +48,6 @@ import { VaultIcons, } from "@bitwarden/vault"; -import { twofactorRefactorSwap } from "../../../../libs/angular/src/utils/two-factor-component-refactor-route-swap"; import { flagEnabled, Flags } from "../utils/flags"; import { VerifyRecoverDeleteOrgComponent } from "./admin-console/organizations/manage/verify-recover-delete-org.component"; @@ -72,8 +73,7 @@ import { SsoComponentV1 } from "./auth/sso-v1.component"; import { CompleteTrialInitiationComponent } from "./auth/trial-initiation/complete-trial-initiation/complete-trial-initiation.component"; import { freeTrialTextResolver } from "./auth/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver"; import { TrialInitiationComponent } from "./auth/trial-initiation/trial-initiation.component"; -import { TwoFactorAuthComponent } from "./auth/two-factor-auth.component"; -import { TwoFactorComponent } from "./auth/two-factor.component"; +import { TwoFactorComponentV1 } from "./auth/two-factor-v1.component"; import { UpdatePasswordComponent } from "./auth/update-password.component"; import { UpdateTempPasswordComponent } from "./auth/update-temp-password.component"; import { VerifyEmailTokenComponent } from "./auth/verify-email-token.component"; @@ -507,6 +507,50 @@ const routes: Routes = [ }, }, }, + ...unauthUiRefreshSwap( + TwoFactorComponentV1, + TwoFactorAuthComponent, + { + path: "2fa", + canActivate: [unauthGuardFn()], + children: [ + { + path: "", + component: TwoFactorComponentV1, + }, + { + path: "", + component: EnvironmentSelectorComponent, + outlet: "environment-selector", + }, + ], + data: { + pageTitle: { + key: "verifyIdentity", + }, + } satisfies RouteDataProperties & AnonLayoutWrapperData, + }, + { + path: "2fa", + canActivate: [unauthGuardFn(), TwoFactorAuthGuard], + children: [ + { + path: "", + component: TwoFactorAuthComponent, + }, + { + path: "", + component: EnvironmentSelectorComponent, + outlet: "environment-selector", + }, + ], + data: { + pageTitle: { + key: "verifyIdentity", + }, + } satisfies RouteDataProperties & AnonLayoutWrapperData, + }, + ), { path: "lock", canActivate: [deepLinkGuard(), lockGuard()], @@ -524,25 +568,6 @@ const routes: Routes = [ showReadonlyHostname: true, } satisfies AnonLayoutWrapperData, }, - { - path: "2fa", - canActivate: [unauthGuardFn()], - children: [ - ...twofactorRefactorSwap(TwoFactorComponent, TwoFactorAuthComponent, { - path: "", - }), - { - path: "", - component: EnvironmentSelectorComponent, - outlet: "environment-selector", - }, - ], - data: { - pageTitle: { - key: "verifyIdentity", - }, - } satisfies RouteDataProperties & AnonLayoutWrapperData, - }, { path: "2fa-timeout", canActivate: [unauthGuardFn()], diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index 3176ac81c1a..3a1c20c18a5 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -52,7 +52,7 @@ import { TwoFactorVerifyComponent } from "../auth/settings/two-factor/two-factor import { UserVerificationModule } from "../auth/shared/components/user-verification"; import { SsoComponentV1 } from "../auth/sso-v1.component"; import { TwoFactorOptionsComponent } from "../auth/two-factor-options.component"; -import { TwoFactorComponent } from "../auth/two-factor.component"; +import { TwoFactorComponentV1 } from "../auth/two-factor-v1.component"; import { UpdatePasswordComponent } from "../auth/update-password.component"; import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component"; import { VerifyEmailTokenComponent } from "../auth/verify-email-token.component"; @@ -158,9 +158,9 @@ import { SharedModule } from "./shared.module"; SetPasswordComponent, SponsoredFamiliesComponent, SponsoringOrgRowComponent, + TwoFactorComponentV1, SsoComponentV1, TwoFactorSetupAuthenticatorComponent, - TwoFactorComponent, TwoFactorSetupDuoComponent, TwoFactorSetupEmailComponent, TwoFactorOptionsComponent, @@ -225,18 +225,16 @@ import { SharedModule } from "./shared.module"; SetPasswordComponent, SponsoredFamiliesComponent, SponsoringOrgRowComponent, + TwoFactorComponentV1, SsoComponentV1, TwoFactorSetupAuthenticatorComponent, - TwoFactorComponent, TwoFactorSetupDuoComponent, TwoFactorSetupEmailComponent, TwoFactorOptionsComponent, - TwoFactorRecoveryComponent, TwoFactorSetupComponent, TwoFactorVerifyComponent, TwoFactorSetupWebAuthnComponent, TwoFactorSetupYubiKeyComponent, - UpdatePasswordComponent, UpdateTempPasswordComponent, UserLayoutComponent, VerifyEmailTokenComponent, diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.html b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.html deleted file mode 100644 index 8462a18ac2e..00000000000 --- a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.html +++ /dev/null @@ -1,76 +0,0 @@ -
- - - - - - - {{ "rememberMe" | i18n }} - - - -

{{ "noTwoStepProviders" | i18n }}

-

{{ "noTwoStepProviders2" | i18n }}

-
-
- -
- -
- - - - - {{ "cancel" | i18n }} - -
- - diff --git a/libs/angular/src/auth/components/two-factor.component.spec.ts b/libs/angular/src/auth/components/two-factor-v1.component.spec.ts similarity index 99% rename from libs/angular/src/auth/components/two-factor.component.spec.ts rename to libs/angular/src/auth/components/two-factor-v1.component.spec.ts index 5a1903d6671..10d227c2fe9 100644 --- a/libs/angular/src/auth/components/two-factor.component.spec.ts +++ b/libs/angular/src/auth/components/two-factor-v1.component.spec.ts @@ -34,11 +34,11 @@ import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/sp import { UserId } from "@bitwarden/common/types/guid"; import { ToastService } from "@bitwarden/components"; -import { TwoFactorComponent } from "./two-factor.component"; +import { TwoFactorComponentV1 } from "./two-factor-v1.component"; // test component that extends the TwoFactorComponent @Component({}) -class TestTwoFactorComponent extends TwoFactorComponent {} +class TestTwoFactorComponent extends TwoFactorComponentV1 {} interface TwoFactorComponentProtected { trustedDeviceEncRoute: string; diff --git a/libs/angular/src/auth/components/two-factor.component.ts b/libs/angular/src/auth/components/two-factor-v1.component.ts similarity index 99% rename from libs/angular/src/auth/components/two-factor.component.ts rename to libs/angular/src/auth/components/two-factor-v1.component.ts index e2b41ad086d..fd3e9dd89a4 100644 --- a/libs/angular/src/auth/components/two-factor.component.ts +++ b/libs/angular/src/auth/components/two-factor-v1.component.ts @@ -40,7 +40,7 @@ import { ToastService } from "@bitwarden/components"; import { CaptchaProtectedComponent } from "./captcha-protected.component"; @Directive() -export class TwoFactorComponent extends CaptchaProtectedComponent implements OnInit, OnDestroy { +export class TwoFactorComponentV1 extends CaptchaProtectedComponent implements OnInit, OnDestroy { token = ""; remember = false; webAuthnReady = false; diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 803808612cf..15472b1d106 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -20,6 +20,12 @@ import { DefaultLoginComponentService, LoginDecryptionOptionsService, DefaultLoginDecryptionOptionsService, + TwoFactorAuthComponentService, + DefaultTwoFactorAuthComponentService, + DefaultTwoFactorAuthEmailComponentService, + TwoFactorAuthEmailComponentService, + DefaultTwoFactorAuthWebAuthnComponentService, + TwoFactorAuthWebAuthnComponentService, DefaultLoginApprovalComponentService, } from "@bitwarden/auth/angular"; import { @@ -1369,6 +1375,21 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultRegistrationFinishService, deps: [KeyService, AccountApiServiceAbstraction], }), + safeProvider({ + provide: TwoFactorAuthComponentService, + useClass: DefaultTwoFactorAuthComponentService, + deps: [], + }), + safeProvider({ + provide: TwoFactorAuthWebAuthnComponentService, + useClass: DefaultTwoFactorAuthWebAuthnComponentService, + deps: [], + }), + safeProvider({ + provide: TwoFactorAuthEmailComponentService, + useClass: DefaultTwoFactorAuthEmailComponentService, + deps: [], + }), safeProvider({ provide: ViewCacheService, useExisting: NoopViewCacheService, diff --git a/libs/angular/src/utils/two-factor-component-refactor-route-swap.ts b/libs/angular/src/utils/two-factor-component-refactor-route-swap.ts deleted file mode 100644 index 8b57a3eb94f..00000000000 --- a/libs/angular/src/utils/two-factor-component-refactor-route-swap.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Type, inject } from "@angular/core"; -import { Route, Routes } from "@angular/router"; - -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; - -import { componentRouteSwap } from "./component-route-swap"; -/** - * Helper function to swap between two components based on the TwoFactorComponentRefactor feature flag. - * @param defaultComponent - The current non-refactored component to render. - * @param refreshedComponent - The new refactored component to render. - * @param defaultOptions - The options to apply to the default component and the refactored component, if alt options are not provided. - * @param altOptions - The options to apply to the refactored component. - */ -export function twofactorRefactorSwap( - defaultComponent: Type, - refreshedComponent: Type, - defaultOptions: Route, - altOptions?: Route, -): Routes { - return componentRouteSwap( - defaultComponent, - refreshedComponent, - async () => { - const configService = inject(ConfigService); - return configService.getFeatureFlag(FeatureFlag.TwoFactorComponentRefactor); - }, - defaultOptions, - altOptions, - ); -} diff --git a/libs/auth/src/angular/index.ts b/libs/auth/src/angular/index.ts index 66111f3e5af..ee8f24dcd9a 100644 --- a/libs/auth/src/angular/index.ts +++ b/libs/auth/src/angular/index.ts @@ -71,3 +71,6 @@ export * from "./self-hosted-env-config-dialog/self-hosted-env-config-dialog.com // login approval export * from "./login-approval/login-approval.component"; export * from "./login-approval/default-login-approval-component.service"; + +// two factor auth +export * from "./two-factor-auth"; diff --git a/libs/auth/src/angular/two-factor-auth/child-components/index.ts b/libs/auth/src/angular/two-factor-auth/child-components/index.ts new file mode 100644 index 00000000000..429da3f14b3 --- /dev/null +++ b/libs/auth/src/angular/two-factor-auth/child-components/index.ts @@ -0,0 +1,3 @@ +export * from "./two-factor-auth-email"; +export * from "./two-factor-auth-duo"; +export * from "./two-factor-auth-webauthn"; diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-authenticator.component.html b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-authenticator.component.html similarity index 100% rename from libs/angular/src/auth/components/two-factor-auth/two-factor-auth-authenticator.component.html rename to libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-authenticator.component.html diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-authenticator.component.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-authenticator.component.ts similarity index 100% rename from libs/angular/src/auth/components/two-factor-auth/two-factor-auth-authenticator.component.ts rename to libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-authenticator.component.ts diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-duo/index.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-duo/index.ts new file mode 100644 index 00000000000..c43325e0d0b --- /dev/null +++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-duo/index.ts @@ -0,0 +1 @@ +export * from "./two-factor-auth-duo-component.service"; diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-duo/two-factor-auth-duo-component.service.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-duo/two-factor-auth-duo-component.service.ts new file mode 100644 index 00000000000..5aa145696bd --- /dev/null +++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-duo/two-factor-auth-duo-component.service.ts @@ -0,0 +1,26 @@ +import { Observable } from "rxjs"; + +export interface Duo2faResult { + code: string; + state: string; + /** + * The code and the state joined by a | character. + */ + token: string; +} + +/** + * A service which manages all the cross client logic for the duo 2FA component. + */ +export abstract class TwoFactorAuthDuoComponentService { + /** + * Retrieves the result of the duo two-factor authentication process. + * @returns {Observable} An observable that emits the result of the duo two-factor authentication process. + */ + abstract listenForDuo2faResult$(): Observable; + + /** + * Launches the client specific duo frameless 2FA flow. + */ + abstract launchDuoFrameless(duoFramelessUrl: string): Promise; +} diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-duo.component.html b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-duo/two-factor-auth-duo.component.html similarity index 100% rename from libs/angular/src/auth/components/two-factor-auth/two-factor-auth-duo.component.html rename to libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-duo/two-factor-auth-duo.component.html diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-duo.component.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-duo/two-factor-auth-duo.component.ts similarity index 70% rename from libs/angular/src/auth/components/two-factor-auth/two-factor-auth-duo.component.ts rename to libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-duo/two-factor-auth-duo.component.ts index 3131cc042f7..b70a7247116 100644 --- a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-duo.component.ts +++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-duo/two-factor-auth-duo.component.ts @@ -1,8 +1,7 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { DialogModule } from "@angular/cdk/dialog"; import { CommonModule } from "@angular/common"; -import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; +import { Component, DestroyRef, EventEmitter, Input, OnInit, Output } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ReactiveFormsModule, FormsModule } from "@angular/forms"; import { JslibModule } from "@bitwarden/angular/jslib.module"; @@ -18,6 +17,11 @@ import { ToastService, } from "@bitwarden/components"; +import { + Duo2faResult, + TwoFactorAuthDuoComponentService, +} from "./two-factor-auth-duo-component.service"; + @Component({ standalone: true, selector: "app-two-factor-auth-duo", @@ -41,32 +45,28 @@ export class TwoFactorAuthDuoComponent implements OnInit { @Input() providerData: any; duoFramelessUrl: string = null; - duoResultListenerInitialized = false; constructor( protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, protected toastService: ToastService, + private twoFactorAuthDuoComponentService: TwoFactorAuthDuoComponentService, + private destroyRef: DestroyRef, ) {} async ngOnInit(): Promise { - await this.init(); - } - - async init() { - // Setup listener for duo-redirect.ts connector to send back the code - if (!this.duoResultListenerInitialized) { - // setup client specific duo result listener - this.setupDuoResultListener(); - this.duoResultListenerInitialized = true; - } + this.twoFactorAuthDuoComponentService + .listenForDuo2faResult$() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((duo2faResult: Duo2faResult) => { + this.token.emit(duo2faResult.token); + }); // flow must be launched by user so they can choose to remember the device or not. this.duoFramelessUrl = this.providerData.AuthUrl; } - // Each client will have own implementation - protected setupDuoResultListener(): void {} + // Called via parent two-factor-auth component. async launchDuoFrameless(): Promise { if (this.duoFramelessUrl === null) { this.toastService.showToast({ @@ -76,6 +76,7 @@ export class TwoFactorAuthDuoComponent implements OnInit { }); return; } - this.platformUtilsService.launchUri(this.duoFramelessUrl); + + await this.twoFactorAuthDuoComponentService.launchDuoFrameless(this.duoFramelessUrl); } } diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/default-two-factor-auth-email-component.service.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/default-two-factor-auth-email-component.service.ts new file mode 100644 index 00000000000..caae13acc38 --- /dev/null +++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/default-two-factor-auth-email-component.service.ts @@ -0,0 +1,6 @@ +import { TwoFactorAuthEmailComponentService } from "./two-factor-auth-email-component.service"; + +export class DefaultTwoFactorAuthEmailComponentService + implements TwoFactorAuthEmailComponentService { + // no default implementation +} diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/index.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/index.ts new file mode 100644 index 00000000000..91f11b0b7dd --- /dev/null +++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/index.ts @@ -0,0 +1,2 @@ +export * from "./default-two-factor-auth-email-component.service"; +export * from "./two-factor-auth-email-component.service"; diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email-component.service.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email-component.service.ts new file mode 100644 index 00000000000..fa96b6b96c2 --- /dev/null +++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email-component.service.ts @@ -0,0 +1,10 @@ +/** + * A service that manages all cross client functionality for the email 2FA component. + */ +export abstract class TwoFactorAuthEmailComponentService { + /** + * Optionally shows a warning to the user that they might need to popout the + * window to complete email 2FA. + */ + abstract openPopoutIfApprovedForEmail2fa?(): Promise; +} diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component.html b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.html similarity index 100% rename from libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component.html rename to libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.html diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.ts similarity index 93% rename from libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component.ts rename to libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.ts index 8f01403cdbb..1052cef366e 100644 --- a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component.ts +++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { DialogModule } from "@angular/cdk/dialog"; import { CommonModule } from "@angular/common"; import { Component, EventEmitter, OnInit, Output } from "@angular/core"; @@ -25,6 +23,8 @@ import { ToastService, } from "@bitwarden/components"; +import { TwoFactorAuthEmailComponentService } from "./two-factor-auth-email-component.service"; + @Component({ standalone: true, selector: "app-two-factor-auth-email", @@ -59,9 +59,12 @@ export class TwoFactorAuthEmailComponent implements OnInit { protected apiService: ApiService, protected appIdService: AppIdService, private toastService: ToastService, + private twoFactorAuthEmailComponentService: TwoFactorAuthEmailComponentService, ) {} async ngOnInit(): Promise { + await this.twoFactorAuthEmailComponentService.openPopoutIfApprovedForEmail2fa?.(); + const providerData = await this.twoFactorService.getProviders().then((providers) => { return providers.get(TwoFactorProviderType.Email); }); diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-webauthn/default-two-factor-auth-webauthn-component.service.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-webauthn/default-two-factor-auth-webauthn-component.service.ts new file mode 100644 index 00000000000..3d3578c656e --- /dev/null +++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-webauthn/default-two-factor-auth-webauthn-component.service.ts @@ -0,0 +1,12 @@ +import { TwoFactorAuthWebAuthnComponentService } from "./two-factor-auth-webauthn-component.service"; + +export class DefaultTwoFactorAuthWebAuthnComponentService + implements TwoFactorAuthWebAuthnComponentService +{ + /** + * Default implementation is to not open in a new tab. + */ + shouldOpenWebAuthnInNewTab(): boolean { + return false; + } +} diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-webauthn/index.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-webauthn/index.ts new file mode 100644 index 00000000000..154566280c7 --- /dev/null +++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-webauthn/index.ts @@ -0,0 +1,2 @@ +export * from "./two-factor-auth-webauthn-component.service"; +export * from "./default-two-factor-auth-webauthn-component.service"; diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-webauthn/two-factor-auth-webauthn-component.service.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-webauthn/two-factor-auth-webauthn-component.service.ts new file mode 100644 index 00000000000..e448d97cc33 --- /dev/null +++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-webauthn/two-factor-auth-webauthn-component.service.ts @@ -0,0 +1,9 @@ +/** + * A service that manages all cross client functionality for the WebAuthn 2FA component. + */ +export abstract class TwoFactorAuthWebAuthnComponentService { + /** + * Determines if the WebAuthn 2FA should be opened in a new tab or can be completed in the current tab. + */ + abstract shouldOpenWebAuthnInNewTab(): boolean; +} diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-webauthn.component.html b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-webauthn/two-factor-auth-webauthn.component.html similarity index 100% rename from libs/angular/src/auth/components/two-factor-auth/two-factor-auth-webauthn.component.html rename to libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-webauthn/two-factor-auth-webauthn.component.html diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-webauthn.component.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-webauthn/two-factor-auth-webauthn.component.ts similarity index 73% rename from libs/angular/src/auth/components/two-factor-auth/two-factor-auth-webauthn.component.ts rename to libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-webauthn/two-factor-auth-webauthn.component.ts index ba3b645c68d..37ddfa42372 100644 --- a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-webauthn.component.ts +++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-webauthn/two-factor-auth-webauthn.component.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { DialogModule } from "@angular/cdk/dialog"; import { CommonModule } from "@angular/common"; import { Component, EventEmitter, Inject, OnDestroy, OnInit, Output } from "@angular/core"; @@ -13,9 +11,9 @@ import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { WebAuthnIFrame } from "@bitwarden/common/auth/webauthn-iframe"; -import { ClientType } from "@bitwarden/common/enums"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ButtonModule, @@ -26,6 +24,8 @@ import { ToastService, } from "@bitwarden/components"; +import { TwoFactorAuthWebAuthnComponentService } from "./two-factor-auth-webauthn-component.service"; + @Component({ standalone: true, selector: "app-two-factor-auth-webauthn", @@ -50,7 +50,7 @@ export class TwoFactorAuthWebAuthnComponent implements OnInit, OnDestroy { webAuthnReady = false; webAuthnNewTab = false; webAuthnSupported = false; - webAuthn: WebAuthnIFrame = null; + webAuthnIframe: WebAuthnIFrame | undefined = undefined; constructor( protected i18nService: I18nService, @@ -60,26 +60,26 @@ export class TwoFactorAuthWebAuthnComponent implements OnInit, OnDestroy { protected twoFactorService: TwoFactorService, protected route: ActivatedRoute, private toastService: ToastService, + private twoFactorAuthWebAuthnComponentService: TwoFactorAuthWebAuthnComponentService, + private logService: LogService, ) { this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win); - - if (this.platformUtilsService.getClientType() == ClientType.Browser) { - // FIXME: Chromium 110 has broken WebAuthn support in extensions via an iframe - this.webAuthnNewTab = true; - } + this.webAuthnNewTab = this.twoFactorAuthWebAuthnComponentService.shouldOpenWebAuthnInNewTab(); } async ngOnInit(): Promise { if (this.route.snapshot.paramMap.has("webAuthnResponse")) { - this.token.emit(this.route.snapshot.paramMap.get("webAuthnResponse")); - } + const webAuthnResponse = this.route.snapshot.paramMap.get("webAuthnResponse"); - this.cleanupWebAuthn(); + if (webAuthnResponse != null) { + this.token.emit(webAuthnResponse); + } + } if (this.win != null && this.webAuthnSupported) { const env = await firstValueFrom(this.environmentService.environment$); const webVaultUrl = env.getWebVaultUrl(); - this.webAuthn = new WebAuthnIFrame( + this.webAuthnIframe = new WebAuthnIFrame( this.win, webVaultUrl, this.webAuthnNewTab, @@ -111,25 +111,30 @@ export class TwoFactorAuthWebAuthnComponent implements OnInit, OnDestroy { } ngOnDestroy(): void { - this.cleanupWebAuthn(); + this.cleanupWebAuthnIframe(); } async authWebAuthn() { - const providerData = (await this.twoFactorService.getProviders()).get( - TwoFactorProviderType.WebAuthn, - ); + const providers = await this.twoFactorService.getProviders(); + + if (providers == null) { + this.logService.error("No 2FA providers found. Unable to authenticate with WebAuthn."); + return; + } + + const providerData = providers?.get(TwoFactorProviderType.WebAuthn); - if (!this.webAuthnSupported || this.webAuthn == null) { + if (!this.webAuthnSupported || this.webAuthnIframe == null) { return; } - this.webAuthn.init(providerData); + this.webAuthnIframe.init(providerData); } - private cleanupWebAuthn() { - if (this.webAuthn != null) { - this.webAuthn.stop(); - this.webAuthn.cleanup(); + private cleanupWebAuthnIframe() { + if (this.webAuthnIframe != null) { + this.webAuthnIframe.stop(); + this.webAuthnIframe.cleanup(); } } } diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-yubikey.component.html b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-yubikey.component.html similarity index 100% rename from libs/angular/src/auth/components/two-factor-auth/two-factor-auth-yubikey.component.html rename to libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-yubikey.component.html diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-yubikey.component.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-yubikey.component.ts similarity index 100% rename from libs/angular/src/auth/components/two-factor-auth/two-factor-auth-yubikey.component.ts rename to libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-yubikey.component.ts diff --git a/libs/auth/src/angular/two-factor-auth/default-two-factor-auth-component.service.ts b/libs/auth/src/angular/two-factor-auth/default-two-factor-auth-component.service.ts new file mode 100644 index 00000000000..579a71aa4b5 --- /dev/null +++ b/libs/auth/src/angular/two-factor-auth/default-two-factor-auth-component.service.ts @@ -0,0 +1,14 @@ +import { + LegacyKeyMigrationAction, + TwoFactorAuthComponentService, +} from "./two-factor-auth-component.service"; + +export class DefaultTwoFactorAuthComponentService implements TwoFactorAuthComponentService { + shouldCheckForWebauthnResponseOnInit() { + return false; + } + + determineLegacyKeyMigrationAction() { + return LegacyKeyMigrationAction.PREVENT_LOGIN_AND_SHOW_REQUIRE_MIGRATION_WARNING; + } +} diff --git a/libs/auth/src/angular/two-factor-auth/index.ts b/libs/auth/src/angular/two-factor-auth/index.ts new file mode 100644 index 00000000000..acf67d94c12 --- /dev/null +++ b/libs/auth/src/angular/two-factor-auth/index.ts @@ -0,0 +1,7 @@ +export * from "./two-factor-auth-component.service"; +export * from "./default-two-factor-auth-component.service"; +export * from "./two-factor-auth.component"; +export * from "./two-factor-auth-expired.component"; +export * from "./two-factor-auth.guard"; + +export * from "./child-components"; diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth-component.service.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth-component.service.ts new file mode 100644 index 00000000000..4b398b5a268 --- /dev/null +++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth-component.service.ts @@ -0,0 +1,54 @@ +import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; + +export enum LegacyKeyMigrationAction { + PREVENT_LOGIN_AND_SHOW_REQUIRE_MIGRATION_WARNING, + NAVIGATE_TO_MIGRATION_COMPONENT, +} + +/** + * Manages all cross client functionality so we can have a single two factor auth component + * implementation for all clients. + */ +export abstract class TwoFactorAuthComponentService { + /** + * Determines if the client should check for a webauthn response on init. + * Currently, only the extension should check on init. + */ + abstract shouldCheckForWebauthnResponseOnInit(): boolean; + + /** + * Extends the popup width if required. + * Some client specific situations require the popup to be wider than the default width. + */ + abstract extendPopupWidthIfRequired?( + selected2faProviderType: TwoFactorProviderType, + ): Promise; + + /** + * Removes the popup width extension. + */ + abstract removePopupWidthExtension?(): void; + + /** + * Optionally closes the window if the client requires it + */ + abstract closeWindow?(): void; + + /** + * We used to use the user's master key to encrypt their data. We deprecated that approach + * and now use a user key. This method should be called if we detect that the user + * is still using the old master key encryption scheme (server sends down a flag to + * indicate this). This method then determines what action to take based on the client. + * + * We have two possible actions: + * 1. Prevent the user from logging in and show a warning that they need to migrate their key on the web client today. + * 2. Navigate the user to the key migration component on the web client. + */ + abstract determineLegacyKeyMigrationAction(): LegacyKeyMigrationAction; + + /** + * Optionally handles the success flow for the SSO + 2FA required flow. + * Only defined on clients that require custom success handling. + */ + abstract handleSso2faFlowSuccess?(): Promise; +} diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-expired.component.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth-expired.component.ts similarity index 100% rename from libs/angular/src/auth/components/two-factor-auth/two-factor-auth-expired.component.ts rename to libs/auth/src/angular/two-factor-auth/two-factor-auth-expired.component.ts diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.html b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.html new file mode 100644 index 00000000000..b1f041e795e --- /dev/null +++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.html @@ -0,0 +1,76 @@ + +
+ +
+
+ + +
+ + + + + + + {{ "rememberMe" | i18n }} + + + +

{{ "noTwoStepProviders" | i18n }}

+

{{ "noTwoStepProviders2" | i18n }}

+
+ +
+ + + + + {{ "cancel" | i18n }} + +
+ + +
diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.spec.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.spec.ts similarity index 77% rename from libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.spec.ts rename to libs/auth/src/angular/two-factor-auth/two-factor-auth.component.spec.ts index 755813a677a..e4c1a16e701 100644 --- a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.spec.ts +++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.spec.ts @@ -19,40 +19,31 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; +import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { - Environment, - EnvironmentService, -} from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { SyncService } from "@bitwarden/common/platform/sync"; import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { UserId } from "@bitwarden/common/types/guid"; import { DialogService, ToastService } from "@bitwarden/components"; +import { TwoFactorAuthComponentService } from "./two-factor-auth-component.service"; import { TwoFactorAuthComponent } from "./two-factor-auth.component"; // test component that extends the TwoFactorAuthComponent @Component({}) class TestTwoFactorComponent extends TwoFactorAuthComponent {} -interface TwoFactorComponentProtected { - trustedDeviceEncRoute: string; - changePasswordRoute: string; - forcePasswordResetRoute: string; - successRoute: string; -} - describe("TwoFactorComponent", () => { let component: TestTwoFactorComponent; - let _component: TwoFactorComponentProtected; let fixture: ComponentFixture; const userId = "userId" as UserId; @@ -64,7 +55,6 @@ describe("TwoFactorComponent", () => { let mockApiService: MockProxy; let mockPlatformUtilsService: MockProxy; let mockWin: MockProxy; - let mockEnvironmentService: MockProxy; let mockStateService: MockProxy; let mockLogService: MockProxy; let mockTwoFactorService: MockProxy; @@ -72,11 +62,13 @@ describe("TwoFactorComponent", () => { let mockLoginEmailService: MockProxy; let mockUserDecryptionOptionsService: MockProxy; let mockSsoLoginService: MockProxy; - let mockConfigService: MockProxy; let mockMasterPasswordService: FakeMasterPasswordService; let mockAccountService: FakeAccountService; let mockDialogService: MockProxy; let mockToastService: MockProxy; + let mockTwoFactorAuthCompService: MockProxy; + let mockSyncService: MockProxy; + let mockMessagingService: MockProxy; let mockUserDecryptionOpts: { noMasterPassword: UserDecryptionOptions; @@ -98,10 +90,6 @@ describe("TwoFactorComponent", () => { mockApiService = mock(); mockPlatformUtilsService = mock(); mockWin = mock(); - const mockEnvironment = mock(); - mockEnvironment.getWebVaultUrl.mockReturnValue("http://example.com"); - mockEnvironmentService = mock(); - mockEnvironmentService.environment$ = new BehaviorSubject(mockEnvironment); mockStateService = mock(); mockLogService = mock(); @@ -110,11 +98,13 @@ describe("TwoFactorComponent", () => { mockLoginEmailService = mock(); mockUserDecryptionOptionsService = mock(); mockSsoLoginService = mock(); - mockConfigService = mock(); mockAccountService = mockAccountServiceWith(userId); mockMasterPasswordService = new FakeMasterPasswordService(); mockDialogService = mock(); mockToastService = mock(); + mockTwoFactorAuthCompService = mock(); + mockSyncService = mock(); + mockMessagingService = mock(); mockUserDecryptionOpts = { noMasterPassword: new UserDecryptionOptions({ @@ -171,7 +161,6 @@ describe("TwoFactorComponent", () => { { provide: ApiService, useValue: mockApiService }, { provide: PlatformUtilsService, useValue: mockPlatformUtilsService }, { provide: WINDOW, useValue: mockWin }, - { provide: EnvironmentService, useValue: mockEnvironmentService }, { provide: StateService, useValue: mockStateService }, { provide: ActivatedRoute, @@ -191,17 +180,18 @@ describe("TwoFactorComponent", () => { useValue: mockUserDecryptionOptionsService, }, { provide: SsoLoginServiceAbstraction, useValue: mockSsoLoginService }, - { provide: ConfigService, useValue: mockConfigService }, { provide: InternalMasterPasswordServiceAbstraction, useValue: mockMasterPasswordService }, { provide: AccountService, useValue: mockAccountService }, { provide: DialogService, useValue: mockDialogService }, { provide: ToastService, useValue: mockToastService }, + { provide: TwoFactorAuthComponentService, useValue: mockTwoFactorAuthCompService }, + { provide: SyncService, useValue: mockSyncService }, + { provide: MessagingService, useValue: mockMessagingService }, ], }); fixture = TestBed.createComponent(TestTwoFactorComponent); component = fixture.componentInstance; - _component = component as any; }); afterEach(() => { @@ -221,9 +211,9 @@ describe("TwoFactorComponent", () => { // Assert expect(mockRouter.navigate).toHaveBeenCalledTimes(1); - expect(mockRouter.navigate).toHaveBeenCalledWith([_component.changePasswordRoute], { + expect(mockRouter.navigate).toHaveBeenCalledWith(["set-password"], { queryParams: { - identifier: component.orgIdentifier, + identifier: component.orgSsoIdentifier, }, }); }); @@ -235,9 +225,9 @@ describe("TwoFactorComponent", () => { await component.submit(); // expect(mockRouter.navigate).toHaveBeenCalledTimes(1); - expect(mockRouter.navigate).toHaveBeenCalledWith([_component.forcePasswordResetRoute], { + expect(mockRouter.navigate).toHaveBeenCalledWith(["update-temp-password"], { queryParams: { - identifier: component.orgIdentifier, + identifier: component.orgSsoIdentifier, }, }); }); @@ -247,14 +237,17 @@ describe("TwoFactorComponent", () => { describe("submit", () => { const token = "testToken"; const remember = false; - const captchaToken = "testCaptchaToken"; + const currentAuthTypeSubject = new BehaviorSubject( + AuthenticationType.Password, + ); beforeEach(() => { component.token = token; component.remember = remember; - component.captchaToken = captchaToken; selectedUserDecryptionOptions.next(mockUserDecryptionOpts.withMasterPassword); + + mockLoginStrategyService.currentAuthType$ = currentAuthTypeSubject.asObservable(); }); it("calls authService.logInTwoFactor with correct parameters when form is submitted", async () => { @@ -267,45 +260,10 @@ describe("TwoFactorComponent", () => { // Assert expect(mockLoginStrategyService.logInTwoFactor).toHaveBeenCalledWith( new TokenTwoFactorRequest(component.selectedProviderType, token, remember), - captchaToken, + null, // captcha token not supported ); }); - it("should return when handleCaptchaRequired returns true", async () => { - // Arrange - const captchaSiteKey = "testCaptchaSiteKey"; - const authResult = new AuthResult(); - authResult.captchaSiteKey = captchaSiteKey; - - mockLoginStrategyService.logInTwoFactor.mockResolvedValue(authResult); - - // Note: the any casts are required b/c typescript cant recognize that - // handleCaptureRequired is a method on TwoFactorComponent b/c it is inherited - // from the CaptchaProtectedComponent - const handleCaptchaRequiredSpy = jest - .spyOn(component, "handleCaptchaRequired") - .mockReturnValue(true); - - // Act - const result = await component.submit(); - - // Assert - expect(handleCaptchaRequiredSpy).toHaveBeenCalled(); - expect(result).toBeUndefined(); - }); - - it("calls onSuccessfulLogin when defined", async () => { - // Arrange - component.onSuccessfulLogin = jest.fn().mockResolvedValue(undefined); - mockLoginStrategyService.logInTwoFactor.mockResolvedValue(new AuthResult()); - - // Act - await component.submit(); - - // Assert - expect(component.onSuccessfulLogin).toHaveBeenCalled(); - }); - it("calls loginEmailService.clearValues() when login is successful", async () => { // Arrange mockLoginStrategyService.logInTwoFactor.mockResolvedValue(new AuthResult()); @@ -341,9 +299,9 @@ describe("TwoFactorComponent", () => { await component.submit(); - expect(mockRouter.navigate).not.toHaveBeenCalledWith([_component.changePasswordRoute], { + expect(mockRouter.navigate).not.toHaveBeenCalledWith(["set-password"], { queryParams: { - identifier: component.orgIdentifier, + identifier: component.orgSsoIdentifier, }, }); }); @@ -369,30 +327,42 @@ describe("TwoFactorComponent", () => { }); }); - it("calls onSuccessfulLoginNavigate when the callback is defined", async () => { - // Arrange - component.onSuccessfulLoginNavigate = jest.fn().mockResolvedValue(undefined); + it("navigates to the component's defined success route (vault is default) when the login is successful", async () => { mockLoginStrategyService.logInTwoFactor.mockResolvedValue(new AuthResult()); // Act await component.submit(); // Assert - expect(component.onSuccessfulLoginNavigate).toHaveBeenCalled(); + expect(mockRouter.navigate).toHaveBeenCalledTimes(1); + expect(mockRouter.navigate).toHaveBeenCalledWith(["vault"], { + queryParams: { + identifier: component.orgSsoIdentifier, + }, + }); }); - it("navigates to the component's defined success route when the login is successful and onSuccessfulLoginNavigate is undefined", async () => { - mockLoginStrategyService.logInTwoFactor.mockResolvedValue(new AuthResult()); + it.each([ + [AuthenticationType.Sso, "lock"], + [AuthenticationType.UserApiKey, "lock"], + ])( + "navigates to the lock component when the authentication type is %s", + async (authType, expectedRoute) => { + mockLoginStrategyService.logInTwoFactor.mockResolvedValue(new AuthResult()); + currentAuthTypeSubject.next(authType); - // Act - await component.submit(); - - // Assert - expect(component.onSuccessfulLoginNavigate).not.toBeDefined(); + // Act + await component.submit(); - expect(mockRouter.navigate).toHaveBeenCalledTimes(1); - expect(mockRouter.navigate).toHaveBeenCalledWith([_component.successRoute], undefined); - }); + // Assert + expect(mockRouter.navigate).toHaveBeenCalledTimes(1); + expect(mockRouter.navigate).toHaveBeenCalledWith(["lock"], { + queryParams: { + identifier: component.orgSsoIdentifier, + }, + }); + }, + ); }); }); @@ -405,17 +375,15 @@ describe("TwoFactorComponent", () => { describe("submit", () => { const token = "testToken"; const remember = false; - const captchaToken = "testCaptchaToken"; beforeEach(() => { component.token = token; component.remember = remember; - component.captchaToken = captchaToken; }); describe("Trusted Device Encryption scenarios", () => { beforeEach(() => { - mockConfigService.getFeatureFlag.mockResolvedValue(true); + mockTwoFactorAuthCompService.closeWindow = undefined; }); describe("Given Trusted Device Encryption is enabled and user needs to set a master password", () => { @@ -428,7 +396,7 @@ describe("TwoFactorComponent", () => { mockLoginStrategyService.logInTwoFactor.mockResolvedValue(authResult); }); - it("navigates to the component's defined trusted device encryption route and sets correct flag when user doesn't have a MP and key connector isn't enabled", async () => { + it("navigates to the login-initiated route and sets correct flag when user doesn't have a MP and key connector isn't enabled", async () => { // Act await component.submit(); @@ -439,10 +407,7 @@ describe("TwoFactorComponent", () => { ); expect(mockRouter.navigate).toHaveBeenCalledTimes(1); - expect(mockRouter.navigate).toHaveBeenCalledWith( - [_component.trustedDeviceEncRoute], - undefined, - ); + expect(mockRouter.navigate).toHaveBeenCalledWith(["login-initiated"]); }); }); @@ -480,23 +445,11 @@ describe("TwoFactorComponent", () => { mockLoginStrategyService.logInTwoFactor.mockResolvedValue(authResult); }); - it("navigates to the component's defined trusted device encryption route when login is successful and onSuccessfulLoginTdeNavigate is undefined", async () => { + it("navigates to the login-initiated route when login is successful", async () => { await component.submit(); expect(mockRouter.navigate).toHaveBeenCalledTimes(1); - expect(mockRouter.navigate).toHaveBeenCalledWith( - [_component.trustedDeviceEncRoute], - undefined, - ); - }); - - it("calls onSuccessfulLoginTdeNavigate instead of router.navigate when the callback is defined", async () => { - component.onSuccessfulLoginTdeNavigate = jest.fn().mockResolvedValue(undefined); - - await component.submit(); - - expect(mockRouter.navigate).not.toHaveBeenCalled(); - expect(component.onSuccessfulLoginTdeNavigate).toHaveBeenCalled(); + expect(mockRouter.navigate).toHaveBeenCalledWith(["login-initiated"]); }); }); }); diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts similarity index 52% rename from libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.ts rename to libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts index 6aca189a79e..cf77c70ecee 100644 --- a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.ts +++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts @@ -1,10 +1,9 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { CommonModule } from "@angular/common"; -import { Component, Inject, OnInit, ViewChild } from "@angular/core"; +import { Component, DestroyRef, Inject, OnDestroy, OnInit, ViewChild } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms"; -import { ActivatedRoute, NavigationExtras, Router, RouterLink } from "@angular/router"; -import { Subject, takeUntil, lastValueFrom, first, firstValueFrom } from "rxjs"; +import { ActivatedRoute, Router, RouterLink } from "@angular/router"; +import { lastValueFrom, firstValueFrom } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; @@ -26,26 +25,29 @@ import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { TwoFactorProviders } from "@bitwarden/common/auth/services/two-factor.service"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SyncService } from "@bitwarden/common/platform/sync"; import { AsyncActionsModule, ButtonModule, + CheckboxModule, DialogService, FormFieldModule, ToastService, } from "@bitwarden/components"; -import { CaptchaProtectedComponent } from "../captcha-protected.component"; - -import { TwoFactorAuthAuthenticatorComponent } from "./two-factor-auth-authenticator.component"; -import { TwoFactorAuthDuoComponent } from "./two-factor-auth-duo.component"; -import { TwoFactorAuthEmailComponent } from "./two-factor-auth-email.component"; -import { TwoFactorAuthWebAuthnComponent } from "./two-factor-auth-webauthn.component"; -import { TwoFactorAuthYubikeyComponent } from "./two-factor-auth-yubikey.component"; +import { TwoFactorAuthAuthenticatorComponent } from "./child-components/two-factor-auth-authenticator.component"; +import { TwoFactorAuthDuoComponent } from "./child-components/two-factor-auth-duo/two-factor-auth-duo.component"; +import { TwoFactorAuthEmailComponent } from "./child-components/two-factor-auth-email/two-factor-auth-email.component"; +import { TwoFactorAuthWebAuthnComponent } from "./child-components/two-factor-auth-webauthn/two-factor-auth-webauthn.component"; +import { TwoFactorAuthYubikeyComponent } from "./child-components/two-factor-auth-yubikey.component"; +import { + LegacyKeyMigrationAction, + TwoFactorAuthComponentService, +} from "./two-factor-auth-component.service"; import { TwoFactorOptionsDialogResult, TwoFactorOptionsComponent, @@ -63,8 +65,9 @@ import { FormFieldModule, AsyncActionsModule, RouterLink, + CheckboxModule, ButtonModule, - TwoFactorOptionsComponent, + TwoFactorOptionsComponent, // used as dialog TwoFactorAuthAuthenticatorComponent, TwoFactorAuthEmailComponent, TwoFactorAuthDuoComponent, @@ -73,18 +76,23 @@ import { ], providers: [I18nPipe], }) -export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements OnInit { - token = ""; +export class TwoFactorAuthComponent implements OnInit, OnDestroy { + loading = true; + + token: string | undefined = undefined; remember = false; - orgIdentifier: string = null; + orgSsoIdentifier: string | undefined = undefined; + inSsoFlow = false; providers = TwoFactorProviders; providerType = TwoFactorProviderType; selectedProviderType: TwoFactorProviderType = TwoFactorProviderType.Authenticator; - providerData: any; + // TODO: PM-17176 - build more specific type for 2FA metadata + providerData: { [key: string]: string } | undefined; @ViewChild("duoComponent") duoComponent!: TwoFactorAuthDuoComponent; - formGroup = this.formBuilder.group({ + + form = this.formBuilder.group({ token: [ "", { @@ -94,99 +102,116 @@ export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements ], remember: [false], }); - actionButtonText = ""; - title = ""; - formPromise: Promise; - private destroy$ = new Subject(); - - onSuccessfulLogin: () => Promise; - onSuccessfulLoginNavigate: () => Promise; + title = ""; - onSuccessfulLoginTde: () => Promise; - onSuccessfulLoginTdeNavigate: () => Promise; + formPromise: Promise | undefined; submitForm = async () => { await this.submit(); }; - goAfterLogIn = async () => { - this.loginEmailService.clearValues(); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate([this.successRoute], { - queryParams: { - identifier: this.orgIdentifier, - }, - }); - }; - protected loginRoute = "login"; - - protected trustedDeviceEncRoute = "login-initiated"; - protected changePasswordRoute = "set-password"; - protected forcePasswordResetRoute = "update-temp-password"; - protected successRoute = "vault"; + private twoFactorSessionTimeoutRoute = "2fa-timeout"; constructor( - protected loginStrategyService: LoginStrategyServiceAbstraction, - protected router: Router, - i18nService: I18nService, - platformUtilsService: PlatformUtilsService, - environmentService: EnvironmentService, + private loginStrategyService: LoginStrategyServiceAbstraction, + private router: Router, + private i18nService: I18nService, + private platformUtilsService: PlatformUtilsService, private dialogService: DialogService, - protected route: ActivatedRoute, + private activatedRoute: ActivatedRoute, private logService: LogService, - protected twoFactorService: TwoFactorService, + private twoFactorService: TwoFactorService, private loginEmailService: LoginEmailServiceAbstraction, private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, - protected ssoLoginService: SsoLoginServiceAbstraction, - protected configService: ConfigService, + private ssoLoginService: SsoLoginServiceAbstraction, private masterPasswordService: InternalMasterPasswordServiceAbstraction, private accountService: AccountService, private formBuilder: FormBuilder, @Inject(WINDOW) protected win: Window, - protected toastService: ToastService, - ) { - super(environmentService, i18nService, platformUtilsService, toastService); - } + private toastService: ToastService, + private twoFactorAuthComponentService: TwoFactorAuthComponentService, + private syncService: SyncService, + private messagingService: MessagingService, + private destroyRef: DestroyRef, + ) {} async ngOnInit() { - if (!(await this.authing()) || (await this.twoFactorService.getProviders()) == null) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate([this.loginRoute]); - return; + this.inSsoFlow = this.activatedRoute.snapshot.queryParamMap.get("sso") === "true"; + + this.orgSsoIdentifier = + this.activatedRoute.snapshot.queryParamMap.get("identifier") ?? undefined; + + this.listenFor2faSessionTimeout(); + + if (this.twoFactorAuthComponentService.shouldCheckForWebauthnResponseOnInit()) { + await this.processWebAuthnResponseIfExists(); } - // eslint-disable-next-line rxjs-angular/prefer-takeuntil - this.route.queryParams.pipe(first()).subscribe((qParams) => { - if (qParams.identifier != null) { - this.orgIdentifier = qParams.identifier; + await this.setSelected2faProviderType(); + await this.set2faProviderData(); + await this.setTitleByTwoFactorProviderType(); + + this.form.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((value) => { + if (value.token) { + this.token = value.token; + } + + if (value.remember) { + this.remember = value.remember; } }); - if (await this.needsLock()) { - this.successRoute = "lock"; + await this.twoFactorAuthComponentService.extendPopupWidthIfRequired?.( + this.selectedProviderType, + ); + + this.loading = false; + } + + private async processWebAuthnResponseIfExists() { + const webAuthn2faResponse = this.activatedRoute.snapshot.queryParamMap.get("webAuthnResponse"); + if (webAuthn2faResponse) { + this.selectedProviderType = TwoFactorProviderType.WebAuthn; + await this.set2faProviderData(); + this.token = webAuthn2faResponse; + this.remember = this.activatedRoute.snapshot.queryParamMap.get("remember") === "true"; + await this.submit(); } + } + private async setSelected2faProviderType() { const webAuthnSupported = this.platformUtilsService.supportsWebAuthn(this.win); this.selectedProviderType = await this.twoFactorService.getDefaultProvider(webAuthnSupported); + } + + private async set2faProviderData() { const providerData = await this.twoFactorService.getProviders().then((providers) => { - return providers.get(this.selectedProviderType); + return providers?.get(this.selectedProviderType); }); this.providerData = providerData; - await this.updateUIToProviderData(); + } - this.actionButtonText = this.i18nService.t("continue"); - this.formGroup.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((value) => { - this.token = value.token; - this.remember = value.remember; - }); + private listenFor2faSessionTimeout() { + this.loginStrategyService.twoFactorTimeout$ + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(async (expired) => { + if (!expired) { + return; + } + + try { + await this.router.navigate([this.twoFactorSessionTimeoutRoute]); + } catch (err) { + this.logService.error( + `Failed to navigate to ${this.twoFactorSessionTimeoutRoute} route`, + err, + ); + } + }); } async submit() { - await this.setupCaptcha(); - if (this.token == null || this.token === "") { this.toastService.showToast({ variant: "error", @@ -199,11 +224,10 @@ export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements try { this.formPromise = this.loginStrategyService.logInTwoFactor( new TokenTwoFactorRequest(this.selectedProviderType, this.token, this.remember), - this.captchaToken, ); const authResult: AuthResult = await this.formPromise; this.logService.info("Successfully submitted two factor token"); - await this.handleLoginResponse(authResult); + await this.handleAuthResult(authResult); } catch { this.logService.error("Error submitting two factor token"); this.toastService.showToast({ @@ -216,14 +240,21 @@ export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements async selectOtherTwofactorMethod() { const dialogRef = TwoFactorOptionsComponent.open(this.dialogService); - const response: TwoFactorOptionsDialogResultType = await lastValueFrom(dialogRef.closed); - if (response.result === TwoFactorOptionsDialogResult.Provider) { + const response: TwoFactorOptionsDialogResultType | undefined = await lastValueFrom( + dialogRef.closed, + ); + + if ( + response !== undefined && + response !== null && + response.result === TwoFactorOptionsDialogResult.Provider + ) { const providerData = await this.twoFactorService.getProviders().then((providers) => { - return providers.get(response.type); + return providers?.get(response.type); }); this.providerData = providerData; this.selectedProviderType = response.type; - await this.updateUIToProviderData(); + await this.setTitleByTwoFactorProviderType(); } } @@ -233,17 +264,30 @@ export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements } } - protected handleMigrateEncryptionKey(result: AuthResult): boolean { + protected async handleMigrateEncryptionKey(result: AuthResult): Promise { if (!result.requiresEncryptionKeyMigration) { return false; } - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["migrate-legacy-encryption"]); + // Migration is forced so prevent login via return + const legacyKeyMigrationAction: LegacyKeyMigrationAction = + this.twoFactorAuthComponentService.determineLegacyKeyMigrationAction(); + + switch (legacyKeyMigrationAction) { + case LegacyKeyMigrationAction.NAVIGATE_TO_MIGRATION_COMPONENT: + await this.router.navigate(["migrate-legacy-encryption"]); + break; + case LegacyKeyMigrationAction.PREVENT_LOGIN_AND_SHOW_REQUIRE_MIGRATION_WARNING: + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccured"), + message: this.i18nService.t("encryptionKeyMigrationRequired"), + }); + break; + } return true; } - async updateUIToProviderData() { + async setTitleByTwoFactorProviderType() { if (this.selectedProviderType == null) { this.title = this.i18nService.t("loginUnavailable"); return; @@ -252,22 +296,25 @@ export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements this.title = (TwoFactorProviders as any)[this.selectedProviderType].name; } - private async handleLoginResponse(authResult: AuthResult) { - if (this.handleCaptchaRequired(authResult)) { - return; - } else if (this.handleMigrateEncryptionKey(authResult)) { - return; + private async handleAuthResult(authResult: AuthResult) { + if (await this.handleMigrateEncryptionKey(authResult)) { + return; // stop login process } + // User is fully logged in so handle any post login logic before executing navigation + await this.syncService.fullSync(true); + this.loginEmailService.clearValues(); + // Save off the OrgSsoIdentifier for use in the TDE flows // - TDE login decryption options component // - Browser SSO on extension open - await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(this.orgIdentifier); - this.loginEmailService.clearValues(); + if (this.orgSsoIdentifier !== undefined) { + await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(this.orgSsoIdentifier); + } // note: this flow affects both TDE & standard users if (this.isForcePasswordResetRequired(authResult)) { - return await this.handleForcePasswordReset(this.orgIdentifier); + return await this.handleForcePasswordReset(this.orgSsoIdentifier); } const userDecryptionOpts = await firstValueFrom( @@ -277,11 +324,7 @@ export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements const tdeEnabled = await this.isTrustedDeviceEncEnabled(userDecryptionOpts.trustedDeviceOption); if (tdeEnabled) { - return await this.handleTrustedDeviceEncryptionEnabled( - authResult, - this.orgIdentifier, - userDecryptionOpts, - ); + return await this.handleTrustedDeviceEncryptionEnabled(userDecryptionOpts); } // User must set password if they don't have one and they aren't using either TDE or key connector. @@ -290,60 +333,78 @@ export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements if (requireSetPassword || authResult.resetMasterPassword) { // Change implies going no password -> password in this case - return await this.handleChangePasswordRequired(this.orgIdentifier); + return await this.handleChangePasswordRequired(this.orgSsoIdentifier); + } + + // if we are in the SSO flow and we have a custom success handler, call it + if ( + this.inSsoFlow && + this.twoFactorAuthComponentService.handleSso2faFlowSuccess !== undefined + ) { + await this.twoFactorAuthComponentService.handleSso2faFlowSuccess(); + return; + } + + const defaultSuccessRoute = await this.determineDefaultSuccessRoute(); + + await this.router.navigate([defaultSuccessRoute], { + queryParams: { + identifier: this.orgSsoIdentifier, + }, + }); + } + + private async determineDefaultSuccessRoute(): Promise { + const authType = await firstValueFrom(this.loginStrategyService.currentAuthType$); + if (authType == AuthenticationType.Sso || authType == AuthenticationType.UserApiKey) { + return "lock"; } - return await this.handleSuccessfulLogin(); + return "vault"; } private async isTrustedDeviceEncEnabled( - trustedDeviceOption: TrustedDeviceUserDecryptionOption, + trustedDeviceOption: TrustedDeviceUserDecryptionOption | undefined, ): Promise { - const ssoTo2faFlowActive = this.route.snapshot.queryParamMap.get("sso") === "true"; + const ssoTo2faFlowActive = this.activatedRoute.snapshot.queryParamMap.get("sso") === "true"; return ssoTo2faFlowActive && trustedDeviceOption !== undefined; } private async handleTrustedDeviceEncryptionEnabled( - authResult: AuthResult, - orgIdentifier: string, userDecryptionOpts: UserDecryptionOptions, ): Promise { // If user doesn't have a MP, but has reset password permission, they must set a MP if ( !userDecryptionOpts.hasMasterPassword && - userDecryptionOpts.trustedDeviceOption.hasManageResetPasswordPermission + userDecryptionOpts.trustedDeviceOption?.hasManageResetPasswordPermission ) { // Set flag so that auth guard can redirect to set password screen after decryption (trusted or untrusted device) // Note: we cannot directly navigate to the set password screen in this scenario as we are in a pre-decryption state, and // if you try to set a new MP before decrypting, you will invalidate the user's data by making a new user key. const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + + if (!userId) { + this.logService.error("User ID not found when setting TDE force set password reason"); + return; + } + await this.masterPasswordService.setForceSetPasswordReason( ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission, userId, ); } - if (this.onSuccessfulLoginTde != null) { - // Note: awaiting this will currently cause a hang on desktop & browser as they will wait for a full sync to complete - // before navigating to the success route. - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.onSuccessfulLoginTde(); + if (this.twoFactorAuthComponentService.handleSso2faFlowSuccess !== undefined) { + await this.twoFactorAuthComponentService.handleSso2faFlowSuccess(); + return; } - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.navigateViaCallbackOrRoute( - this.onSuccessfulLoginTdeNavigate, - // Navigate to TDE page (if user was on trusted device and TDE has decrypted - // their user key, the login-initiated guard will redirect them to the vault) - [this.trustedDeviceEncRoute], - ); + await this.router.navigate(["login-initiated"]); } - private async handleChangePasswordRequired(orgIdentifier: string) { - await this.router.navigate([this.changePasswordRoute], { + private async handleChangePasswordRequired(orgIdentifier: string | undefined) { + await this.router.navigate(["set-password"], { queryParams: { identifier: orgIdentifier, }, @@ -369,45 +430,24 @@ export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements return forceResetReasons.includes(authResult.forcePasswordReset); } - private async handleForcePasswordReset(orgIdentifier: string) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate([this.forcePasswordResetRoute], { + private async handleForcePasswordReset(orgIdentifier: string | undefined) { + await this.router.navigate(["update-temp-password"], { queryParams: { identifier: orgIdentifier, }, }); } - private async handleSuccessfulLogin() { - if (this.onSuccessfulLogin != null) { - // Note: awaiting this will currently cause a hang on desktop & browser as they will wait for a full sync to complete - // before navigating to the success route. - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.onSuccessfulLogin(); - } - await this.navigateViaCallbackOrRoute(this.onSuccessfulLoginNavigate, [this.successRoute]); - } - - private async navigateViaCallbackOrRoute( - callback: () => Promise, - commands: unknown[], - extras?: NavigationExtras, - ): Promise { - if (callback) { - await callback(); - } else { - await this.router.navigate(commands, extras); - } - } - - private async authing(): Promise { - return (await firstValueFrom(this.loginStrategyService.currentAuthType$)) !== null; + showContinueButton() { + return ( + this.selectedProviderType != null && + this.selectedProviderType !== TwoFactorProviderType.WebAuthn && + this.selectedProviderType !== TwoFactorProviderType.Duo && + this.selectedProviderType !== TwoFactorProviderType.OrganizationDuo + ); } - private async needsLock(): Promise { - const authType = await firstValueFrom(this.loginStrategyService.currentAuthType$); - return authType == AuthenticationType.Sso || authType == AuthenticationType.UserApiKey; + async ngOnDestroy() { + this.twoFactorAuthComponentService.removePopupWidthExtension?.(); } } diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth.guard.spec.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth.guard.spec.ts new file mode 100644 index 00000000000..22cfe0820ef --- /dev/null +++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth.guard.spec.ts @@ -0,0 +1,74 @@ +import { Component } from "@angular/core"; +import { TestBed } from "@angular/core/testing"; +import { provideRouter, Router } from "@angular/router"; +import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; + +import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; +import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type"; + +import { LoginStrategyServiceAbstraction } from "../../common"; + +import { TwoFactorAuthGuard } from "./two-factor-auth.guard"; + +@Component({ template: "" }) +export class EmptyComponent {} + +describe("TwoFactorAuthGuard", () => { + let loginStrategyService: MockProxy; + const currentAuthTypesSubject = new BehaviorSubject(null); + + let twoFactorService: MockProxy; + let router: Router; + + beforeEach(() => { + loginStrategyService = mock(); + loginStrategyService.currentAuthType$ = currentAuthTypesSubject.asObservable(); + + twoFactorService = mock(); + + TestBed.configureTestingModule({ + providers: [ + provideRouter([ + { path: "login", component: EmptyComponent }, + { path: "protected", component: EmptyComponent, canActivate: [TwoFactorAuthGuard] }, + ]), + { provide: LoginStrategyServiceAbstraction, useValue: loginStrategyService }, + { provide: TwoFactorService, useValue: twoFactorService }, + ], + }); + + router = TestBed.inject(Router); + }); + + it("should redirect to /login if the user is not authenticating", async () => { + // Arrange + currentAuthTypesSubject.next(null); + twoFactorService.getProviders.mockResolvedValue(null); + + // Act + await router.navigateByUrl("/protected"); + + // Assert + expect(router.url).toBe("/login"); + }); + + const authenticationTypes = Object.entries(AuthenticationType) + // filter out reverse mappings (e.g., "0": "Password") + .filter(([key, value]) => typeof value === "number") + .map(([key, value]) => [value, key]) as [AuthenticationType, string][]; + + authenticationTypes.forEach(([authType, authTypeName]) => { + it(`should redirect to /login if the user is authenticating with ${authTypeName} but no two-factor providers exist`, async () => { + // Arrange + currentAuthTypesSubject.next(authType); + twoFactorService.getProviders.mockResolvedValue(null); + + // Act + await router.navigateByUrl("/protected"); + + // Assert + expect(router.url).toBe("/login"); + }); + }); +}); diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth.guard.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth.guard.ts new file mode 100644 index 00000000000..2aec0bae441 --- /dev/null +++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth.guard.ts @@ -0,0 +1,33 @@ +import { inject } from "@angular/core"; +import { + ActivatedRouteSnapshot, + CanActivateFn, + Router, + RouterStateSnapshot, + UrlTree, +} from "@angular/router"; +import { firstValueFrom } from "rxjs"; + +import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; + +import { LoginStrategyServiceAbstraction } from "../../common"; + +export const TwoFactorAuthGuard: CanActivateFn = async ( + route: ActivatedRouteSnapshot, + routerState: RouterStateSnapshot, +): Promise => { + const loginStrategyService = inject(LoginStrategyServiceAbstraction); + const twoFactorService = inject(TwoFactorService); + const router = inject(Router); + + const currentAuthType = await firstValueFrom(loginStrategyService.currentAuthType$); + const userIsAuthenticating = currentAuthType !== null; + + const twoFactorProviders = await twoFactorService.getProviders(); + + if (!userIsAuthenticating || twoFactorProviders == null) { + return router.createUrlTree(["/login"]); + } + + return true; +}; diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-options.component.html b/libs/auth/src/angular/two-factor-auth/two-factor-options.component.html similarity index 100% rename from libs/angular/src/auth/components/two-factor-auth/two-factor-options.component.html rename to libs/auth/src/angular/two-factor-auth/two-factor-options.component.html diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-options.component.ts b/libs/auth/src/angular/two-factor-auth/two-factor-options.component.ts similarity index 100% rename from libs/angular/src/auth/components/two-factor-auth/two-factor-options.component.ts rename to libs/auth/src/angular/two-factor-auth/two-factor-options.component.ts diff --git a/libs/auth/src/common/abstractions/login-strategy.service.ts b/libs/auth/src/common/abstractions/login-strategy.service.ts index 1088d6de736..4e0a4dffde9 100644 --- a/libs/auth/src/common/abstractions/login-strategy.service.ts +++ b/libs/auth/src/common/abstractions/login-strategy.service.ts @@ -67,7 +67,8 @@ export abstract class LoginStrategyServiceAbstraction { */ logInTwoFactor: ( twoFactor: TokenTwoFactorRequest, - captchaResponse: string, + // TODO: PM-15162 - deprecate captchaResponse + captchaResponse?: string, ) => Promise; /** * Creates a master key from the provided master password and email. diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts index 57a653b205e..d3441a7f3f8 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts @@ -232,7 +232,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { async logInTwoFactor( twoFactor: TokenTwoFactorRequest, - captchaResponse: string, + captchaResponse?: string, ): Promise { if (!(await this.isSessionValid())) { throw new Error(this.i18nService.t("sessionTimeout")); diff --git a/libs/common/src/auth/abstractions/two-factor.service.ts b/libs/common/src/auth/abstractions/two-factor.service.ts index 00987dabd98..8ba959e502f 100644 --- a/libs/common/src/auth/abstractions/two-factor.service.ts +++ b/libs/common/src/auth/abstractions/two-factor.service.ts @@ -11,15 +11,20 @@ export interface TwoFactorProviderDetails { sort: number; premium: boolean; } - export abstract class TwoFactorService { - init: () => void; - getSupportedProviders: (win: Window) => Promise; - getDefaultProvider: (webAuthnSupported: boolean) => Promise; - setSelectedProvider: (type: TwoFactorProviderType) => Promise; - clearSelectedProvider: () => Promise; - - setProviders: (response: IdentityTwoFactorResponse) => Promise; - clearProviders: () => Promise; - getProviders: () => Promise>; + abstract init(): void; + + abstract getSupportedProviders(win: Window): Promise; + + abstract getDefaultProvider(webAuthnSupported: boolean): Promise; + + abstract setSelectedProvider(type: TwoFactorProviderType): Promise; + + abstract clearSelectedProvider(): Promise; + + abstract setProviders(response: IdentityTwoFactorResponse): Promise; + + abstract clearProviders(): Promise; + + abstract getProviders(): Promise | null>; } diff --git a/libs/common/src/auth/services/two-factor.service.ts b/libs/common/src/auth/services/two-factor.service.ts index 3826ffaaa22..83e113268a2 100644 --- a/libs/common/src/auth/services/two-factor.service.ts +++ b/libs/common/src/auth/services/two-factor.service.ts @@ -206,7 +206,7 @@ export class TwoFactorService implements TwoFactorServiceAbstraction { await this.providersState.update(() => null); } - getProviders(): Promise> { + getProviders(): Promise | null> { return firstValueFrom(this.providers$); } } diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index feffe2ca442..5347f93fddd 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -23,7 +23,6 @@ export enum FeatureFlag { PersistPopupView = "persist-popup-view", PM4154_BulkEncryptionService = "PM-4154-bulk-encryption-service", EmailVerification = "email-verification", - TwoFactorComponentRefactor = "two-factor-component-refactor", ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner", VaultBulkManagementAction = "vault-bulk-management-action", IdpAutoSubmitLogin = "idp-auto-submit-login", @@ -80,7 +79,6 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.PersistPopupView]: FALSE, [FeatureFlag.PM4154_BulkEncryptionService]: FALSE, [FeatureFlag.EmailVerification]: FALSE, - [FeatureFlag.TwoFactorComponentRefactor]: FALSE, [FeatureFlag.ProviderClientVaultPrivacyBanner]: FALSE, [FeatureFlag.VaultBulkManagementAction]: FALSE, [FeatureFlag.IdpAutoSubmitLogin]: FALSE,