1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-09 05:00:10 +00:00

Add a login check to trigger at-risk password notification.

This commit is contained in:
Miles Blackwood
2025-04-29 14:13:51 -04:00
parent aff46331e7
commit 62383500b1
9 changed files with 180 additions and 22 deletions

View File

@@ -1,6 +1,10 @@
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config";
import { UserId } from "@bitwarden/common/types/guid";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { SecurityTask } from "@bitwarden/common/vault/tasks";
import { CollectionView } from "../../content/components/common-types";
import { NotificationQueueMessageTypes } from "../../enums/notification-queue-message-type.enum";
@@ -32,10 +36,17 @@ interface AddUnlockVaultQueueMessage extends NotificationQueueMessage {
type: "unlock";
}
interface AtRiskPasswordQueueMessage extends NotificationQueueMessage {
type: "at-risk-password";
organization: Organization;
cipher: CipherView;
}
type NotificationQueueMessageItem =
| AddLoginQueueMessage
| AddChangePasswordQueueMessage
| AddUnlockVaultQueueMessage;
| AddUnlockVaultQueueMessage
| AtRiskPasswordQueueMessage;
type LockedVaultPendingNotificationsData = {
commandToRetry: {
@@ -50,6 +61,13 @@ type LockedVaultPendingNotificationsData = {
target: string;
};
type AtRiskPasswordNotificationsData = {
activeUserId: UserId;
cipher: CipherView;
securityTask: SecurityTask;
uri: string;
};
type AdjustNotificationBarMessageData = {
height: number;
};
@@ -76,7 +94,8 @@ type NotificationBackgroundExtensionMessage = {
data?: Partial<LockedVaultPendingNotificationsData> &
Partial<AdjustNotificationBarMessageData> &
Partial<ChangePasswordMessageData> &
Partial<UnlockVaultMessageData>;
Partial<UnlockVaultMessageData> &
Partial<AtRiskPasswordNotificationsData>;
login?: AddLoginMessageData;
folder?: string;
edit?: boolean;
@@ -101,6 +120,10 @@ type NotificationBackgroundExtensionMessageHandlers = {
sender,
}: BackgroundOnMessageHandlerParams) => Promise<CollectionView[]>;
bgCloseNotificationBar: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
bgOpenAtRisksPasswordNotification: ({
message,
sender,
}: BackgroundOnMessageHandlerParams) => Promise<void>;
bgOpenAtRisksPasswords: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
bgAdjustNotificationBar: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
bgAddLogin: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;

View File

@@ -3,7 +3,10 @@
import { firstValueFrom, switchMap, map, of } from "rxjs";
import { CollectionService } from "@bitwarden/admin-console/common";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import {
getOrganizationById,
OrganizationService,
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
@@ -84,6 +87,8 @@ export default class NotificationBackground {
bgAdjustNotificationBar: ({ message, sender }) =>
this.handleAdjustNotificationBarMessage(message, sender),
bgChangedPassword: ({ message, sender }) => this.changedPassword(message, sender),
bgOpenAtRisksPasswordNotification: ({ message, sender }) =>
this.openAtRisksPasswordNotification(message, sender),
bgCloseNotificationBar: ({ message, sender }) =>
this.handleCloseNotificationBarMessage(message, sender),
bgOpenAtRisksPasswords: ({ message, sender }) =>
@@ -372,6 +377,44 @@ export default class NotificationBackground {
}
}
/**
* Sends a message to trigger the at risk password notification
*
* @param message - The extension message
* @param sender - The contextual sender of the message
*/
async openAtRisksPasswordNotification(
message: NotificationBackgroundExtensionMessage,
sender: chrome.runtime.MessageSender,
) {
const { activeUserId, cipher, securityTask, uri } = message.data;
const domain = Utils.getDomain(uri);
const addLoginIsEnabled = await this.getEnableAddedLoginPrompt();
const wasVaultLocked = AuthenticationStatus.Locked && addLoginIsEnabled;
const organization = await firstValueFrom(
this.organizationService
.organizations$(activeUserId)
.pipe(getOrganizationById(securityTask.organizationId)),
);
this.removeTabFromNotificationQueue(sender.tab);
const launchTimestamp = new Date().getTime();
const queueMessage: NotificationQueueMessageItem = {
domain,
wasVaultLocked,
type: NotificationQueueMessageType.AtRiskPassword,
organization: organization,
tab: sender.tab,
cipher,
launchTimestamp,
expires: new Date(launchTimestamp + NOTIFICATION_BAR_LIFESPAN_MS),
};
this.notificationQueue.push(queueMessage);
await this.checkNotificationQueue(sender.tab);
}
/**
* Adds a login message to the notification queue, prompting the user to save
* the login if it does not already exist in the vault. If the cipher exists

View File

@@ -1,9 +1,15 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Subject, switchMap, timer } from "rxjs";
import { firstValueFrom, Subject, switchMap, timer } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getOptionalUserId } from "@bitwarden/common/auth/services/account.service";
import { CLEAR_NOTIFICATION_LOGIN_DATA_DURATION } from "@bitwarden/common/autofill/constants";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { SecurityTask, TaskService } from "@bitwarden/common/vault/tasks";
import { BrowserApi } from "../../platform/browser/browser-api";
import { generateDomainMatchPatterns, isInvalidResponseStatusCode } from "../utils";
@@ -35,6 +41,9 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
constructor(
private logService: LogService,
private notificationBackground: NotificationBackground,
private taskService: TaskService,
private accountService: AccountService,
private cipherService: CipherService,
) {}
/**
@@ -436,6 +445,29 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
);
this.clearCompletedWebRequest(requestId, tab);
}
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(getOptionalUserId),
);
const { cipher, securityTask } = await this.getSecurityTaskAndCipherForLoginData(
modifyLoginData,
activeUserId,
);
const shouldTriggerAtRiskPasswordNotification: boolean = typeof securityTask !== "undefined";
if (shouldTriggerAtRiskPasswordNotification) {
await this.notificationBackground.openAtRisksPasswordNotification(
{
command: "bgOpenAtRisksPasswordNotification",
data: {
activeUserId,
cipher,
securityTask,
},
},
{ tab },
);
this.clearCompletedWebRequest(requestId, tab);
}
};
/**
@@ -458,6 +490,40 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
return modifyLoginData?.username && (modifyLoginData.password || modifyLoginData.newPassword);
};
/**
* Determines if the at-risk password notification should be triggered.
*
* @param modifyLoginData - The modified login form data
* @param activeUserId - The currently logged in user ID
*/
private async getSecurityTaskAndCipherForLoginData(
modifyLoginData: ModifyLoginCipherFormData,
activeUserId: UserId,
) {
const shouldGetTasks: boolean = await this.notificationBackground.getNotificationFlag();
if (!shouldGetTasks) {
return;
}
const tasks: SecurityTask[] = await this.notificationBackground.getSecurityTasks(activeUserId);
const ciphers: CipherView[] = await this.cipherService.getAllDecryptedForUrl(
modifyLoginData.uri,
activeUserId,
);
const cipherIds: CipherView["id"][] = ciphers.map((c) => c.id);
const securityTask =
tasks.length > 0 && tasks.find((task) => cipherIds.indexOf(task.cipherId) > -1);
const cipher = ciphers.find((cipher) => cipher.id === securityTask.cipherId);
return { securityTask, cipher, uri: modifyLoginData.uri };
// see at-risk-password-component launchChangePassword
// DefaultChangeLoginPasswordService
// this can be implemented as a provider in the view.
}
/**
* Clears the completed web request and removes the modified login form data for the tab.
*

View File

@@ -3,7 +3,10 @@ import { html } from "lit";
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums";
import { NotificationType } from "../../../notification/abstractions/notification-bar";
import {
NotificationType,
NotificationTypes,
} from "../../../notification/abstractions/notification-bar";
import { CipherItem } from "../cipher";
import { NotificationCipherData } from "../cipher/types";
import { scrollbarStyles, spacing, themes, typography } from "../constants/styles";
@@ -17,12 +20,14 @@ const { css } = createEmotion({
export function NotificationBody({
ciphers = [],
passwordChangeUri,
i18n,
notificationType,
theme = ThemeTypes.Light,
handleEditOrUpdateAction,
}: {
ciphers?: NotificationCipherData[];
passwordChangeUri: string;
customClasses?: string[];
i18n: { [key: string]: string };
notificationType?: NotificationType;
@@ -32,22 +37,32 @@ export function NotificationBody({
// @TODO get client vendor from context
const isSafari = false;
return html`
<div class=${notificationBodyStyles({ isSafari, theme })}>
${ciphers.map((cipher) =>
ItemRow({
theme,
children: CipherItem({
cipher,
i18n,
notificationType,
theme,
handleAction: handleEditOrUpdateAction,
}),
}),
)}
</div>
`;
switch (notificationType) {
case NotificationTypes.AtRiskPassword:
return html`
<div class=${notificationBodyStyles({ isSafari, theme })}>
${passwordChangeUri ? i18n.atRiskChangePrompt : i18n.atRiskNavigatePrompt}
${passwordChangeUri && i18n.changePassword}
</div>
`;
default:
return html`
<div class=${notificationBodyStyles({ isSafari, theme })}>
${ciphers.map((cipher) =>
ItemRow({
theme,
children: CipherItem({
cipher,
i18n,
notificationType,
theme,
handleAction: handleEditOrUpdateAction,
}),
}),
)}
</div>
`;
}
}
const notificationBodyStyles = ({ isSafari, theme }: { isSafari: boolean; theme: Theme }) => css`

View File

@@ -107,6 +107,8 @@ function getHeaderMessage(i18n: { [key: string]: string }, type?: NotificationTy
return i18n.updateLogin;
case NotificationTypes.Unlock:
return "";
case NotificationTypes.AtRiskPassword:
return i18n.atRiskPassword;
default:
return undefined;
}

View File

@@ -2,6 +2,7 @@ const NotificationQueueMessageType = {
AddLogin: "add",
ChangePassword: "change",
UnlockVault: "unlock",
AtRiskPassword: "at-risk-password",
} as const;
type NotificationQueueMessageTypes =

View File

@@ -11,7 +11,7 @@ const NotificationTypes = {
Add: "add",
Change: "change",
Unlock: "unlock",
SecurityTaskNotice: "security-task-notice",
AtRiskPassword: "at-risk-password",
} as const;
type NotificationType = (typeof NotificationTypes)[keyof typeof NotificationTypes];
@@ -32,6 +32,7 @@ type NotificationBarIframeInitData = {
removeIndividualVault?: boolean;
theme?: Theme;
type?: NotificationType; // @TODO use `NotificationType`
passwordChangeUri?: string;
};
type NotificationBarWindowMessage = {

View File

@@ -55,6 +55,10 @@ function applyNotificationBarStyle() {
function getI18n() {
return {
appName: chrome.i18n.getMessage("appName"),
atRiskPassword: chrome.i18n.getMessage("atRiskPassword"),
atRiskChangePrompt: chrome.i18n.getMessage("atRiskChangePrompt"),
atRiskNavigatePrompt: chrome.i18n.getMessage("atRiskNavigatePrompt"),
changePassword: chrome.i18n.getMessage("changePassword"),
close: chrome.i18n.getMessage("close"),
collection: chrome.i18n.getMessage("collection"),
folder: chrome.i18n.getMessage("folder"),

View File

@@ -1223,6 +1223,9 @@ export default class MainBackground {
this.overlayNotificationsBackground = new OverlayNotificationsBackground(
this.logService,
this.notificationBackground,
this.taskService,
this.accountService,
this.cipherService,
);
this.autoSubmitLoginBackground = new AutoSubmitLoginBackground(