From c91ceb20143a7a866c0e9bc2aeeca40335193f0b Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Mon, 5 Feb 2024 13:23:50 -0500 Subject: [PATCH] Auth/PM-5368 & PM-4613 - Web & Browser - Add support for new 2FA Duo Frameless Redirect flow (#7670) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [PM-5368] Open Duo auth url. Add BroadcastChannel listener for duo result. * [PM-5368] Remove debug line. Use PlatformUtilService to launch Uri. * PM-5368 - Some progress on getting new frameless duo implementation in place * PM-5368 - Base2FAComp - Save off duoFramelessUrl for use later on as user must be given the option to remember the device before launching the duo frameless flow in the new tab. * PM-5368 - Web - 2FA Comp - (1) Only show larger width when showing backwards compatible duo (2) Stack buttons per new design (3) selectedProviderType === providerType.OrganizationDuo is correct check for when org requires DUO * PM-5368 - Web - 2FA Comp - translate duo stuff * PM-4613 - Browser 2FA - Get most of DUO frameless in place. WIP. Must figure out how to transfer state from popup to popout + add popout logic to auth-popout-windows.ts. Converted existing useAnotherTwoStepMethod button to use new comp lib bitButton per design. * PM-4613 - Browser 2FA Comp - (1) HTML - add margin around duo frameless text to match figma (2) Get popout extension logic working properly - now closes existing popup * PM-4613 - TODO figure out communication between web and browser as broadcast channel will not work. * PM-5368 - Base comp + web changes - (1) Base component now has a setupDuoResultListener method for child classes to override (2) Web overrides setupDuoResultListener and cleans up broadcast channel once a duo result comes through. * PM-4613 - Browser - (1) Add window message handling to content-message-handler content script to pass along the duo result message to the browser extension (2) 2FA comp - override setupDuoResultListener and use browserMessagingApi to listen to duoResult and submit when it comes through. * PM-5368 - Web - 2FA comp - only clean up duo result channel on ngDestroy so that user can re-submit if an error occurs. * PM-5368 and PM-4613 - (1) Update base 2FA comp to only initialize duo result listener once as init is called any time the user changes 2FA option if multiple are present (duo org and duo personal) (2) Each client now will only create a listener once even if it is called more than once (3) On web, only try to clean up the duoResultChannel if it was created to avoid erroring on other 2FA methods. * PM-5368 - Base 2FA comp - add TODO to remove duo SDK handling once we remove the duo-redirect flag * PM-5368 - Per PR feedback, avoid repetition of duo provider check by using a new public property for isDuoProvider * PM-4613 - Per PR feedback: (1) Deconstruct code out of data (2) Add test for duoResult. --------- Co-authored-by: AndreĢ Bispo --- apps/browser/src/_locales/en/messages.json | 15 +++ .../src/auth/popup/two-factor.component.html | 106 ++++++++++++++---- .../src/auth/popup/two-factor.component.ts | 34 +++++- .../abstractions/content-message-handler.ts | 1 + .../content/content-message-handler.spec.ts | 13 +++ .../content/content-message-handler.ts | 13 +++ apps/browser/src/popup/app.module.ts | 3 +- .../src/app/auth/two-factor.component.html | 50 +++++---- apps/web/src/app/auth/two-factor.component.ts | 28 ++++- apps/web/src/locales/en/messages.json | 9 ++ .../auth/components/two-factor.component.ts | 70 +++++++++--- 11 files changed, 282 insertions(+), 60 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 6c10ea2b8a..988c9b0a24 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -2695,6 +2695,21 @@ } } }, + "launchDuoAndFollowStepsToFinishLoggingIn": { + "message": "Launch DUO and follow the steps to finish logging in." + }, + "duoRequiredByOrgForAccount": { + "message": "DUO two-step login is required for your account." + }, + "openExtensionInNewWindowToCompleteLogin": { + "message": "Open the extension in a new window to complete login" + }, + "popoutExtension": { + "message": "Popout extension" + }, + "launchDuo": { + "message": "Launch DUO" + }, "importFormatError": { "message": "Data is not formatted correctly. Please check your import file and try again." }, diff --git a/apps/browser/src/auth/popup/two-factor.component.html b/apps/browser/src/auth/popup/two-factor.component.html index 6d190120f5..361259e102 100644 --- a/apps/browser/src/auth/popup/two-factor.component.html +++ b/apps/browser/src/auth/popup/two-factor.component.html @@ -12,8 +12,7 @@ [disabled]="form.loading" *ngIf=" selectedProviderType != null && - selectedProviderType !== providerType.Duo && - selectedProviderType !== providerType.OrganizationDuo && + !isDuoProvider && (selectedProviderType !== providerType.WebAuthn || form.loading) " > @@ -23,6 +22,7 @@
+

{{ "insertYubiKey" | i18n }}

@@ -85,6 +86,7 @@
+
@@ -98,6 +100,7 @@
+

{{ "webAuthnNewTab" | i18n }}

@@ -106,26 +109,48 @@
- -
- + + +
+

+ {{ "duoRequiredByOrgForAccount" | i18n }} +

+ +

+ {{ "openExtensionInNewWindowToCompleteLogin" | i18n }} +

+ + +

{{ "launchDuoAndFollowStepsToFinishLoggingIn" | i18n }}

+ + +
-
-
-
- - + + +
+ +
+ + +
+ + +
+
+
+ + +
-
+
@@ -134,12 +159,47 @@

{{ "noTwoStepProviders" | i18n }}

{{ "noTwoStepProviders2" | i18n }}

+
-

- -

+ + + + + +

-
+ +
- + + {{ "cancel" | i18n }}
diff --git a/apps/web/src/app/auth/two-factor.component.ts b/apps/web/src/app/auth/two-factor.component.ts index 2f6b3b27e1..16b8f5b932 100644 --- a/apps/web/src/app/auth/two-factor.component.ts +++ b/apps/web/src/app/auth/two-factor.component.ts @@ -1,4 +1,4 @@ -import { Component, Inject, ViewChild, ViewContainerRef } from "@angular/core"; +import { Component, Inject, OnDestroy, ViewChild, ViewContainerRef } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { TwoFactorComponent as BaseTwoFactorComponent } from "@bitwarden/angular/auth/components/two-factor.component"; @@ -25,7 +25,7 @@ import { TwoFactorOptionsComponent } from "./two-factor-options.component"; templateUrl: "two-factor.component.html", }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class TwoFactorComponent extends BaseTwoFactorComponent { +export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDestroy { @ViewChild("twoFactorOptions", { read: ViewContainerRef, static: true }) twoFactorOptionsModal: ViewContainerRef; @@ -104,4 +104,28 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { }, }); }; + + 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 } }) => { + this.token = msg.data.code; + await this.submit(); + }; + + async ngOnDestroy() { + super.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/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 756b6afde3..2cd6da7da8 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -5918,6 +5918,15 @@ } } }, + "launchDuoAndFollowStepsToFinishLoggingIn": { + "message": "Launch DUO and follow the steps to finish logging in." + }, + "duoRequiredByOrgForAccount": { + "message": "DUO two-step login is required for your account." + }, + "launchDuo": { + "message": "Launch DUO" + }, "turnOn": { "message": "Turn on" }, diff --git a/libs/angular/src/auth/components/two-factor.component.ts b/libs/angular/src/auth/components/two-factor.component.ts index 8981d5a5d8..6e63e569c6 100644 --- a/libs/angular/src/auth/components/two-factor.component.ts +++ b/libs/angular/src/auth/components/two-factor.component.ts @@ -44,6 +44,11 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI formPromise: Promise; emailPromise: Promise; orgIdentifier: string = null; + + duoFrameless = false; + duoFramelessUrl: string = null; + duoResultListenerInitialized = false; + onSuccessfulLogin: () => Promise; onSuccessfulLoginNavigate: () => Promise; @@ -57,6 +62,13 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI protected forcePasswordResetRoute = "update-temp-password"; protected successRoute = "vault"; + get isDuoProvider(): boolean { + return ( + this.selectedProviderType === TwoFactorProviderType.Duo || + this.selectedProviderType === TwoFactorProviderType.OrganizationDuo + ); + } + constructor( protected authService: AuthService, protected router: Router, @@ -148,20 +160,42 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI break; case TwoFactorProviderType.Duo: case TwoFactorProviderType.OrganizationDuo: - setTimeout(() => { - DuoWebSDK.init({ - iframe: undefined, - host: providerData.Host, - sig_request: providerData.Signature, - submit_callback: async (f: HTMLFormElement) => { - const sig = f.querySelector('input[name="sig_response"]') as HTMLInputElement; - if (sig != null) { - this.token = sig.value; - await this.submit(); - } - }, - }); - }, 0); + // 2 Duo 2FA flows available + // 1. Duo Web SDK (iframe) - existing, to be deprecated + // 2. Duo Frameless (new tab) - new + + // AuthUrl only exists for new Duo Frameless flow + if (providerData.AuthUrl) { + this.duoFrameless = true; + // 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; + } + + // flow must be launched by user so they can choose to remember the device or not. + this.duoFramelessUrl = providerData.AuthUrl; + } else { + // Duo Web SDK (iframe) flow + // TODO: remove when we remove the "duo-redirect" feature flag + setTimeout(() => { + DuoWebSDK.init({ + iframe: undefined, + host: providerData.Host, + sig_request: providerData.Signature, + submit_callback: async (f: HTMLFormElement) => { + const sig = f.querySelector('input[name="sig_response"]') as HTMLInputElement; + if (sig != null) { + this.token = sig.value; + await this.submit(); + } + }, + }); + }, 0); + } + break; case TwoFactorProviderType.Email: this.twoFactorEmail = providerData.Email; @@ -231,6 +265,9 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI return true; } + // Each client will have own implementation + protected setupDuoResultListener(): void {} + private async handleLoginResponse(authResult: AuthResult) { if (this.handleCaptchaRequired(authResult)) { return; @@ -449,4 +486,9 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI get needsLock(): boolean { return this.authService.authingWithSso() || this.authService.authingWithUserApiKey(); } + + launchDuoFrameless() { + // Launch Duo Frameless flow in new tab + this.platformUtilsService.launchUri(this.duoFramelessUrl); + } }