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 0917b2703cf..00000000000
--- a/apps/browser/src/auth/popup/two-factor-auth-duo.component.ts
+++ /dev/null
@@ -1,110 +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";
-
-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 { I18nPipe } from "../../../../../libs/components/src/shared/i18n.pipe";
-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 b6211bba05f..00000000000
--- a/apps/browser/src/auth/popup/two-factor-auth-email.component.ts
+++ /dev/null
@@ -1,55 +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";
-
-import { AsyncActionsModule } from "../../../../../libs/components/src/async-actions";
-import { ButtonModule } from "../../../../../libs/components/src/button";
-import { DialogService } from "../../../../../libs/components/src/dialog";
-import { FormFieldModule } from "../../../../../libs/components/src/form-field";
-import { LinkModule } from "../../../../../libs/components/src/link";
-import { I18nPipe } from "../../../../../libs/components/src/shared/i18n.pipe";
-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 3cb82118597..00000000000
--- a/apps/browser/src/auth/popup/two-factor-auth.component.ts
+++ /dev/null
@@ -1,168 +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";
-
-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..8def30023e9
--- /dev/null
+++ b/apps/browser/src/auth/services/extension-two-factor-auth-component.service.ts
@@ -0,0 +1,53 @@
+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)
+ 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..93041028075
--- /dev/null
+++ b/apps/browser/src/auth/services/extension-two-factor-auth-duo-component.service.ts
@@ -0,0 +1,55 @@
+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: Message) => msg.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/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts
index ad839bbd7ce..e8595b624a1 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 {
@@ -42,6 +41,9 @@ import {
DevicesIcon,
SsoComponent,
TwoFactorTimeoutIcon,
+ TwoFactorAuthComponent,
+ TwoFactorTimeoutComponent,
+ TwoFactorAuthGuard,
} from "@bitwarden/auth/angular";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import {
@@ -50,7 +52,6 @@ import {
VaultIcons,
} from "@bitwarden/vault";
-import { twofactorRefactorSwap } from "../../../../libs/angular/src/utils/two-factor-component-refactor-route-swap";
import { fido2AuthGuard } from "../auth/guards/fido2-auth.guard";
import { AccountSwitcherComponent } from "../auth/popup/account-switching/account-switcher.component";
import { EnvironmentComponent } from "../auth/popup/environment.component";
@@ -70,9 +71,8 @@ import { SetPasswordComponent } from "../auth/popup/set-password.component";
import { AccountSecurityComponent as AccountSecurityV1Component } from "../auth/popup/settings/account-security-v1.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 { Fido2V1Component } from "../autofill/popup/fido2/fido2-v1.component";
import { Fido2Component } from "../autofill/popup/fido2/fido2.component";
@@ -183,9 +183,9 @@ const routes: Routes = [
canMatch: [extensionRefreshRedirect("/lockV2")],
data: { elevation: 1, doNotSaveUrl: true } satisfies RouteDataProperties,
},
- ...twofactorRefactorSwap(
- TwoFactorComponent,
- AnonLayoutWrapperComponent,
+ ...unauthUiRefreshSwap(
+ TwoFactorComponentV1,
+ ExtensionAnonLayoutWrapperComponent,
{
path: "2fa",
canActivate: [unauthGuardFn(unauthRouteOverrides)],
@@ -193,7 +193,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 76bd06565c7..15fbfd66d19 100644
--- a/apps/browser/src/popup/app.module.ts
+++ b/apps/browser/src/popup/app.module.ts
@@ -35,7 +35,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 { Fido2CipherRowV1Component } from "../autofill/popup/fido2/fido2-cipher-row-v1.component";
import { Fido2CipherRowComponent } from "../autofill/popup/fido2/fido2-cipher-row.component";
@@ -179,7 +179,7 @@ import "../platform/popup/locales";
SyncComponent,
TabsComponent,
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 7014d908ac3..b5986b9d715 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 {
@@ -27,6 +28,9 @@ import {
LockComponentService,
SsoComponentService,
LoginDecryptionOptionsService,
+ TwoFactorAuthComponentService,
+ TwoFactorAuthEmailComponentService,
+ TwoFactorAuthDuoComponentService,
} from "@bitwarden/auth/angular";
import { LockService, LoginEmailService, PinServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
@@ -122,6 +126,9 @@ 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 { 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 +137,7 @@ import { BrowserKeyService } from "../../key-management/browser-key.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";
@@ -537,6 +545,26 @@ const safeProviders: SafeProvider[] = [
useClass: ExtensionLockComponentService,
deps: [],
}),
+ safeProvider({
+ provide: TwoFactorAuthComponentService,
+ useClass: ExtensionTwoFactorAuthComponentService,
+ deps: [WINDOW],
+ }),
+ safeProvider({
+ provide: TwoFactorAuthEmailComponentService,
+ useClass: ExtensionTwoFactorAuthEmailComponentService,
+ deps: [DialogService, WINDOW],
+ }),
+ 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 c7642638dc3..8cea1bc546b 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,
@@ -39,6 +38,9 @@ import {
DevicesIcon,
SsoComponent,
TwoFactorTimeoutIcon,
+ TwoFactorAuthComponent,
+ TwoFactorTimeoutComponent,
+ TwoFactorAuthGuard,
} from "@bitwarden/auth/angular";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import {
@@ -47,7 +49,6 @@ import {
VaultIcons,
} from "@bitwarden/vault";
-import { twofactorRefactorSwap } from "../../../../libs/angular/src/utils/two-factor-component-refactor-route-swap";
import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component";
import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard";
import { HintComponent } from "../auth/hint.component";
@@ -59,8 +60,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";
@@ -87,8 +87,16 @@ const routes: Routes = [
canActivate: [lockGuard()],
canMatch: [extensionRefreshRedirect("/lockV2")],
},
- ...twofactorRefactorSwap(
- TwoFactorComponent,
+ {
+ path: "login-with-device",
+ component: LoginViaAuthRequestComponent,
+ },
+ {
+ path: "admin-approval-requested",
+ component: LoginViaAuthRequestComponent,
+ },
+ ...unauthUiRefreshSwap(
+ TwoFactorComponentV1,
AnonLayoutWrapperComponent,
{
path: "2fa",
@@ -96,11 +104,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 5bd1c66b87c..9616d6d5777 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 0f541907995..626730a8f51 100644
--- a/apps/desktop/src/app/services/services.module.ts
+++ b/apps/desktop/src/app/services/services.module.ts
@@ -27,6 +27,7 @@ import {
LockComponentService,
SsoComponentService,
DefaultSsoComponentService,
+ TwoFactorAuthDuoComponentService,
} from "@bitwarden/auth/angular";
import {
InternalUserDecryptionOptionsServiceAbstraction,
@@ -99,6 +100,7 @@ import {
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";
@@ -380,6 +382,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 72137dc5364..00000000000
--- a/apps/desktop/src/auth/two-factor-auth-duo.component.ts
+++ /dev/null
@@ -1,115 +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";
-
-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 29271b565c1..00000000000
--- a/apps/desktop/src/auth/two-factor-auth.component.ts
+++ /dev/null
@@ -1,50 +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";
-
-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 { JslibModule } from "../../../../libs/angular/src/jslib.module";
-import { AsyncActionsModule } from "../../../../libs/components/src/async-actions";
-import { ButtonModule } from "../../../../libs/components/src/button";
-import { CheckboxModule } from "../../../../libs/components/src/checkbox";
-import { FormFieldModule } from "../../../../libs/components/src/form-field";
-import { LinkModule } from "../../../../libs/components/src/link";
-import { I18nPipe } from "../../../../libs/components/src/shared/i18n.pipe";
-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/config/development.json b/apps/web/config/development.json
index f0a15f4f4d6..a02f00bd07b 100644
--- a/apps/web/config/development.json
+++ b/apps/web/config/development.json
@@ -20,5 +20,7 @@
}
],
"flags": {},
- "devFlags": {}
+ "devFlags": {
+ "configRetrievalIntervalMs": 10000
+ }
}
diff --git a/apps/web/src/app/auth/core/services/index.ts b/apps/web/src/app/auth/core/services/index.ts
index c14292d7c6d..c6f6572486d 100644
--- a/apps/web/src/app/auth/core/services/index.ts
+++ b/apps/web/src/app/auth/core/services/index.ts
@@ -3,4 +3,5 @@ export * from "./login-decryption-options";
export * from "./webauthn-login";
export * from "./set-password-jit";
export * from "./registration";
+export * from "./two-factor-auth";
export * from "./web-lock-component.service";
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..a99305627b2
--- /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 2dd1db9fdb6..c3dd797a20b 100644
--- a/apps/web/src/app/core/core.module.ts
+++ b/apps/web/src/app/core/core.module.ts
@@ -34,6 +34,8 @@ import {
SetPasswordJitService,
SsoComponentService,
LoginDecryptionOptionsService,
+ TwoFactorAuthComponentService,
+ TwoFactorAuthDuoComponentService,
} from "@bitwarden/auth/angular";
import {
InternalUserDecryptionOptionsServiceAbstraction,
@@ -101,6 +103,8 @@ import {
WebLoginComponentService,
WebLockComponentService,
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 9f2a86c1c06..6b9d1968dd9 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,
@@ -39,6 +38,9 @@ import {
SsoComponent,
VaultIcon,
LoginDecryptionOptionsComponent,
+ TwoFactorAuthComponent,
+ TwoFactorTimeoutComponent,
+ TwoFactorAuthGuard,
} from "@bitwarden/auth/angular";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import {
@@ -47,7 +49,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";
@@ -74,8 +75,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";
@@ -547,25 +547,51 @@ const routes: Routes = [
} satisfies AnonLayoutWrapperData,
},
),
- {
- path: "2fa",
- canActivate: [unauthGuardFn()],
- children: [
- ...twofactorRefactorSwap(TwoFactorComponent, TwoFactorAuthComponent, {
- path: "",
- }),
- {
- path: "",
- component: EnvironmentSelectorComponent,
- outlet: "environment-selector",
- },
- ],
- data: {
- pageTitle: {
- key: "verifyIdentity",
- },
- } satisfies RouteDataProperties & AnonLayoutWrapperData,
- },
+ ...extensionRefreshSwap(
+ 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: "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.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 18bfe546600..9e047ae6b8a 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 0765fd8e4c6..c9f9b5e6833 100644
--- a/libs/angular/src/services/jslib-services.module.ts
+++ b/libs/angular/src/services/jslib-services.module.ts
@@ -20,6 +20,10 @@ import {
DefaultLoginComponentService,
LoginDecryptionOptionsService,
DefaultLoginDecryptionOptionsService,
+ TwoFactorAuthComponentService,
+ DefaultTwoFactorAuthComponentService,
+ DefaultTwoFactorAuthEmailComponentService,
+ TwoFactorAuthEmailComponentService,
} from "@bitwarden/auth/angular";
import {
AuthRequestServiceAbstraction,
@@ -1358,6 +1362,16 @@ const safeProviders: SafeProvider[] = [
useClass: DefaultRegistrationFinishService,
deps: [KeyServiceAbstraction, AccountApiServiceAbstraction],
}),
+ safeProvider({
+ provide: TwoFactorAuthComponentService,
+ useClass: DefaultTwoFactorAuthComponentService,
+ 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 817687ef2bc..b6eb2556603 100644
--- a/libs/auth/src/angular/index.ts
+++ b/libs/auth/src/angular/index.ts
@@ -75,3 +75,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..de8bfa59589
--- /dev/null
+++ b/libs/auth/src/angular/two-factor-auth/child-components/index.ts
@@ -0,0 +1,2 @@
+export * from "./two-factor-auth-email";
+export * from "./two-factor-auth-duo";
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 76%
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..5638cd5c404 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
@@ -3,6 +3,7 @@
import { DialogModule } from "@angular/cdk/dialog";
import { CommonModule } from "@angular/common";
import { Component, 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 +19,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 +47,27 @@ 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,
) {}
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())
+ .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 +77,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..dd811d3e73f 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
@@ -25,6 +25,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 +61,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/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.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.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.component.ts
similarity index 100%
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.component.ts
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/angular/src/auth/components/two-factor-auth/two-factor-auth.component.html b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.html
similarity index 77%
rename from libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.html
rename to libs/auth/src/angular/two-factor-auth/two-factor-auth.component.html
index 8462a18ac2e..1a8a82096af 100644
--- a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.html
+++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.html
@@ -1,4 +1,4 @@
-
{{ "noTwoStepProviders2" | i18n }}
-
-
-
-