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:
@@ -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>;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ const NotificationQueueMessageType = {
|
||||
AddLogin: "add",
|
||||
ChangePassword: "change",
|
||||
UnlockVault: "unlock",
|
||||
AtRiskPassword: "at-risk-password",
|
||||
} as const;
|
||||
|
||||
type NotificationQueueMessageTypes =
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user