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 @@ -
+ {{ "noTwoStepProviders" | i18n }}

{{ "noTwoStepProviders2" | i18n }}

-
- -
-