diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 31b02def8a9..f0e3bbf3c9b 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -493,6 +493,12 @@ "newAccountCreated": { "message": "Your new account has been created! You may now log in." }, + "youSuccessfullyLoggedIn": { + "message": "You successfully logged in" + }, + "youMayCloseThisWindow": { + "message": "You may close this window" + }, "masterPassSent": { "message": "We've sent you an email with your master password hint." }, diff --git a/apps/browser/src/auth/popup/two-factor.component.ts b/apps/browser/src/auth/popup/two-factor.component.ts index a0f0e4f0ee1..e511122f9a5 100644 --- a/apps/browser/src/auth/popup/two-factor.component.ts +++ b/apps/browser/src/auth/popup/two-factor.component.ts @@ -203,7 +203,6 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { } duoResultSubscription: Subscription; - protected override setupDuoResultListener() { if (!this.duoResultSubscription) { this.duoResultSubscription = this.browserMessagingApi @@ -212,12 +211,31 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { filter((msg: any) => msg.command === "duoResult"), takeUntil(this.destroy$), ) - .subscribe((msg: { command: string; code: string }) => { - this.token = msg.code; + .subscribe((msg: { command: string; code: string; state: string }) => { + this.token = msg.code + "|" + msg.state; // This floating promise is intentional. We don't need to await the submit + awaiting in a subscription is not recommended. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.submit(); }); } } + + override launchDuoFrameless() { + 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 launchUrl = + this.environmentService.getWebVaultUrl() + + "/duo-redirect-connector.html" + + "?duoFramelessUrl=" + + encodeURIComponent(this.duoFramelessUrl) + + "&handOffMessage=" + + encodeURIComponent(JSON.stringify(duoHandOffMessage)); + this.platformUtilsService.launchUri(launchUrl); + } } diff --git a/apps/browser/src/autofill/content/content-message-handler.ts b/apps/browser/src/autofill/content/content-message-handler.ts index 4ee273039c8..05e87c2da81 100644 --- a/apps/browser/src/autofill/content/content-message-handler.ts +++ b/apps/browser/src/autofill/content/content-message-handler.ts @@ -46,8 +46,8 @@ async function handleAuthResultMessage(data: ContentMessageWindowData, referrer: * @param referrer - The referrer of the window */ async function handleDuoResultMessage(data: ContentMessageWindowData, referrer: string) { - const { command, code } = data; - await chrome.runtime.sendMessage({ command, code: code, referrer }); + const { command, code, state } = data; + await chrome.runtime.sendMessage({ command, code, state, referrer }); } /** diff --git a/apps/desktop/src/auth/two-factor.component.ts b/apps/desktop/src/auth/two-factor.component.ts index 531e4fb13c7..7c624f4adb9 100644 --- a/apps/desktop/src/auth/two-factor.component.ts +++ b/apps/desktop/src/auth/two-factor.component.ts @@ -127,7 +127,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => { await this.ngZone.run(async () => { if (message.command === "duoCallback") { - this.token = message.code; + this.token = message.code + "|" + message.state; await this.submit(); } }); @@ -136,6 +136,25 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { } } + override launchDuoFrameless() { + 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 launchUrl = + this.environmentService.getWebVaultUrl() + + "/duo-redirect-connector.html" + + "?duoFramelessUrl=" + + encodeURIComponent(this.duoFramelessUrl) + + "&handOffMessage=" + + encodeURIComponent(JSON.stringify(duoHandOffMessage)); + this.platformUtilsService.launchUri(launchUrl); + } + ngOnDestroy(): void { if (this.duoCallbackSubscriptionEnabled) { this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index d86d1773c92..48c7184c888 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -560,6 +560,12 @@ "newAccountCreated": { "message": "Your new account has been created! You may now log in." }, + "youSuccessfullyLoggedIn": { + "message": "You successfully logged in" + }, + "youMayCloseThisWindow": { + "message": "You may close this window" + }, "masterPassSent": { "message": "We've sent you an email with your master password hint." }, @@ -2484,7 +2490,7 @@ }, "aliasDomain": { "message": "Alias domain" - }, + }, "importData": { "message": "Import data", "description": "Used for the desktop menu item and the header of the import dialog" diff --git a/apps/web/src/app/auth/two-factor.component.ts b/apps/web/src/app/auth/two-factor.component.ts index cd2982299e5..44a9674fbdf 100644 --- a/apps/web/src/app/auth/two-factor.component.ts +++ b/apps/web/src/app/auth/two-factor.component.ts @@ -117,11 +117,22 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest } } - private handleDuoResultMessage = async (msg: { data: { code: string } }) => { - this.token = msg.data.code; + private handleDuoResultMessage = async (msg: { data: { code: string; state: string } }) => { + this.token = msg.data.code + "|" + msg.data.state; await this.submit(); }; + override launchDuoFrameless() { + const duoHandOffMessage = { + title: this.i18nService.t("youSuccessfullyLoggedIn"), + message: this.i18nService.t("thisWindowWillCloseIn5Seconds"), + buttonText: this.i18nService.t("close"), + isCountdown: true, + }; + document.cookie = `duoHandOffMessage=${JSON.stringify(duoHandOffMessage)}; SameSite=strict;`; + this.platformUtilsService.launchUri(this.duoFramelessUrl); + } + async ngOnDestroy() { super.ngOnDestroy(); diff --git a/apps/web/src/connectors/duo-redirect.ts b/apps/web/src/connectors/duo-redirect.ts index 362cf78e7d4..337e807ae31 100644 --- a/apps/web/src/connectors/duo-redirect.ts +++ b/apps/web/src/connectors/duo-redirect.ts @@ -5,24 +5,57 @@ require("./duo-redirect.scss"); const mobileDesktopCallback = "bitwarden://duo-callback"; window.addEventListener("load", () => { + const redirectUrl = getQsParam("duoFramelessUrl"); + const handOffMessage = getQsParam("handOffMessage"); + + if (redirectUrl) { + redirectToDuoFrameless(redirectUrl, handOffMessage); + return; + } + const client = getQsParam("client"); const code = getQsParam("code"); + const state = getQsParam("state"); if (client === "web") { const channel = new BroadcastChannel("duoResult"); - channel.postMessage({ code: code }); + channel.postMessage({ code: code, state: state }); channel.close(); processAndDisplayHandoffMessage(); } else if (client === "browser") { - window.postMessage({ command: "duoResult", code: code }, "*"); + window.postMessage({ command: "duoResult", code: code, state: state }, "*"); processAndDisplayHandoffMessage(); } else if (client === "mobile" || client === "desktop") { - document.location.replace(mobileDesktopCallback + "?code=" + encodeURIComponent(code)); + processAndDisplayHandoffMessage(); + document.location.replace( + mobileDesktopCallback + + "?code=" + + encodeURIComponent(code) + + "&state=" + + encodeURIComponent(state), + ); } }); +/** + * In order to set a cookie with the hand off message, some clients need to use + * this connector as a middleman to set the cookie before continuing to the duo url + * @param redirectUrl the duo auth url + * @param handOffMessage message to save as cookie + */ +function redirectToDuoFrameless(redirectUrl: string, handOffMessage: string) { + const validateUrl = new URL(redirectUrl); + + if (validateUrl.protocol !== "https:" || !validateUrl.hostname.endsWith("duosecurity.com")) { + throw new Error("Invalid redirect URL"); + } + + document.cookie = `duoHandOffMessage=${handOffMessage}; SameSite=strict;`; + window.location.href = decodeURIComponent(redirectUrl); +} + /** * The `duoHandOffMessage` must be set in the client via a cookie. This is so * we can make use of i18n translations. @@ -58,6 +91,11 @@ window.addEventListener("load", () => { * * If `isCountdown` is undefined/false, there will be no countdown timer and the user * will simply have to close the tab manually. + * + * If `buttonText` is undefined, there will be no close button. + * + * Note: browsers won't let javascript close a tab that wasn't opened by javascript, + * so some clients may not be able to take advantage of the countdown timer/close button. */ function processAndDisplayHandoffMessage() { const handOffMessageCookie = ("; " + document.cookie) @@ -93,7 +131,9 @@ function processAndDisplayHandoffMessage() { content.appendChild(h1); content.appendChild(p); - content.appendChild(button); + if (handOffMessage.buttonText) { + content.appendChild(button); + } // Countdown timer (closes tab upon completion) if (handOffMessage.isCountdown) { diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index f9bbd0031bf..80e74c8f654 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -4043,6 +4043,12 @@ "ssoHandOff": { "message": "You may now close this tab and continue in the extension." }, + "youSuccessfullyLoggedIn": { + "message": "You successfully logged in" + }, + "thisWindowWillCloseIn5Seconds": { + "message": "This window will automatically close in 5 seconds" + }, "includeAllTeamsFeatures": { "message": "All Teams features, plus:" }, diff --git a/libs/angular/src/auth/components/two-factor.component.ts b/libs/angular/src/auth/components/two-factor.component.ts index 89b05496072..e5f32306890 100644 --- a/libs/angular/src/auth/components/two-factor.component.ts +++ b/libs/angular/src/auth/components/two-factor.component.ts @@ -492,8 +492,6 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI ); } - launchDuoFrameless() { - // Launch Duo Frameless flow in new tab - this.platformUtilsService.launchUri(this.duoFramelessUrl); - } + // implemented in clients + launchDuoFrameless() {} }