mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 06:13:38 +00:00
[PM-6302, PM-6303] Add duo state and connector message on browser/desktop (#7957)
* pass state for clients * use redirect connector to set cookie with translations * simplify duo redirect url validation
This commit is contained in:
@@ -493,6 +493,12 @@
|
|||||||
"newAccountCreated": {
|
"newAccountCreated": {
|
||||||
"message": "Your new account has been created! You may now log in."
|
"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": {
|
"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."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -203,7 +203,6 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
duoResultSubscription: Subscription;
|
duoResultSubscription: Subscription;
|
||||||
|
|
||||||
protected override setupDuoResultListener() {
|
protected override setupDuoResultListener() {
|
||||||
if (!this.duoResultSubscription) {
|
if (!this.duoResultSubscription) {
|
||||||
this.duoResultSubscription = this.browserMessagingApi
|
this.duoResultSubscription = this.browserMessagingApi
|
||||||
@@ -212,12 +211,31 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
|
|||||||
filter((msg: any) => msg.command === "duoResult"),
|
filter((msg: any) => msg.command === "duoResult"),
|
||||||
takeUntil(this.destroy$),
|
takeUntil(this.destroy$),
|
||||||
)
|
)
|
||||||
.subscribe((msg: { command: string; code: string }) => {
|
.subscribe((msg: { command: string; code: string; state: string }) => {
|
||||||
this.token = msg.code;
|
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.
|
// 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
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.submit();
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ async function handleAuthResultMessage(data: ContentMessageWindowData, referrer:
|
|||||||
* @param referrer - The referrer of the window
|
* @param referrer - The referrer of the window
|
||||||
*/
|
*/
|
||||||
async function handleDuoResultMessage(data: ContentMessageWindowData, referrer: string) {
|
async function handleDuoResultMessage(data: ContentMessageWindowData, referrer: string) {
|
||||||
const { command, code } = data;
|
const { command, code, state } = data;
|
||||||
await chrome.runtime.sendMessage({ command, code: code, referrer });
|
await chrome.runtime.sendMessage({ command, code, state, referrer });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
|
|||||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
|
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
|
||||||
await this.ngZone.run(async () => {
|
await this.ngZone.run(async () => {
|
||||||
if (message.command === "duoCallback") {
|
if (message.command === "duoCallback") {
|
||||||
this.token = message.code;
|
this.token = message.code + "|" + message.state;
|
||||||
await this.submit();
|
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 {
|
ngOnDestroy(): void {
|
||||||
if (this.duoCallbackSubscriptionEnabled) {
|
if (this.duoCallbackSubscriptionEnabled) {
|
||||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||||
|
|||||||
@@ -560,6 +560,12 @@
|
|||||||
"newAccountCreated": {
|
"newAccountCreated": {
|
||||||
"message": "Your new account has been created! You may now log in."
|
"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": {
|
"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."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -117,11 +117,22 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleDuoResultMessage = async (msg: { data: { code: string } }) => {
|
private handleDuoResultMessage = async (msg: { data: { code: string; state: string } }) => {
|
||||||
this.token = msg.data.code;
|
this.token = msg.data.code + "|" + msg.data.state;
|
||||||
await this.submit();
|
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() {
|
async ngOnDestroy() {
|
||||||
super.ngOnDestroy();
|
super.ngOnDestroy();
|
||||||
|
|
||||||
|
|||||||
@@ -5,24 +5,57 @@ require("./duo-redirect.scss");
|
|||||||
const mobileDesktopCallback = "bitwarden://duo-callback";
|
const mobileDesktopCallback = "bitwarden://duo-callback";
|
||||||
|
|
||||||
window.addEventListener("load", () => {
|
window.addEventListener("load", () => {
|
||||||
|
const redirectUrl = getQsParam("duoFramelessUrl");
|
||||||
|
const handOffMessage = getQsParam("handOffMessage");
|
||||||
|
|
||||||
|
if (redirectUrl) {
|
||||||
|
redirectToDuoFrameless(redirectUrl, handOffMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const client = getQsParam("client");
|
const client = getQsParam("client");
|
||||||
const code = getQsParam("code");
|
const code = getQsParam("code");
|
||||||
|
const state = getQsParam("state");
|
||||||
|
|
||||||
if (client === "web") {
|
if (client === "web") {
|
||||||
const channel = new BroadcastChannel("duoResult");
|
const channel = new BroadcastChannel("duoResult");
|
||||||
|
|
||||||
channel.postMessage({ code: code });
|
channel.postMessage({ code: code, state: state });
|
||||||
channel.close();
|
channel.close();
|
||||||
|
|
||||||
processAndDisplayHandoffMessage();
|
processAndDisplayHandoffMessage();
|
||||||
} else if (client === "browser") {
|
} else if (client === "browser") {
|
||||||
window.postMessage({ command: "duoResult", code: code }, "*");
|
window.postMessage({ command: "duoResult", code: code, state: state }, "*");
|
||||||
processAndDisplayHandoffMessage();
|
processAndDisplayHandoffMessage();
|
||||||
} else if (client === "mobile" || client === "desktop") {
|
} 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
|
* The `duoHandOffMessage` must be set in the client via a cookie. This is so
|
||||||
* we can make use of i18n translations.
|
* 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
|
* If `isCountdown` is undefined/false, there will be no countdown timer and the user
|
||||||
* will simply have to close the tab manually.
|
* 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 processAndDisplayHandoffMessage() {
|
||||||
const handOffMessageCookie = ("; " + document.cookie)
|
const handOffMessageCookie = ("; " + document.cookie)
|
||||||
@@ -93,7 +131,9 @@ function processAndDisplayHandoffMessage() {
|
|||||||
|
|
||||||
content.appendChild(h1);
|
content.appendChild(h1);
|
||||||
content.appendChild(p);
|
content.appendChild(p);
|
||||||
content.appendChild(button);
|
if (handOffMessage.buttonText) {
|
||||||
|
content.appendChild(button);
|
||||||
|
}
|
||||||
|
|
||||||
// Countdown timer (closes tab upon completion)
|
// Countdown timer (closes tab upon completion)
|
||||||
if (handOffMessage.isCountdown) {
|
if (handOffMessage.isCountdown) {
|
||||||
|
|||||||
@@ -4043,6 +4043,12 @@
|
|||||||
"ssoHandOff": {
|
"ssoHandOff": {
|
||||||
"message": "You may now close this tab and continue in the extension."
|
"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": {
|
"includeAllTeamsFeatures": {
|
||||||
"message": "All Teams features, plus:"
|
"message": "All Teams features, plus:"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -492,8 +492,6 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
launchDuoFrameless() {
|
// implemented in clients
|
||||||
// Launch Duo Frameless flow in new tab
|
launchDuoFrameless() {}
|
||||||
this.platformUtilsService.launchUri(this.duoFramelessUrl);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user