[PM-7693] Remove cookie from Duo connector (#9699)

* utilizing locale service in duo

* refactor launchDuoUri method

* Add cookie information back in ext. and desktop to support backwards compatibility

* Update duo-redirect.ts

fixing comment
This commit is contained in:
Ike 2024-06-21 14:56:27 -07:00 committed by GitHub
parent 4f5c0de039
commit 705a02086e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 45 additions and 96 deletions

View File

@ -573,18 +573,18 @@
} }
} }
}, },
"masterPassDoesntMatch": {
"message": "Master password confirmation does not match."
},
"newAccountCreated": {
"message": "Your new account has been created! You may now log in."
},
"youSuccessfullyLoggedIn": { "youSuccessfullyLoggedIn": {
"message": "You successfully logged in" "message": "You successfully logged in"
}, },
"youMayCloseThisWindow": { "youMayCloseThisWindow": {
"message": "You may close this window" "message": "You may close this window"
}, },
"masterPassDoesntMatch": {
"message": "Master password confirmation does not match."
},
"newAccountCreated": {
"message": "Your new account has been created! You may now log in."
},
"masterPassSent": { "masterPassSent": {
"message": "We've sent you an email with your master password hint." "message": "We've sent you an email with your master password hint."
}, },

View File

@ -149,17 +149,6 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest
await this.submit(); await this.submit();
}; };
override async 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() { async ngOnDestroy() {
super.ngOnDestroy(); super.ngOnDestroy();

View File

@ -1,15 +1,16 @@
import { getQsParam } from "./common"; import { getQsParam } from "./common";
import { TranslationService } from "./translation.service";
require("./duo-redirect.scss"); require("./duo-redirect.scss");
const mobileDesktopCallback = "bitwarden://duo-callback"; const mobileDesktopCallback = "bitwarden://duo-callback";
let localeService: TranslationService = null;
window.addEventListener("load", () => { window.addEventListener("load", async () => {
const redirectUrl = getQsParam("duoFramelessUrl"); const redirectUrl = getQsParam("duoFramelessUrl");
const handOffMessage = getQsParam("handOffMessage");
if (redirectUrl) { if (redirectUrl) {
redirectToDuoFrameless(redirectUrl, handOffMessage); redirectToDuoFrameless(redirectUrl);
return; return;
} }
@ -17,19 +18,22 @@ window.addEventListener("load", () => {
const code = getQsParam("code"); const code = getQsParam("code");
const state = getQsParam("state"); const state = getQsParam("state");
localeService = new TranslationService(navigator.language, "locales");
await localeService.init();
if (client === "web") { if (client === "web") {
const channel = new BroadcastChannel("duoResult"); const channel = new BroadcastChannel("duoResult");
channel.postMessage({ code: code, state: state }); channel.postMessage({ code: code, state: state });
channel.close(); channel.close();
processAndDisplayHandoffMessage(); displayHandoffMessage(client);
} else if (client === "browser") { } else if (client === "browser") {
window.postMessage({ command: "duoResult", code: code, state: state }, "*"); window.postMessage({ command: "duoResult", code: code, state: state }, "*");
processAndDisplayHandoffMessage(); displayHandoffMessage(client);
} else if (client === "mobile" || client === "desktop") { } else if (client === "mobile" || client === "desktop") {
if (client === "desktop") { if (client === "desktop") {
processAndDisplayHandoffMessage(); displayHandoffMessage(client);
} }
document.location.replace( document.location.replace(
mobileDesktopCallback + mobileDesktopCallback +
@ -42,103 +46,55 @@ window.addEventListener("load", () => {
}); });
/** /**
* In order to set a cookie with the hand off message, some clients need to use * validate the Duo AuthUrl and redirect to it.
* this connector as a middleman to set the cookie before continuing to the duo url
* @param redirectUrl the duo auth url * @param redirectUrl the duo auth url
* @param handOffMessage message to save as cookie
*/ */
function redirectToDuoFrameless(redirectUrl: string, handOffMessage: string) { function redirectToDuoFrameless(redirectUrl: string) {
const validateUrl = new URL(redirectUrl); const validateUrl = new URL(redirectUrl);
if (validateUrl.protocol !== "https:" || !validateUrl.hostname.endsWith("duosecurity.com")) { if (validateUrl.protocol !== "https:" || !validateUrl.hostname.endsWith("duosecurity.com")) {
throw new Error("Invalid redirect URL"); throw new Error("Invalid redirect URL");
} }
document.cookie = `duoHandOffMessage=${handOffMessage}; SameSite=strict;`;
window.location.href = decodeURIComponent(redirectUrl); window.location.href = decodeURIComponent(redirectUrl);
} }
/** /**
* The `duoHandOffMessage` must be set in the client via a cookie. This is so * Note: browsers won't let javascript close a tab (button or otherwise) that wasn't opened by javascript,
* we can make use of i18n translations. * so browser, desktop, and mobile are not able to take advantage of the countdown timer or close button.
*
* Format the message as an object and set is as a cookie. The following gives an
* example (be sure to replace strings with i18n translated text):
*
* ```
* const duoHandOffMessage = {
* title: "You successfully logged in",
* message: "This window will automatically close in 5 seconds",
* buttonText: "Close",
* isCountdown: true
* };
*
* document.cookie = `duoHandOffMessage=${encodeURIComponent(JSON.stringify(duoHandOffMessage))};SameSite=strict`;
*
* ```
*
* The `title`, `message`, and `buttonText` properties will be used to create the
* relevant DOM elements.
*
* Countdown timer:
* The `isCountdown` signifies that you want to start a countdown timer that will
* automatically close the tab when finished. The starting point for the timer will
* be based upon the first number that can be parsed from the `message` property
* (so be sure to add exactly one number to the `message`).
*
* This implementation makes it so the client does not have to split up the `message` into
* three translations, such as:
* ['This window will automatically close in', '5', 'seconds']
* ...which would cause bad translations in languages that swap the order of words.
*
* 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() { function displayHandoffMessage(client: string) {
const handOffMessageCookie = ("; " + document.cookie)
.split("; duoHandOffMessage=")
.pop()
.split(";")
.shift();
const handOffMessage = JSON.parse(decodeURIComponent(handOffMessageCookie));
// Clear the cookie
document.cookie = "duoHandOffMessage=;SameSite=strict;max-age=0";
const content = document.getElementById("content"); const content = document.getElementById("content");
content.className = "text-center"; content.className = "text-center";
content.innerHTML = ""; content.innerHTML = "";
const h1 = document.createElement("h1"); const h1 = document.createElement("h1");
const p = document.createElement("p"); const p = document.createElement("p");
const button = document.createElement("button");
h1.textContent = handOffMessage.title; h1.textContent = localeService.t("youSuccessfullyLoggedIn");
p.textContent = handOffMessage.message; p.textContent =
button.textContent = handOffMessage.buttonText; client == "web"
? (p.textContent = localeService.t("thisWindowWillCloseIn5Seconds"))
: localeService.t("youMayCloseThisWindow");
h1.className = "font-weight-semibold"; h1.className = "font-weight-semibold";
p.className = "mb-4"; p.className = "mb-4";
content.appendChild(h1);
content.appendChild(p);
// Web client will have a close button as well as an auto close timer
if (client == "web") {
const button = document.createElement("button");
button.textContent = localeService.t("close");
button.className = "bg-primary text-white border-0 rounded py-2 px-3"; button.className = "bg-primary text-white border-0 rounded py-2 px-3";
button.addEventListener("click", () => { button.addEventListener("click", () => {
window.close(); window.close();
}); });
content.appendChild(h1);
content.appendChild(p);
if (handOffMessage.buttonText) {
content.appendChild(button); content.appendChild(button);
}
// Countdown timer (closes tab upon completion) // Countdown timer (closes tab upon completion)
if (handOffMessage.isCountdown) {
let num = Number(p.textContent.match(/\d+/)[0]); let num = Number(p.textContent.match(/\d+/)[0]);
const interval = setInterval(() => { const interval = setInterval(() => {

View File

@ -4151,6 +4151,9 @@
"thisWindowWillCloseIn5Seconds": { "thisWindowWillCloseIn5Seconds": {
"message": "This window will automatically close in 5 seconds" "message": "This window will automatically close in 5 seconds"
}, },
"youMayCloseThisWindow": {
"message": "You may close this window"
},
"includeAllTeamsFeatures": { "includeAllTeamsFeatures": {
"message": "All Teams features, plus:" "message": "All Teams features, plus:"
}, },

View File

@ -506,6 +506,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
return authType == AuthenticationType.Sso || authType == AuthenticationType.UserApiKey; return authType == AuthenticationType.Sso || authType == AuthenticationType.UserApiKey;
} }
// implemented in clients async launchDuoFrameless() {
async launchDuoFrameless() {} this.platformUtilsService.launchUri(this.duoFramelessUrl);
}
} }