diff --git a/apps/browser/src/autofill/content/components/cipher/cipher-info.ts b/apps/browser/src/autofill/content/components/cipher/cipher-info.ts index 49a629de7a3..877cddfdbf8 100644 --- a/apps/browser/src/autofill/content/components/cipher/cipher-info.ts +++ b/apps/browser/src/autofill/content/components/cipher/cipher-info.ts @@ -29,7 +29,9 @@ export function CipherInfo({ cipher, theme }: CipherInfoProps) { ${login?.username - ? html`${login.username}` + ? html`${login.username}` : null} `; diff --git a/apps/browser/src/autofill/content/components/notification/confirmation/body.ts b/apps/browser/src/autofill/content/components/notification/confirmation/body.ts index 33085f53b42..f3dc1117209 100644 --- a/apps/browser/src/autofill/content/components/notification/confirmation/body.ts +++ b/apps/browser/src/autofill/content/components/notification/confirmation/body.ts @@ -3,7 +3,7 @@ import { html, nothing } from "lit"; import { Theme } from "@bitwarden/common/platform/enums"; -import { themes } from "../../constants/styles"; +import { spacing, themes } from "../../constants/styles"; import { Celebrate, Keyhole, Warning } from "../../illustrations"; import { NotificationConfirmationMessage } from "./message"; @@ -67,7 +67,7 @@ export const iconContainerStyles = (error?: string | boolean) => css` } `; export const notificationConfirmationBodyStyles = ({ theme }: { theme: Theme }) => css` - gap: 16px; + gap: ${spacing[4]}; display: flex; align-items: center; justify-content: flex-start; diff --git a/apps/browser/src/autofill/content/components/notification/confirmation/container.ts b/apps/browser/src/autofill/content/components/notification/confirmation/container.ts index ca1c26eeaa6..b824d88a994 100644 --- a/apps/browser/src/autofill/content/components/notification/confirmation/container.ts +++ b/apps/browser/src/autofill/content/components/notification/confirmation/container.ts @@ -43,7 +43,7 @@ export function NotificationConfirmationContainer({ type, }: NotificationConfirmationContainerProps) { const headerMessage = getHeaderMessage(i18n, type, error); - const confirmationMessage = getConfirmationMessage(i18n, itemName, type, error); + const confirmationMessage = getConfirmationMessage(i18n, type, error); const buttonText = error ? i18n.newItem : i18n.view; const buttonAria = chrome.i18n.getMessage("notificationViewAria", [itemName]); @@ -109,19 +109,13 @@ export const notificationContainerStyles = (theme: Theme) => css` } `; -function getConfirmationMessage( - i18n: I18n, - itemName: string, - type?: NotificationType, - error?: string, -) { - const loginSaveConfirmation = chrome.i18n.getMessage("loginSaveConfirmation", [itemName]); - const loginUpdatedConfirmation = chrome.i18n.getMessage("loginUpdatedConfirmation", [itemName]); - +function getConfirmationMessage(i18n: I18n, type?: NotificationType, error?: string) { if (error) { return i18n.saveFailureDetails; } - return type === NotificationTypes.Add ? loginSaveConfirmation : loginUpdatedConfirmation; + return type === NotificationTypes.Add + ? i18n.loginSaveConfirmation + : i18n.loginUpdatedConfirmation; } function getHeaderMessage(i18n: I18n, type?: NotificationType, error?: string) { diff --git a/apps/browser/src/autofill/content/components/notification/confirmation/message.ts b/apps/browser/src/autofill/content/components/notification/confirmation/message.ts index 65f7223fc0e..24181e6fc22 100644 --- a/apps/browser/src/autofill/content/components/notification/confirmation/message.ts +++ b/apps/browser/src/autofill/content/components/notification/confirmation/message.ts @@ -3,7 +3,7 @@ import { html, nothing } from "lit"; import { Theme } from "@bitwarden/common/platform/enums"; -import { themes, typography } from "../../constants/styles"; +import { spacing, themes, typography } from "../../constants/styles"; export type NotificationConfirmationMessageProps = { buttonAria?: string; @@ -18,15 +18,17 @@ export type NotificationConfirmationMessageProps = { export function NotificationConfirmationMessage({ buttonAria, buttonText, + itemName, message, messageDetails, handleClick, theme, }: NotificationConfirmationMessageProps) { return html` -
+
${message || buttonText ? html` + ${itemName} css` font-weight: 400; `; +const itemNameStyles = (theme: Theme) => css` + ${baseTextStyles} + + color: ${themes[theme].text.main}; + font-weight: 400; + white-space: nowrap; + max-width: 300px; +`; + export const notificationConfirmationButtonTextStyles = (theme: Theme) => css` ${baseTextStyles} diff --git a/apps/browser/src/autofill/content/components/notification/header.ts b/apps/browser/src/autofill/content/components/notification/header.ts index 18b1f25bd74..05ac5693e78 100644 --- a/apps/browser/src/autofill/content/components/notification/header.ts +++ b/apps/browser/src/autofill/content/components/notification/header.ts @@ -4,7 +4,7 @@ import { html } from "lit"; import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums"; import { CloseButton } from "../buttons/close-button"; -import { themes } from "../constants/styles"; +import { spacing, themes } from "../constants/styles"; import { BrandIconContainer } from "../icons/brand-icon-container"; import { NotificationHeaderMessage } from "./header-message"; @@ -47,7 +47,7 @@ const notificationHeaderStyles = ({ standalone: boolean; theme: Theme; }) => css` - gap: 8px; + gap: ${spacing[2]}; display: flex; align-items: center; justify-content: flex-start; diff --git a/apps/browser/src/autofill/content/components/rows/button-row.ts b/apps/browser/src/autofill/content/components/rows/button-row.ts index f6674da6b6e..3527d050b81 100644 --- a/apps/browser/src/autofill/content/components/rows/button-row.ts +++ b/apps/browser/src/autofill/content/components/rows/button-row.ts @@ -51,7 +51,7 @@ export function ButtonRow({ theme, primaryButton, selectButtons }: ButtonRowProp } const buttonRowStyles = css` - gap: 16px; + gap: ${spacing[4]}; display: flex; align-items: center; justify-content: space-between; diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 4f2df8deb0b..9a3ff381c90 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -39,7 +39,6 @@ import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/ import { UserVerificationApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification-api.service.abstraction"; import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service"; import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { AvatarService } from "@bitwarden/common/auth/services/avatar.service"; @@ -1514,9 +1513,6 @@ export default class MainBackground { } nextAccountStatus = await this.authService.getAuthStatus(userId); - const forcePasswordReset = - (await firstValueFrom(this.masterPasswordService.forceSetPasswordReason$(userId))) != - ForceSetPasswordReason.None; await this.systemService.clearPendingClipboard(); @@ -1524,8 +1520,6 @@ export default class MainBackground { this.messagingService.send("goHome"); } else if (nextAccountStatus === AuthenticationStatus.Locked) { this.messagingService.send("locked", { userId: userId }); - } else if (forcePasswordReset) { - this.messagingService.send("update-temp-password", { userId: userId }); } else { this.messagingService.send("unlocked", { userId: userId }); await this.refreshBadge(); diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index 6a08bf007bb..49579f889b3 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -160,10 +160,6 @@ export class AppComponent implements OnInit, OnDestroy { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.router.navigate(["/remove-password"]); - } else if (msg.command == "update-temp-password") { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/update-temp-password"]); } }), takeUntil(this.destroy$), diff --git a/apps/browser/src/tools/popup/settings/about-page/about-page-v2.component.html b/apps/browser/src/tools/popup/settings/about-page/about-page-v2.component.html index 9bba3994357..839681889a8 100644 --- a/apps/browser/src/tools/popup/settings/about-page/about-page-v2.component.html +++ b/apps/browser/src/tools/popup/settings/about-page/about-page-v2.component.html @@ -23,6 +23,12 @@ + + + {{ "moreFromBitwarden" | i18n }} + + + diff --git a/apps/web/src/app/admin-console/organizations/collections/vault.component.html b/apps/web/src/app/admin-console/organizations/collections/vault.component.html index 22da9a566f4..e8782ca0f2d 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault.component.html +++ b/apps/web/src/app/admin-console/organizations/collections/vault.component.html @@ -153,6 +153,3 @@
- - - diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts index 5df2d7799d8..c90a2a657e7 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts @@ -456,7 +456,13 @@ export class MemberDialogComponent implements OnDestroy { return Object.assign(p, partialPermissions); } - handleDependentPermissions() { + async handleDependentPermissions() { + const separateCustomRolePermissions = await this.configService.getFeatureFlag( + FeatureFlag.SeparateCustomRolePermissions, + ); + if (separateCustomRolePermissions) { + return; + } // Manage Password Reset (Account Recovery) must have Manage Users enabled if ( this.permissionsGroup.value.manageResetPassword && diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.html b/apps/web/src/app/admin-console/organizations/members/members.component.html index d35bb1a8dad..2162e33081f 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.html +++ b/apps/web/src/app/admin-console/organizations/members/members.component.html @@ -374,4 +374,3 @@ - diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.ts b/apps/web/src/app/admin-console/organizations/members/members.component.ts index 5f9df110d3c..6a3ca58b73d 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.component.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Component, ViewChild, ViewContainerRef } from "@angular/core"; +import { Component } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute, Router } from "@angular/router"; import { @@ -90,9 +90,6 @@ class MembersTableDataSource extends PeopleTableDataSource templateUrl: "members.component.html", }) export class MembersComponent extends BaseMembersComponent { - @ViewChild("resetPasswordTemplate", { read: ViewContainerRef, static: true }) - resetPasswordModalRef: ViewContainerRef; - userType = OrganizationUserType; userStatusType = OrganizationUserStatusType; memberTab = MemberDialogTab; diff --git a/apps/web/src/app/admin-console/organizations/policies/policies.component.html b/apps/web/src/app/admin-console/organizations/policies/policies.component.html index e40b9d80e9e..016d631019e 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policies.component.html +++ b/apps/web/src/app/admin-console/organizations/policies/policies.component.html @@ -35,5 +35,4 @@ - diff --git a/apps/web/src/app/admin-console/organizations/policies/policies.component.ts b/apps/web/src/app/admin-console/organizations/policies/policies.component.ts index 2b86d76d9b1..6e3b34eaa30 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policies.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policies.component.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { firstValueFrom, lastValueFrom, map, Observable, switchMap } from "rxjs"; import { first } from "rxjs/operators"; @@ -33,9 +33,6 @@ import { PolicyEditComponent, PolicyEditDialogResult } from "./policy-edit.compo templateUrl: "policies.component.html", }) export class PoliciesComponent implements OnInit { - @ViewChild("editTemplate", { read: ViewContainerRef, static: true }) - editModalRef: ViewContainerRef; - loading = true; organizationId: string; policies: BasePolicy[]; diff --git a/apps/web/src/app/admin-console/organizations/settings/account.component.html b/apps/web/src/app/admin-console/organizations/settings/account.component.html index 8ae94b08f57..e6064779ece 100644 --- a/apps/web/src/app/admin-console/organizations/settings/account.component.html +++ b/apps/web/src/app/admin-console/organizations/settings/account.component.html @@ -93,7 +93,4 @@ {{ "purgeVault" | i18n }} - - - diff --git a/apps/web/src/app/admin-console/organizations/settings/account.component.ts b/apps/web/src/app/admin-console/organizations/settings/account.component.ts index 57892442c16..f3997fe669e 100644 --- a/apps/web/src/app/admin-console/organizations/settings/account.component.ts +++ b/apps/web/src/app/admin-console/organizations/settings/account.component.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Component, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; +import { Component, OnDestroy, OnInit } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { @@ -43,11 +43,6 @@ import { DeleteOrganizationDialogResult, openDeleteOrganizationDialog } from "./ templateUrl: "account.component.html", }) export class AccountComponent implements OnInit, OnDestroy { - @ViewChild("apiKeyTemplate", { read: ViewContainerRef, static: true }) - apiKeyModalRef: ViewContainerRef; - @ViewChild("rotateApiKeyTemplate", { read: ViewContainerRef, static: true }) - rotateApiKeyModalRef: ViewContainerRef; - selfHosted = false; canEditSubscription = true; loading = true; diff --git a/apps/web/src/app/auth/settings/account/account.component.html b/apps/web/src/app/auth/settings/account/account.component.html index c5edc021614..74fa02f5f93 100644 --- a/apps/web/src/app/auth/settings/account/account.component.html +++ b/apps/web/src/app/auth/settings/account/account.component.html @@ -51,7 +51,4 @@ {{ "deleteAccount" | i18n }} - - - diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.html b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.html index ab93f0be3bc..8a802e4f6af 100644 --- a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.html +++ b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.html @@ -272,7 +272,3 @@ - - - - diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts index dc464c18059..f55d731d7f2 100644 --- a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { lastValueFrom, Observable, firstValueFrom, switchMap } from "rxjs"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; @@ -44,12 +44,6 @@ import { templateUrl: "emergency-access.component.html", }) export class EmergencyAccessComponent implements OnInit { - @ViewChild("addEdit", { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef; - @ViewChild("takeoverTemplate", { read: ViewContainerRef, static: true }) - takeoverModalRef: ViewContainerRef; - @ViewChild("confirmTemplate", { read: ViewContainerRef, static: true }) - confirmModalRef: ViewContainerRef; - loaded = false; canAccessPremium$: Observable; trustedContacts: GranteeEmergencyAccess[]; diff --git a/apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.html b/apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.html index cb170a1275a..20cc50c4d59 100644 --- a/apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.html +++ b/apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.html @@ -51,5 +51,3 @@ {{ "loading" | i18n }} - - diff --git a/apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.ts b/apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.ts index bf7ca29da9b..55ebf860cff 100644 --- a/apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { firstValueFrom } from "rxjs"; @@ -17,7 +17,6 @@ import { EmergencyViewDialogComponent } from "./emergency-view-dialog.component" providers: [{ provide: CipherFormConfigService, useClass: DefaultCipherFormConfigService }], }) export class EmergencyAccessViewComponent implements OnInit { - @ViewChild("attachments", { read: ViewContainerRef, static: true }) id: EmergencyAccessId | null = null; ciphers: CipherView[] = []; loaded = false; diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.html b/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.html index 4206927772b..16c3dcb3cda 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.html +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.html @@ -84,8 +84,3 @@ - - - - - diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts index fcf07dce9b7..d240dc467ae 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Component, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; +import { Component, OnDestroy, OnInit } from "@angular/core"; import { first, firstValueFrom, @@ -12,7 +12,6 @@ import { switchMap, } from "rxjs"; -import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; @@ -52,9 +51,6 @@ import { TwoFactorVerifyComponent } from "./two-factor-verify.component"; imports: [ItemModule, LooseComponentsModule, SharedModule], }) export class TwoFactorSetupComponent implements OnInit, OnDestroy { - @ViewChild("yubikeyTemplate", { read: ViewContainerRef, static: true }) - yubikeyModalRef: ViewContainerRef; - organizationId: string; organization: Organization; providers: any[] = []; @@ -62,7 +58,6 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { recoveryCodeWarningMessage: string; showPolicyWarning = false; loading = true; - modal: ModalRef; formPromise: Promise; tabbedHeader = true; @@ -283,9 +278,6 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { } protected updateStatus(enabled: boolean, type: TwoFactorProviderType) { - if (!enabled && this.modal != null) { - this.modal.close(); - } this.providers.forEach((p) => { if (p.type === type && enabled !== undefined) { p.enabled = enabled; diff --git a/apps/web/src/app/dirt/reports/pages/cipher-report.component.ts b/apps/web/src/app/dirt/reports/pages/cipher-report.component.ts index ceda7b1c480..d6c96ff232e 100644 --- a/apps/web/src/app/dirt/reports/pages/cipher-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/cipher-report.component.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Directive, ViewChild, ViewContainerRef, OnDestroy } from "@angular/core"; +import { Directive, OnDestroy } from "@angular/core"; import { BehaviorSubject, lastValueFrom, @@ -37,8 +37,6 @@ import { AdminConsoleCipherFormConfigService } from "../../../vault/org-vault/se @Directive() export class CipherReportComponent implements OnDestroy { - @ViewChild("cipherAddEdit", { read: ViewContainerRef, static: true }) - cipherAddEditModalRef: ViewContainerRef; isAdminConsoleActive = false; loading = false; diff --git a/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.html b/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.html index 05da9865c62..8e665936496 100644 --- a/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.html +++ b/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.html @@ -96,5 +96,4 @@ - diff --git a/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.html b/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.html index cec9d45ff56..fb19bb382b8 100644 --- a/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.html +++ b/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.html @@ -108,5 +108,4 @@ > - diff --git a/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.html b/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.html index 78d13ba5c65..37c2c2f8a8c 100644 --- a/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.html +++ b/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.html @@ -98,5 +98,4 @@ - diff --git a/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.html b/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.html index 4e1c7feb22c..e28760f7746 100644 --- a/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.html +++ b/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.html @@ -96,5 +96,4 @@ - diff --git a/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.html b/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.html index 21053d70916..807bc751b23 100644 --- a/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.html +++ b/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.html @@ -100,5 +100,4 @@ - diff --git a/apps/web/src/app/tools/send/send.component.html b/apps/web/src/app/tools/send/send.component.html index 72e3031398b..e55d5e56f78 100644 --- a/apps/web/src/app/tools/send/send.component.html +++ b/apps/web/src/app/tools/send/send.component.html @@ -206,4 +206,3 @@ - diff --git a/apps/web/src/app/vault/individual-vault/folder-add-edit.component.html b/apps/web/src/app/vault/individual-vault/folder-add-edit.component.html index b6c9679887e..556672534ea 100644 --- a/apps/web/src/app/vault/individual-vault/folder-add-edit.component.html +++ b/apps/web/src/app/vault/individual-vault/folder-add-edit.component.html @@ -13,7 +13,7 @@ -
diff --git a/apps/web/src/app/vault/individual-vault/vault.component.html b/apps/web/src/app/vault/individual-vault/vault.component.html index aa27fa4ad85..c20209a0192 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.html +++ b/apps/web/src/app/vault/individual-vault/vault.component.html @@ -84,10 +84,3 @@
- - - - - - - diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.html index 668d59b8830..de1db6f0c7a 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.html @@ -57,9 +57,6 @@ bitMenuItem buttonType="secondary" type="button" - data-toggle="dropdown" - aria-haspopup="true" - aria-expanded="false" appA11yTitle="{{ 'options' | i18n }}" > diff --git a/libs/angular/src/auth/components/base-login-via-webauthn.component.ts b/libs/angular/src/auth/components/base-login-via-webauthn.component.ts index 1ad4829767a..5d30fc997dc 100644 --- a/libs/angular/src/auth/components/base-login-via-webauthn.component.ts +++ b/libs/angular/src/auth/components/base-login-via-webauthn.component.ts @@ -6,7 +6,6 @@ import { firstValueFrom } from "rxjs"; import { LoginSuccessHandlerService } from "@bitwarden/auth/common"; import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction"; -import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { WebAuthnLoginCredentialAssertionView } from "@bitwarden/common/auth/models/view/webauthn-login/webauthn-login-credential-assertion.view"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -21,7 +20,6 @@ export class BaseLoginViaWebAuthnComponent implements OnInit { protected currentState: State = "assert"; protected successRoute = "/vault"; - protected forcePasswordResetRoute = "/update-temp-password"; constructor( private webAuthnLoginService: WebAuthnLoginServiceAbstraction, @@ -73,11 +71,6 @@ export class BaseLoginViaWebAuthnComponent implements OnInit { await this.loginSuccessHandlerService.run(authResult.userId); } - if (authResult.forcePasswordReset == ForceSetPasswordReason.AdminForcePasswordReset) { - await this.router.navigate([this.forcePasswordResetRoute]); - return; - } - await this.router.navigate([this.successRoute]); } catch (error) { if (error instanceof ErrorResponse) { diff --git a/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts b/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts index 5de2339bda1..ab5b0c09b32 100644 --- a/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts +++ b/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts @@ -19,7 +19,6 @@ import { AuthRequestType } from "@bitwarden/common/auth/enums/auth-request-type" import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; -import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { AuthRequest } from "@bitwarden/common/auth/models/request/auth.request"; import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; import { LoginViaAuthRequestView } from "@bitwarden/common/auth/models/view/login-via-auth-request.view"; @@ -820,8 +819,6 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { private async handlePostLoginNavigation(loginResponse: AuthResult) { if (loginResponse.requiresTwoFactor) { await this.router.navigate(["2fa"]); - } else if (loginResponse.forcePasswordReset != ForceSetPasswordReason.None) { - await this.router.navigate(["update-temp-password"]); } else { await this.handleSuccessfulLoginNavigation(loginResponse.userId); } diff --git a/libs/auth/src/angular/login/login.component.ts b/libs/auth/src/angular/login/login.component.ts index eb2bdcee291..cd226cddcec 100644 --- a/libs/auth/src/angular/login/login.component.ts +++ b/libs/auth/src/angular/login/login.component.ts @@ -17,7 +17,6 @@ import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/mod import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; -import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { ClientType, HttpStatusCode } from "@bitwarden/common/enums"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; @@ -307,10 +306,7 @@ export class LoginComponent implements OnInit, OnDestroy { await this.loginSuccessHandlerService.run(authResult.userId); // Determine where to send the user next - if (authResult.forcePasswordReset != ForceSetPasswordReason.None) { - await this.router.navigate(["update-temp-password"]); - return; - } + // The AuthGuard will handle routing to update-temp-password based on state // TODO: PM-18269 - evaluate if we can combine this with the // password evaluation done in the password login strategy. diff --git a/libs/auth/src/angular/new-device-verification/new-device-verification.component.ts b/libs/auth/src/angular/new-device-verification/new-device-verification.component.ts index c083643c9b4..a2b0b23d05c 100644 --- a/libs/auth/src/angular/new-device-verification/new-device-verification.component.ts +++ b/libs/auth/src/angular/new-device-verification/new-device-verification.component.ts @@ -136,11 +136,6 @@ export class NewDeviceVerificationComponent implements OnInit, OnDestroy { return; } - if (authResult.forcePasswordReset) { - await this.router.navigate(["/update-temp-password"]); - return; - } - this.loginSuccessHandlerService.run(authResult.userId); // If verification succeeds, navigate to vault diff --git a/libs/auth/src/angular/sso/sso.component.ts b/libs/auth/src/angular/sso/sso.component.ts index 311ebf174ef..a91a8ed20e9 100644 --- a/libs/auth/src/angular/sso/sso.component.ts +++ b/libs/auth/src/angular/sso/sso.component.ts @@ -541,14 +541,6 @@ export class SsoComponent implements OnInit { }); } - private async handleForcePasswordReset(orgIdentifier: string) { - await this.router.navigate(["update-temp-password"], { - queryParams: { - identifier: orgIdentifier, - }, - }); - } - private async handleSuccessfulLogin() { await this.router.navigate(["lock"]); } diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts index f09d7163667..e7e62260b49 100644 --- a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts +++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts @@ -575,25 +575,6 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { }); } - /** - * Determines if a user needs to reset their password based on certain conditions. - * Users can be forced to reset their password via an admin or org policy disallowing weak passwords. - * Note: this is different from the SSO component login flow as a user can - * login with MP and then have to pass 2FA to finish login and we can actually - * evaluate if they have a weak password at that time. - * - * @param {AuthResult} authResult - The authentication result. - * @returns {boolean} Returns true if a password reset is required, false otherwise. - */ - private isForcePasswordResetRequired(authResult: AuthResult): boolean { - const forceResetReasons = [ - ForceSetPasswordReason.AdminForcePasswordReset, - ForceSetPasswordReason.WeakMasterPassword, - ]; - - return forceResetReasons.includes(authResult.forcePasswordReset); - } - showContinueButton() { return ( this.selectedProviderType != null && diff --git a/libs/auth/src/common/login-strategies/login.strategy.spec.ts b/libs/auth/src/common/login-strategies/login.strategy.spec.ts index fc3be61fe11..5a5a9dc2575 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts @@ -296,13 +296,9 @@ describe("LoginStrategy", () => { const expected = new AuthResult(); expected.userId = userId; - expected.forcePasswordReset = ForceSetPasswordReason.AdminForcePasswordReset; expected.resetMasterPassword = true; - expected.twoFactorProviders = {} as Partial< - Record> - >; - expected.captchaSiteKey = ""; expected.twoFactorProviders = null; + expected.captchaSiteKey = ""; expect(result).toEqual(expected); }); @@ -316,13 +312,9 @@ describe("LoginStrategy", () => { const expected = new AuthResult(); expected.userId = userId; - expected.forcePasswordReset = ForceSetPasswordReason.AdminForcePasswordReset; expected.resetMasterPassword = false; - expected.twoFactorProviders = {} as Partial< - Record> - >; - expected.captchaSiteKey = ""; expected.twoFactorProviders = null; + expected.captchaSiteKey = ""; expect(result).toEqual(expected); expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith( diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index 96d7b6b0f74..e252d50a5ab 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -277,17 +277,7 @@ export abstract class LoginStrategy { result.resetMasterPassword = response.resetMasterPassword; - // Convert boolean to enum and set the state for the master password service to - // so we know when we reach the auth guard that we need to guide them properly to admin - // password reset. - if (response.forcePasswordReset) { - result.forcePasswordReset = ForceSetPasswordReason.AdminForcePasswordReset; - - await this.masterPasswordService.setForceSetPasswordReason( - ForceSetPasswordReason.AdminForcePasswordReset, - userId, - ); - } + await this.processForceSetPasswordReason(response.forcePasswordReset, userId); if (response.twoFactorToken != null) { // note: we can read email from access token b/c it was saved in saveAccountInformation @@ -318,6 +308,30 @@ export abstract class LoginStrategy { return false; } + /** + * Checks if adminForcePasswordReset is true and sets the ForceSetPasswordReason.AdminForcePasswordReset flag in the master password service. + * @param adminForcePasswordReset - The admin force password reset flag + * @param userId - The user ID + * @returns a promise that resolves to a boolean indicating whether the admin force password reset flag was set + */ + async processForceSetPasswordReason( + adminForcePasswordReset: boolean, + userId: UserId, + ): Promise { + if (!adminForcePasswordReset) { + return false; + } + + // set the flag in the master password service so we know when we reach the auth guard + // that we need to guide them properly to admin password reset. + await this.masterPasswordService.setForceSetPasswordReason( + ForceSetPasswordReason.AdminForcePasswordReset, + userId, + ); + + return true; + } + protected async createKeyPairForOldAccount(userId: UserId) { try { const userKey = await this.keyService.getUserKeyWithLegacySupport(userId); diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts index 3752960fc47..2923908fb7b 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts @@ -211,20 +211,18 @@ describe("PasswordLoginStrategy", () => { it("does not force the user to update their master password when there are no requirements", async () => { apiService.postIdentityToken.mockResolvedValueOnce(identityTokenResponseFactory()); - const result = await passwordLoginStrategy.logIn(credentials); + await passwordLoginStrategy.logIn(credentials); expect(policyService.evaluateMasterPassword).not.toHaveBeenCalled(); - expect(result.forcePasswordReset).toEqual(ForceSetPasswordReason.None); }); it("does not force the user to update their master password when it meets requirements", async () => { passwordStrengthService.getPasswordStrength.mockReturnValue({ score: 5 } as any); policyService.evaluateMasterPassword.mockReturnValue(true); - const result = await passwordLoginStrategy.logIn(credentials); + await passwordLoginStrategy.logIn(credentials); expect(policyService.evaluateMasterPassword).toHaveBeenCalled(); - expect(result.forcePasswordReset).toEqual(ForceSetPasswordReason.None); }); it("forces the user to update their master password on successful login when it does not meet master password policy requirements", async () => { @@ -232,14 +230,13 @@ describe("PasswordLoginStrategy", () => { policyService.evaluateMasterPassword.mockReturnValue(false); tokenService.decodeAccessToken.mockResolvedValue({ sub: userId }); - const result = await passwordLoginStrategy.logIn(credentials); + await passwordLoginStrategy.logIn(credentials); expect(policyService.evaluateMasterPassword).toHaveBeenCalled(); expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith( ForceSetPasswordReason.WeakMasterPassword, userId, ); - expect(result.forcePasswordReset).toEqual(ForceSetPasswordReason.WeakMasterPassword); }); it("forces the user to update their master password on successful 2FA login when it does not meet master password policy requirements", async () => { @@ -257,13 +254,13 @@ describe("PasswordLoginStrategy", () => { // First login request fails requiring 2FA apiService.postIdentityToken.mockResolvedValueOnce(token2FAResponse); - const firstResult = await passwordLoginStrategy.logIn(credentials); + await passwordLoginStrategy.logIn(credentials); // Second login request succeeds apiService.postIdentityToken.mockResolvedValueOnce( identityTokenResponseFactory(masterPasswordPolicy), ); - const secondResult = await passwordLoginStrategy.logInTwoFactor( + await passwordLoginStrategy.logInTwoFactor( { provider: TwoFactorProviderType.Authenticator, token: "123456", @@ -272,15 +269,11 @@ describe("PasswordLoginStrategy", () => { "", ); - // First login attempt should not save the force password reset options - expect(firstResult.forcePasswordReset).toEqual(ForceSetPasswordReason.None); - - // Second login attempt should save the force password reset options and return in result + // Second login attempt should save the force password reset options expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith( ForceSetPasswordReason.WeakMasterPassword, userId, ); - expect(secondResult.forcePasswordReset).toEqual(ForceSetPasswordReason.WeakMasterPassword); }); it("handles new device verification login with OTP", async () => { @@ -298,7 +291,6 @@ describe("PasswordLoginStrategy", () => { newDeviceOtp: deviceVerificationOtp, }), ); - expect(result.forcePasswordReset).toBe(ForceSetPasswordReason.None); expect(result.resetMasterPassword).toBe(false); expect(result.userId).toBe(userId); }); diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.ts b/libs/auth/src/common/login-strategies/password-login.strategy.ts index f0a8d40f914..6af9d8dbb6b 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.ts @@ -109,35 +109,8 @@ export class PasswordLoginStrategy extends LoginStrategy { return authResult; } - const masterPasswordPolicyOptions = - this.getMasterPasswordPolicyOptionsFromResponse(identityResponse); + await this.evaluateMasterPasswordIfRequired(identityResponse, credentials, authResult); - // The identity result can contain master password policies for the user's organizations - if (masterPasswordPolicyOptions?.enforceOnLogin) { - // If there is a policy active, evaluate the supplied password before its no longer in memory - const meetsRequirements = this.evaluateMasterPassword( - credentials, - masterPasswordPolicyOptions, - ); - if (meetsRequirements) { - return authResult; - } - - if (identityResponse instanceof IdentityTwoFactorResponse) { - // Save the flag to this strategy for use in 2fa login as the master password is about to pass out of scope - this.cache.next({ - ...this.cache.value, - forcePasswordResetReason: ForceSetPasswordReason.WeakMasterPassword, - }); - } else { - // Authentication was successful, save the force update password options with the state service - await this.masterPasswordService.setForceSetPasswordReason( - ForceSetPasswordReason.WeakMasterPassword, - authResult.userId, // userId is only available on successful login - ); - authResult.forcePasswordReset = ForceSetPasswordReason.WeakMasterPassword; - } - } return authResult; } @@ -151,20 +124,6 @@ export class PasswordLoginStrategy extends LoginStrategy { const result = await super.logInTwoFactor(twoFactor); - // 2FA was successful, save the force update password options with the state service if defined - const forcePasswordResetReason = this.cache.value.forcePasswordResetReason; - if ( - !result.requiresTwoFactor && - !result.requiresCaptcha && - forcePasswordResetReason != ForceSetPasswordReason.None - ) { - await this.masterPasswordService.setForceSetPasswordReason( - forcePasswordResetReason, - result.userId, - ); - result.forcePasswordReset = forcePasswordResetReason; - } - return result; } @@ -208,13 +167,58 @@ export class PasswordLoginStrategy extends LoginStrategy { return !response.key; } - private getMasterPasswordPolicyOptionsFromResponse( - response: + private async evaluateMasterPasswordIfRequired( + identityResponse: | IdentityTokenResponse | IdentityTwoFactorResponse | IdentityDeviceVerificationResponse, - ): MasterPasswordPolicyOptions { - if (response == null || response instanceof IdentityDeviceVerificationResponse) { + credentials: PasswordLoginCredentials, + authResult: AuthResult, + ): Promise { + // TODO: PM-21084 - investigate if we should be sending down masterPasswordPolicy on the IdentityDeviceVerificationResponse like we do for the IdentityTwoFactorResponse + // If the response is a device verification response, we don't need to evaluate the password + if (identityResponse instanceof IdentityDeviceVerificationResponse) { + return; + } + + // The identity result can contain master password policies for the user's organizations + const masterPasswordPolicyOptions = + this.getMasterPasswordPolicyOptionsFromResponse(identityResponse); + + if (!masterPasswordPolicyOptions?.enforceOnLogin) { + return; + } + + // If there is a policy active, evaluate the supplied password before its no longer in memory + const meetsRequirements = this.evaluateMasterPassword(credentials, masterPasswordPolicyOptions); + if (meetsRequirements) { + return; + } + + if (identityResponse instanceof IdentityTwoFactorResponse) { + // Save the flag to this strategy for use in 2fa as the master password is about to pass out of scope + this.cache.next({ + ...this.cache.value, + forcePasswordResetReason: ForceSetPasswordReason.WeakMasterPassword, + }); + } + + // Authentication was successful, save the force update password options with the state service + // if there isn't already a reason set (this would only be AdminForcePasswordReset as that can be set server side + // and would have already been processed in the base login strategy processForceSetPasswordReason method) + // Note: masterPasswordService.setForceSetPasswordReason will not allow overwriting + // AdminForcePasswordReset with any other reason except for None. This is because + // an AdminForcePasswordReset will always force a user to update their password to a password that meets the policy. + await this.masterPasswordService.setForceSetPasswordReason( + ForceSetPasswordReason.WeakMasterPassword, + authResult.userId, // userId is only available on successful login + ); + } + + private getMasterPasswordPolicyOptionsFromResponse( + response: IdentityTokenResponse | IdentityTwoFactorResponse, + ): MasterPasswordPolicyOptions | null { + if (response == null) { return null; } return MasterPasswordPolicyOptions.fromResponse(response.masterPasswordPolicy); @@ -246,4 +250,35 @@ export class PasswordLoginStrategy extends LoginStrategy { const [authResult] = await this.startLogIn(); return authResult; } + + /** + * Override to handle the WeakMasterPassword reason if no other reason is set. + * @param authResult - The authentication result + * @param userId - The user ID + */ + override async processForceSetPasswordReason( + adminForcePasswordReset: boolean, + userId: UserId, + ): Promise { + // handle any existing reasons + const adminForcePasswordResetFlagSet = await super.processForceSetPasswordReason( + adminForcePasswordReset, + userId, + ); + + // If we are already processing an admin force password reset, don't process other reasons + if (adminForcePasswordResetFlagSet) { + return false; + } + + // If we have a cached weak password reason from login/logInTwoFactor apply it + const cachedReason = this.cache.value.forcePasswordResetReason; + if (cachedReason !== ForceSetPasswordReason.None) { + await this.masterPasswordService.setForceSetPasswordReason(cachedReason, userId); + return true; + } + + // If none of the conditions are met, return false + return false; + } } diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts index 546fa0c5fa7..d743a71f160 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts @@ -1,5 +1,5 @@ import { mock, MockProxy } from "jest-mock-extended"; -import { BehaviorSubject } from "rxjs"; +import { BehaviorSubject, of } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; @@ -37,10 +37,11 @@ import { AuthRequestServiceAbstraction, InternalUserDecryptionOptionsServiceAbstraction, } from "../abstractions"; +import { UserDecryptionOptions } from "../models"; import { SsoLoginCredentials } from "../models/domain/login-credentials"; import { identityTokenResponseFactory } from "./login.strategy.spec"; -import { SsoLoginStrategy } from "./sso-login.strategy"; +import { SsoLoginStrategy, SsoLoginStrategyData } from "./sso-login.strategy"; describe("SsoLoginStrategy", () => { let accountService: FakeAccountService; @@ -123,8 +124,11 @@ describe("SsoLoginStrategy", () => { mockVaultTimeoutBSub.asObservable(), ); + const userDecryptionOptions = new UserDecryptionOptions(); + userDecryptionOptionsService.userDecryptionOptions$ = of(userDecryptionOptions); + ssoLoginStrategy = new SsoLoginStrategy( - null, + {} as SsoLoginStrategyData, keyConnectorService, deviceTrustService, authRequestService, diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.ts index 1dd01d6fc75..d81284a960e 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.ts @@ -4,6 +4,7 @@ import { firstValueFrom, Observable, map, BehaviorSubject } from "rxjs"; import { Jsonify } from "type-fest"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; +import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { SsoTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/sso-token.request"; import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; @@ -355,4 +356,75 @@ export class SsoLoginStrategy extends LoginStrategy { sso: this.cache.value, }; } + + /** + * Override to handle SSO-specific ForceSetPasswordReason flags,including TdeOffboarding, + * TdeUserWithoutPasswordHasPasswordResetPermission, and SsoNewJitProvisionedUser cases. + * @param authResult - The authentication result + * @param userId - The user ID + */ + override async processForceSetPasswordReason( + adminForcePasswordReset: boolean, + userId: UserId, + ): Promise { + // handle any existing reasons + const adminForcePasswordResetFlagSet = await super.processForceSetPasswordReason( + adminForcePasswordReset, + userId, + ); + + // If we are already processing an admin force password reset, don't process other reasons + if (adminForcePasswordResetFlagSet) { + return false; + } + + // Check for TDE-related conditions + const userDecryptionOptions = await firstValueFrom( + this.userDecryptionOptionsService.userDecryptionOptions$, + ); + + if (!userDecryptionOptions) { + return false; + } + + // Check for TDE offboarding - user is being offboarded from TDE and needs to set a password + if (userDecryptionOptions.trustedDeviceOption?.isTdeOffboarding) { + await this.masterPasswordService.setForceSetPasswordReason( + ForceSetPasswordReason.TdeOffboarding, + userId, + ); + return true; + } + + // Check if user has permission to set password but hasn't yet + if ( + !userDecryptionOptions.hasMasterPassword && + userDecryptionOptions.trustedDeviceOption?.hasManageResetPasswordPermission + ) { + await this.masterPasswordService.setForceSetPasswordReason( + ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission, + userId, + ); + + return true; + } + + // Check for new SSO JIT provisioned user + // If a user logs in via SSO but has no master password and no alternative encryption methods + // Then they must be a newly provisioned user who needs to set up their encryption + if ( + !userDecryptionOptions.hasMasterPassword && + !userDecryptionOptions.keyConnectorOption?.keyConnectorUrl && + !userDecryptionOptions.trustedDeviceOption + ) { + await this.masterPasswordService.setForceSetPasswordReason( + ForceSetPasswordReason.SsoNewJitProvisionedUser, + userId, + ); + return true; + } + + // If none of the conditions are met, return false + return false; + } } diff --git a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts index ebbe2f3b6cb..fb4cbd55ad9 100644 --- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts @@ -209,7 +209,6 @@ describe("WebAuthnLoginStrategy", () => { expect(authResult).toBeInstanceOf(AuthResult); expect(authResult).toMatchObject({ captchaSiteKey: "", - forcePasswordReset: 0, resetMasterPassword: false, twoFactorProviders: null, requiresTwoFactor: false, diff --git a/libs/common/src/auth/models/domain/auth-result.ts b/libs/common/src/auth/models/domain/auth-result.ts index fdc8c963a1b..5177363ac89 100644 --- a/libs/common/src/auth/models/domain/auth-result.ts +++ b/libs/common/src/auth/models/domain/auth-result.ts @@ -4,8 +4,6 @@ import { Utils } from "../../../platform/misc/utils"; import { UserId } from "../../../types/guid"; import { TwoFactorProviderType } from "../../enums/two-factor-provider-type"; -import { ForceSetPasswordReason } from "./force-set-password-reason"; - export class AuthResult { userId: UserId; captchaSiteKey = ""; @@ -17,7 +15,6 @@ export class AuthResult { * */ resetMasterPassword = false; - forcePasswordReset: ForceSetPasswordReason = ForceSetPasswordReason.None; twoFactorProviders: Partial>> = null; ssoEmail2FaSessionToken?: string; email: string; diff --git a/libs/common/src/auth/models/domain/force-set-password-reason.ts b/libs/common/src/auth/models/domain/force-set-password-reason.ts index 011ef7fff8d..56d52860443 100644 --- a/libs/common/src/auth/models/domain/force-set-password-reason.ts +++ b/libs/common/src/auth/models/domain/force-set-password-reason.ts @@ -31,4 +31,9 @@ export enum ForceSetPasswordReason { * Occurs when TDE is disabled and master password has to be set. */ TdeOffboarding, + + /** + * Occurs when a new SSO user is JIT provisioned and needs to set their master password. + */ + SsoNewJitProvisionedUser, } diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index b75c0b71d7a..08de8ada25a 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -13,6 +13,7 @@ export enum FeatureFlag { LimitItemDeletion = "pm-15493-restrict-item-deletion-to-can-manage-permission", SsoExternalIdVisibility = "pm-18630-sso-external-id-visibility", AccountDeprovisioningBanner = "pm-17120-account-deprovisioning-admin-console-banner", + SeparateCustomRolePermissions = "pm-19917-separate-custom-role-permissions", /* Auth */ PM9112_DeviceApprovalPersistence = "pm-9112-device-approval-persistence", @@ -83,6 +84,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.LimitItemDeletion]: FALSE, [FeatureFlag.SsoExternalIdVisibility]: FALSE, [FeatureFlag.AccountDeprovisioningBanner]: FALSE, + [FeatureFlag.SeparateCustomRolePermissions]: FALSE, /* Autofill */ [FeatureFlag.BlockBrowserInjectionsByDomain]: FALSE, diff --git a/libs/common/src/key-management/master-password/services/master-password.service.spec.ts b/libs/common/src/key-management/master-password/services/master-password.service.spec.ts new file mode 100644 index 00000000000..93439ac8caa --- /dev/null +++ b/libs/common/src/key-management/master-password/services/master-password.service.spec.ts @@ -0,0 +1,104 @@ +import { mock, MockProxy } from "jest-mock-extended"; +import { of } from "rxjs"; +import * as rxjs from "rxjs"; + +import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason"; +import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service"; +import { LogService } from "../../../platform/abstractions/log.service"; +import { StateService } from "../../../platform/abstractions/state.service"; +import { StateProvider } from "../../../platform/state"; +import { UserId } from "../../../types/guid"; +import { EncryptService } from "../../crypto/abstractions/encrypt.service"; + +import { MasterPasswordService } from "./master-password.service"; + +describe("MasterPasswordService", () => { + let sut: MasterPasswordService; + + let stateProvider: MockProxy; + let stateService: MockProxy; + let keyGenerationService: MockProxy; + let encryptService: MockProxy; + let logService: MockProxy; + + const userId = "user-id" as UserId; + const mockUserState = { + state$: of(null), + update: jest.fn().mockResolvedValue(null), + }; + + beforeEach(() => { + stateProvider = mock(); + stateService = mock(); + keyGenerationService = mock(); + encryptService = mock(); + logService = mock(); + + stateProvider.getUser.mockReturnValue(mockUserState as any); + + mockUserState.update.mockReset(); + + sut = new MasterPasswordService( + stateProvider, + stateService, + keyGenerationService, + encryptService, + logService, + ); + }); + + describe("setForceSetPasswordReason", () => { + it("calls stateProvider with the provided reason and user ID", async () => { + const reason = ForceSetPasswordReason.WeakMasterPassword; + + await sut.setForceSetPasswordReason(reason, userId); + + expect(stateProvider.getUser).toHaveBeenCalled(); + expect(mockUserState.update).toHaveBeenCalled(); + + // Call the update function to verify it returns the correct reason + const updateFn = mockUserState.update.mock.calls[0][0]; + expect(updateFn(null)).toBe(reason); + }); + + it("throws an error if reason is null", async () => { + await expect( + sut.setForceSetPasswordReason(null as unknown as ForceSetPasswordReason, userId), + ).rejects.toThrow("Reason is required."); + }); + + it("throws an error if user ID is null", async () => { + await expect( + sut.setForceSetPasswordReason(ForceSetPasswordReason.None, null as unknown as UserId), + ).rejects.toThrow("User ID is required."); + }); + + it("does not overwrite AdminForcePasswordReset with other reasons except None", async () => { + jest + .spyOn(sut, "forceSetPasswordReason$") + .mockReturnValue(of(ForceSetPasswordReason.AdminForcePasswordReset)); + + jest + .spyOn(rxjs, "firstValueFrom") + .mockResolvedValue(ForceSetPasswordReason.AdminForcePasswordReset); + + await sut.setForceSetPasswordReason(ForceSetPasswordReason.WeakMasterPassword, userId); + + expect(mockUserState.update).not.toHaveBeenCalled(); + }); + + it("allows overwriting AdminForcePasswordReset with None", async () => { + jest + .spyOn(sut, "forceSetPasswordReason$") + .mockReturnValue(of(ForceSetPasswordReason.AdminForcePasswordReset)); + + jest + .spyOn(rxjs, "firstValueFrom") + .mockResolvedValue(ForceSetPasswordReason.AdminForcePasswordReset); + + await sut.setForceSetPasswordReason(ForceSetPasswordReason.None, userId); + + expect(mockUserState.update).toHaveBeenCalled(); + }); + }); +}); diff --git a/libs/common/src/key-management/master-password/services/master-password.service.ts b/libs/common/src/key-management/master-password/services/master-password.service.ts index 9ed01cf0c83..72b18d8bfba 100644 --- a/libs/common/src/key-management/master-password/services/master-password.service.ts +++ b/libs/common/src/key-management/master-password/services/master-password.service.ts @@ -148,6 +148,17 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr if (userId == null) { throw new Error("User ID is required."); } + + // Don't overwrite AdminForcePasswordReset with any other reasons other than None + // as we must allow a reset when the user has completed admin account recovery + const currentReason = await firstValueFrom(this.forceSetPasswordReason$(userId)); + if ( + currentReason === ForceSetPasswordReason.AdminForcePasswordReset && + reason !== ForceSetPasswordReason.None + ) { + return; + } + await this.stateProvider.getUser(userId, FORCE_SET_PASSWORD_REASON).update((_) => reason); } diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 24ddeda66de..a32568c8112 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -124,8 +124,12 @@ export class CipherService implements CipherServiceAbstraction { * decryption is in progress. The latest decrypted ciphers will be emitted once decryption is complete. */ cipherViews$ = perUserCache$((userId: UserId): Observable => { - return combineLatest([this.encryptedCiphersState(userId).state$, this.localData$(userId)]).pipe( - filter(([ciphers]) => ciphers != null), // Skip if ciphers haven't been loaded yor synced yet + return combineLatest([ + this.encryptedCiphersState(userId).state$, + this.localData$(userId), + this.keyService.cipherDecryptionKeys$(userId, true), + ]).pipe( + filter(([ciphers, _, keys]) => ciphers != null && keys != null), // Skip if ciphers haven't been loaded yor synced yet switchMap(() => this.getAllDecrypted(userId)), ); }, this.clearCipherViewsForUser$); diff --git a/libs/components/src/breadcrumbs/breadcrumbs.component.html b/libs/components/src/breadcrumbs/breadcrumbs.component.html index 5205e19cee5..887c7982f3b 100644 --- a/libs/components/src/breadcrumbs/breadcrumbs.component.html +++ b/libs/components/src/breadcrumbs/breadcrumbs.component.html @@ -36,7 +36,6 @@ bitIconButton="bwi-ellipsis-h" [bitMenuTriggerFor]="overflowMenu" size="small" - aria-haspopup > @for (breadcrumb of overflow; track breadcrumb) { diff --git a/libs/key-management-ui/src/lock/components/lock.component.ts b/libs/key-management-ui/src/lock/components/lock.component.ts index 125bf6ab6af..80d64e17b84 100644 --- a/libs/key-management-ui/src/lock/components/lock.component.ts +++ b/libs/key-management-ui/src/lock/components/lock.component.ts @@ -121,8 +121,6 @@ export class LockComponent implements OnInit, OnDestroy { showPassword = false; private enforcedMasterPasswordOptions?: MasterPasswordPolicyOptions = undefined; - forcePasswordResetRoute = "update-temp-password"; - formGroup: FormGroup | null = null; // Browser extension properties: @@ -605,8 +603,6 @@ export class LockComponent implements OnInit, OnDestroy { ForceSetPasswordReason.WeakMasterPassword, userId, ); - await this.router.navigate([this.forcePasswordResetRoute]); - return; } } catch (e) { // Do not prevent unlock if there is an error evaluating policies diff --git a/package-lock.json b/package-lock.json index d94f8d56dc0..823be6819e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -87,7 +87,7 @@ "@ngtools/webpack": "18.2.12", "@storybook/addon-a11y": "8.5.2", "@storybook/addon-actions": "8.5.2", - "@storybook/addon-designs": "8.0.4", + "@storybook/addon-designs": "8.2.1", "@storybook/addon-essentials": "8.5.2", "@storybook/addon-interactions": "8.5.2", "@storybook/addon-links": "8.5.2", @@ -162,7 +162,7 @@ "prettier": "3.5.3", "prettier-plugin-tailwindcss": "0.6.11", "process": "0.11.10", - "remark-gfm": "4.0.0", + "remark-gfm": "4.0.1", "rimraf": "6.0.1", "sass": "1.83.4", "sass-loader": "16.0.4", @@ -179,7 +179,7 @@ "url": "0.11.4", "util": "0.12.5", "wait-on": "8.0.3", - "webpack": "5.97.1", + "webpack": "5.99.7", "webpack-cli": "6.0.1", "webpack-dev-server": "5.2.0", "webpack-node-externals": "3.0.0" @@ -10163,9 +10163,9 @@ } }, "node_modules/@storybook/addon-designs": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/@storybook/addon-designs/-/addon-designs-8.0.4.tgz", - "integrity": "sha512-BrEWks1BRnZis2e8OoE1LhFS+x2d094Tzpbb3jQBve2IfDv/X006RSuy1WyplNxskdYdBESCH45MlRn4lhP5ew==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@storybook/addon-designs/-/addon-designs-8.2.1.tgz", + "integrity": "sha512-orwihs1D5alhh4Qu3BSJKbSgQOdSagvRX/25m5fYZQAaqVErBY0lRR4vCAU/G/STkcdv+MHwIQ5U+0kX5Tm2+w==", "dev": true, "license": "MIT", "dependencies": { @@ -10175,8 +10175,8 @@ "@storybook/blocks": "^8.0.0 || ^8.1.0-0 || ^8.2.0-0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0", "@storybook/components": "^8.0.0 || ^8.1.0-0 || ^8.2.0-0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0", "@storybook/theming": "^8.0.0 || ^8.1.0-0 || ^8.2.0-0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta" }, "peerDependenciesMeta": { "@storybook/blocks": { @@ -31490,9 +31490,9 @@ } }, "node_modules/remark-gfm": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.0.tgz", - "integrity": "sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", "dev": true, "license": "MIT", "dependencies": { @@ -32259,9 +32259,9 @@ } }, "node_modules/schema-utils": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", - "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", "dev": true, "license": "MIT", "dependencies": { @@ -36640,14 +36640,15 @@ } }, "node_modules/webpack": { - "version": "5.97.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", - "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", + "version": "5.99.7", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.7.tgz", + "integrity": "sha512-CNqKBRMQjwcmKR0idID5va1qlhrqVUKpovi+Ec79ksW8ux7iS1+A6VqzfZXgVYCFRKl7XL5ap3ZoMpwBJxcg0w==", "dev": true, "license": "MIT", "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", @@ -36664,9 +36665,9 @@ "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", + "schema-utils": "^4.3.2", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", + "terser-webpack-plugin": "^5.3.11", "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" }, @@ -37112,39 +37113,12 @@ "license": "MIT" }, "node_modules/webpack/node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", "dev": true, "license": "MIT" }, - "node_modules/webpack/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/webpack/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, "node_modules/webpack/node_modules/browserslist": { "version": "4.24.4", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", @@ -37209,32 +37183,6 @@ "dev": true, "license": "MIT" }, - "node_modules/webpack/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/webpack/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/websocket-driver": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", diff --git a/package.json b/package.json index 5035e1b1da8..b59397dd704 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@ngtools/webpack": "18.2.12", "@storybook/addon-a11y": "8.5.2", "@storybook/addon-actions": "8.5.2", - "@storybook/addon-designs": "8.0.4", + "@storybook/addon-designs": "8.2.1", "@storybook/addon-essentials": "8.5.2", "@storybook/addon-interactions": "8.5.2", "@storybook/addon-links": "8.5.2", @@ -124,7 +124,7 @@ "prettier": "3.5.3", "prettier-plugin-tailwindcss": "0.6.11", "process": "0.11.10", - "remark-gfm": "4.0.0", + "remark-gfm": "4.0.1", "rimraf": "6.0.1", "sass": "1.83.4", "sass-loader": "16.0.4", @@ -141,7 +141,7 @@ "url": "0.11.4", "util": "0.12.5", "wait-on": "8.0.3", - "webpack": "5.97.1", + "webpack": "5.99.7", "webpack-cli": "6.0.1", "webpack-dev-server": "5.2.0", "webpack-node-externals": "3.0.0"