mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 13:23:34 +00:00
feat(change-password): [PM-18720] (#5319) Change Password Implementation for Non Dialog Cases (#15319)
* feat(change-password-component): Change Password Update [18720] - Very close to complete. * fix(policy-enforcement): [PM-21085] Fix Bug with Policy Enforcement - Removed temp code to force the state I need to verify correctness. * fix(policy-enforcement): [PM-21085] Fix Bug with Policy Enforcement - Recover account working with change password component. * fix(policy-enforcement): [PM-21085] Fix Bug with Policy Enforcement - Made code more dry. * fix(change-password-component): Change Password Update [18720] - Updates to routing and the extension. Extension is still a wip. * fix(change-password-component): Change Password Update [18720] - Extension routing changes. * feat(change-password-component): Change Password Update [18720] - More extension work * feat(change-password-component): Change Password Update [18720] - Pausing work for now while we wait for product to hear back. * feat(change-password-component): Change Password Update [18720] - Removed duplicated anon layouts. * feat(change-password-component): Change Password Update [18720] - Tidied up code. * feat(change-password-component): Change Password Update [18720] - Small fixes to the styling * feat(change-password-component): Change Password Update [18720] - Adding more content for the routing. * feat(change-password-component): Change Password Update [18720] - Removed circular loop for now. * feat(change-password-component): Change Password Update [18720] - Made comments regarding the change password routing complexities with change-password and auth guard. * feat(change-password-component): Change Password Update [18720] - Undid some changes because they will be conflicts later on. * feat(change-password-component): Change Password Update [18720] - Small directive change. * feat(change-password-component): Change Password Update [18720] - Small changes and added some clarification on where I'm blocked * feat(change-password-component): Change Password Update [18720] - Org invite is seemingly working, found one bug to iron out. * refactor(change-password-component): Change Password Update [18720] - Fixed up policy service to be made more clear. * docs(change-password-component): Change Password Update [18720] - Updated documentation. * refactor(change-password-component): Change Password Update [18720] - Routing changes and policy service changes. * fix(change-password-component): Change Password Update [18720] - Wrapping up changes. * feat(change-password-component): Change Password Update [18720] - Should be working fully * feat(change-password-component): Change Password Update [18720] - Found a bug, working on password policy being present on login. * feat(change-password-component): Change Password Update [18720] - Turned on auth guard on other clients for change-password route. * feat(change-password-component): Change Password Update [18720] - Committing intermediate changes. * feat(change-password-component): Change Password Update [18720] - The master password policy endpoint has been added! Should be working. Testing now. * feat(change-password-component): Change Password Update [18720] - Minor fixes. * feat(change-password-component): Change Password Update [18720] - Undid naming change. * feat(change-password-component): Change Password Update [18720] - Removed comment. * feat(change-password-component): Change Password Update [18720] - Removed unneeded code. * fix(change-password-component): Change Password Update [18720] - Took org invite state out of service and made it accessible. * fix(change-password-component): Change Password Update [18720] - Small changes. * fix(change-password-component): Change Password Update [18720] - Split up org invite service into client specific implementations and have them injected into clients properly * feat(change-password-component): Change Password Update [18720] - Stopping work and going to switch to a new branch to pare down some of the solutions that were made to get this over the finish line * feat(change-password-component): Change Password Update [18720] - Started to remove functionality in the login.component and the password login strategy. * feat(change-password-component): Change Password Update [18720] - Removed more unneded changes. * feat(change-password-component): Change Password Update [18720] - Change password clearing state working properly. * fix(change-password-component): Change Password Update [18720] - Added docs and moved web implementation. * comments(change-password-component): Change Password Update [18720] - Added more notes. * test(change-password-component): Change Password Update [18720] - Added in tests for policy service. * comment(change-password-component): Change Password Update [18720] - Updated doc with correct ticket number. * comment(change-password-component): Change Password Update [18720] - Fixed doc. * test(change-password-component): Change Password Update [18720] - Fixed tests. * test(change-password-component): Change Password Update [18720] - Fixed linting errors. Have more tests to fix. * test(change-password-component): Change Password Update [18720] - Added back in ignore for typesafety. * fix(change-password-component): Change Password Update [18720] - Fixed other type issues. * test(change-password-component): Change Password Update [18720] - Fixed tests. * test(change-password-component): Change Password Update [18720] - Fixed more tests. * test(change-password-component): Change Password Update [18720] - Fixed tiny duplicate code. * fix(change-password-component): Change Password Update [18720] - Fixed desktop component. * fix(change-password-component): Change Password Update [18720] - Removed unused code * fix(change-password-component): Change Password Update [18720] - Fixed locales. * fix(change-password-component): Change Password Update [18720] - Removed tracing. * fix(change-password-component): Change Password Update [18720] - Removed duplicative services module entry. * fix(change-password-component): Change Password Update [18720] - Added comment. * fix(change-password-component): Change Password Update [18720] - Fixed unneeded call in two factor to get user id. * fix(change-password-component): Change Password Update [18720] - Fixed a couple of tiny things. * fix(change-password-component): Change Password Update [18720] - Added comment for later fix. * fix(change-password-component): Change Password Update [18720] - Fixed linting error. * PM-18720 - AuthGuard - move call to get isChangePasswordFlagOn down after other conditions for efficiency. * PM-18720 - PasswordLoginStrategy tests - test new feature flagged combine org invite policies logic for weak password evaluation. * PM-18720 - CLI - fix dep issue * PM-18720 - ChangePasswordComp - extract change password warning up out of input password component * PM-18720 - InputPassword - remove unused dependency. * PM-18720 - ChangePasswordComp - add callout dep * PM-18720 - Revert all anon-layout changes * PM-18720 - Anon Layout - finish reverting changes. * PM-18720 - WIP move of change password out of libs/auth * PM-18720 - Clean up remaining imports from moving change password out of libs/auth * PM-18720 - Add change-password barrel file for better import grouping * PM-18720 - Change Password comp - restore maxWidth * PM-18720 - After merge, fix errors * PM-18720 - Desktop - fix api service import * PM-18720 - NDV - fix routing. * PM-18720 - Change Password Comp - add logout service todo * PM-18720 - PasswordSettings - per feedback, component is already feature flagged behind PM16117_ChangeExistingPasswordRefactor so we can just delete the replaced callout (new text is in change-password comp) * PM-18720 - Routing Modules - properly flag new component behind feature flag. * PM-18720 - SSO Login Strategy - fix config service import since it is now in shared deps from main merge. * PM-18720 - Fix SSO login strategy tests * PM-18720 - Default Policy Service - address AC PR feedback --------- Co-authored-by: Jared Snider <jsnider@bitwarden.com> Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
ec015bd253
commit
1f60bcdcc0
@@ -1,20 +0,0 @@
|
||||
@if (initializing) {
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-2x tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
} @else {
|
||||
<auth-input-password
|
||||
[flow]="inputPasswordFlow"
|
||||
[email]="email"
|
||||
[userId]="userId"
|
||||
[loading]="submitting"
|
||||
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
|
||||
[inlineButtons]="true"
|
||||
[primaryButtonText]="{ key: 'changeMasterPassword' }"
|
||||
(onPasswordFormSubmit)="handlePasswordFormSubmit($event)"
|
||||
>
|
||||
</auth-input-password>
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
import { Component, Input, OnInit } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import {
|
||||
InputPasswordComponent,
|
||||
InputPasswordFlow,
|
||||
} from "../input-password/input-password.component";
|
||||
import { PasswordInputResult } from "../input-password/password-input-result";
|
||||
|
||||
import { ChangePasswordService } from "./change-password.service.abstraction";
|
||||
|
||||
@Component({
|
||||
selector: "auth-change-password",
|
||||
templateUrl: "change-password.component.html",
|
||||
imports: [InputPasswordComponent, I18nPipe],
|
||||
})
|
||||
export class ChangePasswordComponent implements OnInit {
|
||||
@Input() inputPasswordFlow: InputPasswordFlow = InputPasswordFlow.ChangePassword;
|
||||
|
||||
activeAccount: Account | null = null;
|
||||
email?: string;
|
||||
userId?: UserId;
|
||||
masterPasswordPolicyOptions?: MasterPasswordPolicyOptions;
|
||||
initializing = true;
|
||||
submitting = false;
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private changePasswordService: ChangePasswordService,
|
||||
private i18nService: I18nService,
|
||||
private messagingService: MessagingService,
|
||||
private policyService: PolicyService,
|
||||
private toastService: ToastService,
|
||||
private syncService: SyncService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||
this.userId = this.activeAccount?.id;
|
||||
this.email = this.activeAccount?.email;
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Error("userId not found");
|
||||
}
|
||||
|
||||
this.masterPasswordPolicyOptions = await firstValueFrom(
|
||||
this.policyService.masterPasswordPolicyOptions$(this.userId),
|
||||
);
|
||||
|
||||
this.initializing = false;
|
||||
}
|
||||
|
||||
async handlePasswordFormSubmit(passwordInputResult: PasswordInputResult) {
|
||||
this.submitting = true;
|
||||
|
||||
try {
|
||||
if (passwordInputResult.rotateUserKey) {
|
||||
if (this.activeAccount == null) {
|
||||
throw new Error("activeAccount not found");
|
||||
}
|
||||
|
||||
if (
|
||||
passwordInputResult.currentPassword == null ||
|
||||
passwordInputResult.newPasswordHint == null
|
||||
) {
|
||||
throw new Error("currentPassword or newPasswordHint not found");
|
||||
}
|
||||
|
||||
await this.syncService.fullSync(true);
|
||||
|
||||
await this.changePasswordService.rotateUserKeyMasterPasswordAndEncryptedData(
|
||||
passwordInputResult.currentPassword,
|
||||
passwordInputResult.newPassword,
|
||||
this.activeAccount,
|
||||
passwordInputResult.newPasswordHint,
|
||||
);
|
||||
} else {
|
||||
if (!this.userId) {
|
||||
throw new Error("userId not found");
|
||||
}
|
||||
|
||||
await this.changePasswordService.changePassword(passwordInputResult, this.userId);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: this.i18nService.t("masterPasswordChanged"),
|
||||
message: this.i18nService.t("masterPasswordChangedDesc"),
|
||||
});
|
||||
|
||||
this.messagingService.send("logout");
|
||||
}
|
||||
} catch {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: "",
|
||||
message: this.i18nService.t("errorOccurred"),
|
||||
});
|
||||
} finally {
|
||||
this.submitting = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { PasswordInputResult } from "@bitwarden/auth/angular";
|
||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
export abstract class ChangePasswordService {
|
||||
/**
|
||||
* Creates a new user key and re-encrypts all required data with it.
|
||||
* - does so by calling the underlying method on the `UserKeyRotationService`
|
||||
* - implemented in Web only
|
||||
*
|
||||
* @param currentPassword the current password
|
||||
* @param newPassword the new password
|
||||
* @param user the user account
|
||||
* @param newPasswordHint the new password hint
|
||||
* @throws if called from a non-Web client
|
||||
*/
|
||||
abstract rotateUserKeyMasterPasswordAndEncryptedData(
|
||||
currentPassword: string,
|
||||
newPassword: string,
|
||||
user: Account,
|
||||
newPasswordHint: string,
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Changes the user's password and re-encrypts the user key with the `newMasterKey`.
|
||||
* - Specifically, this method uses credentials from the `passwordInputResult` to:
|
||||
* 1. Decrypt the user key with the `currentMasterKey`
|
||||
* 2. Re-encrypt that user key with the `newMasterKey`, resulting in a `newMasterKeyEncryptedUserKey`
|
||||
* 3. Build a `PasswordRequest` object that gets POSTed to `"/accounts/password"`
|
||||
*
|
||||
* @param passwordInputResult credentials object received from the `InputPasswordComponent`
|
||||
* @param userId the `userId`
|
||||
* @throws if the `userId`, `currentMasterKey`, or `currentServerMasterKeyHash` is not found
|
||||
*/
|
||||
abstract changePassword(passwordInputResult: PasswordInputResult, userId: UserId): Promise<void>;
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
import { KeyService, PBKDF2KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
import { PasswordInputResult } from "../input-password/password-input-result";
|
||||
|
||||
import { ChangePasswordService } from "./change-password.service.abstraction";
|
||||
import { DefaultChangePasswordService } from "./default-change-password.service";
|
||||
|
||||
describe("DefaultChangePasswordService", () => {
|
||||
let keyService: MockProxy<KeyService>;
|
||||
let masterPasswordApiService: MockProxy<MasterPasswordApiService>;
|
||||
let masterPasswordService: MockProxy<InternalMasterPasswordServiceAbstraction>;
|
||||
|
||||
let sut: ChangePasswordService;
|
||||
|
||||
const userId = "userId" as UserId;
|
||||
|
||||
const user: Account = {
|
||||
id: userId,
|
||||
email: "email",
|
||||
emailVerified: false,
|
||||
name: "name",
|
||||
};
|
||||
|
||||
const passwordInputResult: PasswordInputResult = {
|
||||
currentMasterKey: new SymmetricCryptoKey(new Uint8Array(32)) as MasterKey,
|
||||
currentServerMasterKeyHash: "currentServerMasterKeyHash",
|
||||
|
||||
newPassword: "newPassword",
|
||||
newPasswordHint: "newPasswordHint",
|
||||
newMasterKey: new SymmetricCryptoKey(new Uint8Array(32)) as MasterKey,
|
||||
newServerMasterKeyHash: "newServerMasterKeyHash",
|
||||
newLocalMasterKeyHash: "newLocalMasterKeyHash",
|
||||
|
||||
kdfConfig: new PBKDF2KdfConfig(),
|
||||
};
|
||||
|
||||
const decryptedUserKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey;
|
||||
const newMasterKeyEncryptedUserKey: [UserKey, EncString] = [
|
||||
decryptedUserKey,
|
||||
{ encryptedString: "newMasterKeyEncryptedUserKey" } as EncString,
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
keyService = mock<KeyService>();
|
||||
masterPasswordApiService = mock<MasterPasswordApiService>();
|
||||
masterPasswordService = mock<InternalMasterPasswordServiceAbstraction>();
|
||||
|
||||
sut = new DefaultChangePasswordService(
|
||||
keyService,
|
||||
masterPasswordApiService,
|
||||
masterPasswordService,
|
||||
);
|
||||
|
||||
masterPasswordService.decryptUserKeyWithMasterKey.mockResolvedValue(decryptedUserKey);
|
||||
keyService.encryptUserKeyWithMasterKey.mockResolvedValue(newMasterKeyEncryptedUserKey);
|
||||
});
|
||||
|
||||
describe("changePassword()", () => {
|
||||
it("should call the postPassword() API method with a the correct PasswordRequest credentials", async () => {
|
||||
// Act
|
||||
await sut.changePassword(passwordInputResult, userId);
|
||||
|
||||
// Assert
|
||||
expect(masterPasswordApiService.postPassword).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
masterPasswordHash: passwordInputResult.currentServerMasterKeyHash,
|
||||
masterPasswordHint: passwordInputResult.newPasswordHint,
|
||||
newMasterPasswordHash: passwordInputResult.newServerMasterKeyHash,
|
||||
key: newMasterKeyEncryptedUserKey[1].encryptedString,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should call decryptUserKeyWithMasterKey and encryptUserKeyWithMasterKey", async () => {
|
||||
// Act
|
||||
await sut.changePassword(passwordInputResult, userId);
|
||||
|
||||
// Assert
|
||||
expect(masterPasswordService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(
|
||||
passwordInputResult.currentMasterKey,
|
||||
userId,
|
||||
);
|
||||
expect(keyService.encryptUserKeyWithMasterKey).toHaveBeenCalledWith(
|
||||
passwordInputResult.newMasterKey,
|
||||
decryptedUserKey,
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw if a userId was not found", async () => {
|
||||
// Arrange
|
||||
const userId: null = null;
|
||||
|
||||
// Act
|
||||
const testFn = sut.changePassword(passwordInputResult, userId);
|
||||
|
||||
// Assert
|
||||
await expect(testFn).rejects.toThrow("userId not found");
|
||||
});
|
||||
|
||||
it("should throw if a currentMasterKey was not found", async () => {
|
||||
// Arrange
|
||||
const incorrectPasswordInputResult = { ...passwordInputResult };
|
||||
incorrectPasswordInputResult.currentMasterKey = null;
|
||||
|
||||
// Act
|
||||
const testFn = sut.changePassword(incorrectPasswordInputResult, userId);
|
||||
|
||||
// Assert
|
||||
await expect(testFn).rejects.toThrow(
|
||||
"invalid PasswordInputResult credentials, could not change password",
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw if a currentServerMasterKeyHash was not found", async () => {
|
||||
// Arrange
|
||||
const incorrectPasswordInputResult = { ...passwordInputResult };
|
||||
incorrectPasswordInputResult.currentServerMasterKeyHash = null;
|
||||
|
||||
// Act
|
||||
const testFn = sut.changePassword(incorrectPasswordInputResult, userId);
|
||||
|
||||
// Assert
|
||||
await expect(testFn).rejects.toThrow(
|
||||
"invalid PasswordInputResult credentials, could not change password",
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw an error if user key decryption fails", async () => {
|
||||
// Arrange
|
||||
masterPasswordService.decryptUserKeyWithMasterKey.mockResolvedValue(null);
|
||||
|
||||
// Act
|
||||
const testFn = sut.changePassword(passwordInputResult, userId);
|
||||
|
||||
// Assert
|
||||
await expect(testFn).rejects.toThrow("Could not decrypt user key");
|
||||
});
|
||||
|
||||
it("should throw an error if postPassword() fails", async () => {
|
||||
// Arrange
|
||||
masterPasswordApiService.postPassword.mockRejectedValueOnce(new Error("error"));
|
||||
|
||||
// Act
|
||||
const testFn = sut.changePassword(passwordInputResult, userId);
|
||||
|
||||
// Assert
|
||||
await expect(testFn).rejects.toThrow("Could not change password");
|
||||
expect(masterPasswordApiService.postPassword).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("rotateUserKeyMasterPasswordAndEncryptedData()", () => {
|
||||
it("should throw an error (the method is only implemented in Web)", async () => {
|
||||
// Act
|
||||
const testFn = sut.rotateUserKeyMasterPasswordAndEncryptedData(
|
||||
"currentPassword",
|
||||
"newPassword",
|
||||
user,
|
||||
"newPasswordHint",
|
||||
);
|
||||
|
||||
// Assert
|
||||
await expect(testFn).rejects.toThrow(
|
||||
"rotateUserKeyMasterPasswordAndEncryptedData() is only implemented in Web",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,65 +0,0 @@
|
||||
import { PasswordInputResult, ChangePasswordService } from "@bitwarden/auth/angular";
|
||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
export class DefaultChangePasswordService implements ChangePasswordService {
|
||||
constructor(
|
||||
protected keyService: KeyService,
|
||||
protected masterPasswordApiService: MasterPasswordApiService,
|
||||
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
) {}
|
||||
|
||||
async rotateUserKeyMasterPasswordAndEncryptedData(
|
||||
currentPassword: string,
|
||||
newPassword: string,
|
||||
user: Account,
|
||||
hint: string,
|
||||
): Promise<void> {
|
||||
throw new Error("rotateUserKeyMasterPasswordAndEncryptedData() is only implemented in Web");
|
||||
}
|
||||
|
||||
async changePassword(passwordInputResult: PasswordInputResult, userId: UserId) {
|
||||
if (!userId) {
|
||||
throw new Error("userId not found");
|
||||
}
|
||||
if (
|
||||
!passwordInputResult.currentMasterKey ||
|
||||
!passwordInputResult.currentServerMasterKeyHash ||
|
||||
!passwordInputResult.newMasterKey ||
|
||||
!passwordInputResult.newServerMasterKeyHash ||
|
||||
passwordInputResult.newPasswordHint == null
|
||||
) {
|
||||
throw new Error("invalid PasswordInputResult credentials, could not change password");
|
||||
}
|
||||
|
||||
const decryptedUserKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
|
||||
passwordInputResult.currentMasterKey,
|
||||
userId,
|
||||
);
|
||||
|
||||
if (decryptedUserKey == null) {
|
||||
throw new Error("Could not decrypt user key");
|
||||
}
|
||||
|
||||
const newMasterKeyEncryptedUserKey = await this.keyService.encryptUserKeyWithMasterKey(
|
||||
passwordInputResult.newMasterKey,
|
||||
decryptedUserKey,
|
||||
);
|
||||
|
||||
const request = new PasswordRequest();
|
||||
request.masterPasswordHash = passwordInputResult.currentServerMasterKeyHash;
|
||||
request.newMasterPasswordHash = passwordInputResult.newServerMasterKeyHash;
|
||||
request.masterPasswordHint = passwordInputResult.newPasswordHint;
|
||||
request.key = newMasterKeyEncryptedUserKey[1].encryptedString as string;
|
||||
|
||||
try {
|
||||
await this.masterPasswordApiService.postPassword(request);
|
||||
} catch {
|
||||
throw new Error("Could not change password");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,6 @@
|
||||
/**
|
||||
* This barrel file should only contain Angular exports
|
||||
*/
|
||||
// change password
|
||||
export * from "./change-password/change-password.component";
|
||||
export * from "./change-password/change-password.service.abstraction";
|
||||
export * from "./change-password/default-change-password.service";
|
||||
|
||||
// fingerprint dialog
|
||||
export * from "./fingerprint-dialog/fingerprint-dialog.component";
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
</bit-form-field>
|
||||
|
||||
<div class="tw-mb-6">
|
||||
<bit-form-field>
|
||||
<bit-form-field [disableMargin]="true">
|
||||
<bit-label>{{ "newMasterPass" | i18n }}</bit-label>
|
||||
<input
|
||||
id="input-password-form_new-password"
|
||||
|
||||
@@ -129,7 +129,7 @@ export class InputPasswordComponent implements OnInit {
|
||||
@Input({ transform: (val: string) => val?.trim().toLowerCase() }) email?: string;
|
||||
@Input() userId?: UserId;
|
||||
@Input() loading = false;
|
||||
@Input() masterPasswordPolicyOptions: MasterPasswordPolicyOptions | null = null;
|
||||
@Input() masterPasswordPolicyOptions?: MasterPasswordPolicyOptions;
|
||||
|
||||
@Input() inlineButtons = false;
|
||||
@Input() primaryButtonText?: Translation;
|
||||
@@ -169,7 +169,7 @@ export class InputPasswordComponent implements OnInit {
|
||||
|
||||
protected get minPasswordLengthMsg() {
|
||||
if (
|
||||
this.masterPasswordPolicyOptions != null &&
|
||||
this.masterPasswordPolicyOptions != undefined &&
|
||||
this.masterPasswordPolicyOptions.minLength > 0
|
||||
) {
|
||||
return this.i18nService.t("characterMinimum", this.masterPasswordPolicyOptions.minLength);
|
||||
@@ -463,7 +463,7 @@ export class InputPasswordComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* Returns `true` if the current password is correct (it can be used to successfully decrypt
|
||||
* the masterKeyEncrypedUserKey), `false` otherwise
|
||||
* the masterKeyEncryptedUserKey), `false` otherwise
|
||||
*/
|
||||
private async verifyCurrentPassword(
|
||||
currentPassword: string,
|
||||
|
||||
@@ -18,9 +18,12 @@ 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 { ClientType, HttpStatusCode } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
@@ -122,6 +125,8 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
private logService: LogService,
|
||||
private validationService: ValidationService,
|
||||
private loginSuccessHandlerService: LoginSuccessHandlerService,
|
||||
private masterPasswordService: MasterPasswordServiceAbstraction,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
this.clientType = this.platformUtilsService.getClientType();
|
||||
}
|
||||
@@ -225,7 +230,29 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
const credentials = new PasswordLoginCredentials(email, masterPassword);
|
||||
let credentials: PasswordLoginCredentials;
|
||||
|
||||
if (
|
||||
await this.configService.getFeatureFlag(FeatureFlag.PM16117_ChangeExistingPasswordRefactor)
|
||||
) {
|
||||
// Try to retrieve any org policies from an org invite now so we can send it to the
|
||||
// login strategies. Since it is optional and we only want to be doing this on the
|
||||
// web we will only send in content in the right context.
|
||||
const orgPoliciesFromInvite = this.loginComponentService.getOrgPoliciesFromOrgInvite
|
||||
? await this.loginComponentService.getOrgPoliciesFromOrgInvite()
|
||||
: null;
|
||||
|
||||
const orgMasterPasswordPolicyOptions = orgPoliciesFromInvite?.enforcedPasswordPolicyOptions;
|
||||
|
||||
credentials = new PasswordLoginCredentials(
|
||||
email,
|
||||
masterPassword,
|
||||
undefined,
|
||||
orgMasterPasswordPolicyOptions,
|
||||
);
|
||||
} else {
|
||||
credentials = new PasswordLoginCredentials(email, masterPassword);
|
||||
}
|
||||
|
||||
try {
|
||||
const authResult = await this.loginStrategyService.logIn(credentials);
|
||||
@@ -284,7 +311,7 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
This is now unsupported and requires a downgraded client */
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccured"),
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("legacyEncryptionUnsupported"),
|
||||
});
|
||||
return;
|
||||
@@ -325,7 +352,13 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
orgPolicies.enforcedPasswordPolicyOptions,
|
||||
);
|
||||
if (isPasswordChangeRequired) {
|
||||
await this.router.navigate(["update-password"]);
|
||||
const changePasswordFeatureFlagOn = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
|
||||
);
|
||||
|
||||
await this.router.navigate(
|
||||
changePasswordFeatureFlagOn ? ["change-password"] : ["update-password"],
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -337,9 +370,15 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
await this.router.navigate(["vault"]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the master password meets the enforced policy requirements
|
||||
* and if the user is required to change their password.
|
||||
*
|
||||
* TODO: This is duplicate checking that we want to only do in the password login strategy.
|
||||
* Once we no longer need the policies state being set to reference later in change password
|
||||
* via using the Admin Console's new policy endpoint changes we can remove this. Consult
|
||||
* PM-23001 for details.
|
||||
*/
|
||||
private async isPasswordChangeRequiredByOrgPolicy(
|
||||
enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions,
|
||||
|
||||
@@ -2,11 +2,17 @@ import { CommonModule } from "@angular/common";
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||
import { Router } from "@angular/router";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
import { firstValueFrom, Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { LoginSuccessHandlerService } from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
@@ -61,6 +67,9 @@ export class NewDeviceVerificationComponent implements OnInit, OnDestroy {
|
||||
private logService: LogService,
|
||||
private i18nService: I18nService,
|
||||
private loginSuccessHandlerService: LoginSuccessHandlerService,
|
||||
private configService: ConfigService,
|
||||
private accountService: AccountService,
|
||||
private masterPasswordService: MasterPasswordServiceAbstraction,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -141,8 +150,29 @@ export class NewDeviceVerificationComponent implements OnInit, OnDestroy {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.loginSuccessHandlerService.run(authResult.userId);
|
||||
|
||||
// If verification succeeds, navigate to vault
|
||||
await this.router.navigate(["/vault"]);
|
||||
// TODO: PM-22663 use the new service to handle routing.
|
||||
if (
|
||||
await this.configService.getFeatureFlag(FeatureFlag.PM16117_ChangeExistingPasswordRefactor)
|
||||
) {
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(getUserId),
|
||||
);
|
||||
|
||||
const forceSetPasswordReason = await firstValueFrom(
|
||||
this.masterPasswordService.forceSetPasswordReason$(activeUserId),
|
||||
);
|
||||
|
||||
if (
|
||||
forceSetPasswordReason === ForceSetPasswordReason.WeakMasterPassword ||
|
||||
forceSetPasswordReason === ForceSetPasswordReason.AdminForcePasswordReset
|
||||
) {
|
||||
await this.router.navigate(["/change-password"]);
|
||||
} else {
|
||||
await this.router.navigate(["/vault"]);
|
||||
}
|
||||
} else {
|
||||
await this.router.navigate(["/vault"]);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
let errorMessage =
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
FakeUserDecryptionOptions as UserDecryptionOptions,
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { OrganizationKeysResponse } from "@bitwarden/common/admin-console/models/response/organization-keys.response";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
@@ -33,7 +32,6 @@ import { SetPasswordCredentials } from "./set-password-jit.service.abstraction";
|
||||
describe("DefaultSetPasswordJitService", () => {
|
||||
let sut: DefaultSetPasswordJitService;
|
||||
|
||||
let apiService: MockProxy<ApiService>;
|
||||
let masterPasswordApiService: MockProxy<MasterPasswordApiService>;
|
||||
let keyService: MockProxy<KeyService>;
|
||||
let encryptService: MockProxy<EncryptService>;
|
||||
@@ -45,7 +43,6 @@ describe("DefaultSetPasswordJitService", () => {
|
||||
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
|
||||
|
||||
beforeEach(() => {
|
||||
apiService = mock<ApiService>();
|
||||
masterPasswordApiService = mock<MasterPasswordApiService>();
|
||||
keyService = mock<KeyService>();
|
||||
encryptService = mock<EncryptService>();
|
||||
@@ -57,12 +54,11 @@ describe("DefaultSetPasswordJitService", () => {
|
||||
userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>();
|
||||
|
||||
sut = new DefaultSetPasswordJitService(
|
||||
apiService,
|
||||
masterPasswordApiService,
|
||||
keyService,
|
||||
encryptService,
|
||||
i18nService,
|
||||
kdfConfigService,
|
||||
keyService,
|
||||
masterPasswordApiService,
|
||||
masterPasswordService,
|
||||
organizationApiService,
|
||||
organizationUserApiService,
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
OrganizationUserResetPasswordEnrollmentRequest,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
@@ -31,12 +30,11 @@ import {
|
||||
|
||||
export class DefaultSetPasswordJitService implements SetPasswordJitService {
|
||||
constructor(
|
||||
protected apiService: ApiService,
|
||||
protected masterPasswordApiService: MasterPasswordApiService,
|
||||
protected keyService: KeyService,
|
||||
protected encryptService: EncryptService,
|
||||
protected i18nService: I18nService,
|
||||
protected kdfConfigService: KdfConfigService,
|
||||
protected keyService: KeyService,
|
||||
protected masterPasswordApiService: MasterPasswordApiService,
|
||||
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
protected organizationApiService: OrganizationApiServiceAbstraction,
|
||||
protected organizationUserApiService: OrganizationUserApiService,
|
||||
|
||||
@@ -394,7 +394,7 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccured"),
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("legacyEncryptionUnsupported"),
|
||||
});
|
||||
return true;
|
||||
@@ -494,7 +494,7 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultSuccessRoute = await this.determineDefaultSuccessRoute();
|
||||
const defaultSuccessRoute = await this.determineDefaultSuccessRoute(authResult.userId);
|
||||
|
||||
await this.router.navigate([defaultSuccessRoute], {
|
||||
queryParams: {
|
||||
@@ -503,12 +503,28 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
private async determineDefaultSuccessRoute(): Promise<string> {
|
||||
private async determineDefaultSuccessRoute(userId: UserId): Promise<string> {
|
||||
const activeAccountStatus = await firstValueFrom(this.authService.activeAccountStatus$);
|
||||
if (activeAccountStatus === AuthenticationStatus.Locked) {
|
||||
return "lock";
|
||||
}
|
||||
|
||||
// TODO: PM-22663 use the new service to handle routing.
|
||||
if (
|
||||
await this.configService.getFeatureFlag(FeatureFlag.PM16117_ChangeExistingPasswordRefactor)
|
||||
) {
|
||||
const forceSetPasswordReason = await firstValueFrom(
|
||||
this.masterPasswordService.forceSetPasswordReason$(userId),
|
||||
);
|
||||
|
||||
if (
|
||||
forceSetPasswordReason === ForceSetPasswordReason.WeakMasterPassword ||
|
||||
forceSetPasswordReason === ForceSetPasswordReason.AdminForcePasswordReset
|
||||
) {
|
||||
return "change-password";
|
||||
}
|
||||
}
|
||||
|
||||
return "vault";
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
VaultTimeoutSettingsService,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
@@ -55,6 +56,7 @@ describe("AuthRequestLoginStrategy", () => {
|
||||
let vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService>;
|
||||
let kdfConfigService: MockProxy<KdfConfigService>;
|
||||
let environmentService: MockProxy<EnvironmentService>;
|
||||
let configService: MockProxy<ConfigService>;
|
||||
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
let accountService: FakeAccountService;
|
||||
@@ -91,6 +93,7 @@ describe("AuthRequestLoginStrategy", () => {
|
||||
vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
|
||||
kdfConfigService = mock<KdfConfigService>();
|
||||
environmentService = mock<EnvironmentService>();
|
||||
configService = mock<ConfigService>();
|
||||
|
||||
accountService = mockAccountServiceWith(mockUserId);
|
||||
masterPasswordService = new FakeMasterPasswordService();
|
||||
@@ -121,6 +124,7 @@ describe("AuthRequestLoginStrategy", () => {
|
||||
vaultTimeoutSettingsService,
|
||||
kdfConfigService,
|
||||
environmentService,
|
||||
configService,
|
||||
);
|
||||
|
||||
tokenResponse = identityTokenResponseFactory();
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
VaultTimeoutSettingsService,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
@@ -123,6 +124,7 @@ describe("LoginStrategy", () => {
|
||||
let vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService>;
|
||||
let kdfConfigService: MockProxy<KdfConfigService>;
|
||||
let environmentService: MockProxy<EnvironmentService>;
|
||||
let configService: MockProxy<ConfigService>;
|
||||
|
||||
let passwordLoginStrategy: PasswordLoginStrategy;
|
||||
let credentials: PasswordLoginCredentials;
|
||||
@@ -148,6 +150,7 @@ describe("LoginStrategy", () => {
|
||||
passwordStrengthService = mock<PasswordStrengthService>();
|
||||
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
|
||||
environmentService = mock<EnvironmentService>();
|
||||
configService = mock<ConfigService>();
|
||||
|
||||
vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
|
||||
|
||||
@@ -177,6 +180,7 @@ describe("LoginStrategy", () => {
|
||||
vaultTimeoutSettingsService,
|
||||
kdfConfigService,
|
||||
environmentService,
|
||||
configService,
|
||||
);
|
||||
credentials = new PasswordLoginCredentials(email, masterPassword);
|
||||
});
|
||||
@@ -491,6 +495,7 @@ describe("LoginStrategy", () => {
|
||||
vaultTimeoutSettingsService,
|
||||
kdfConfigService,
|
||||
environmentService,
|
||||
configService,
|
||||
);
|
||||
|
||||
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory());
|
||||
@@ -551,6 +556,7 @@ describe("LoginStrategy", () => {
|
||||
vaultTimeoutSettingsService,
|
||||
kdfConfigService,
|
||||
environmentService,
|
||||
configService,
|
||||
);
|
||||
|
||||
const result = await passwordLoginStrategy.logIn(credentials);
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
@@ -91,6 +92,7 @@ export abstract class LoginStrategy {
|
||||
protected vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||
protected KdfConfigService: KdfConfigService,
|
||||
protected environmentService: EnvironmentService,
|
||||
protected configService: ConfigService,
|
||||
) {}
|
||||
|
||||
abstract exportCache(): CacheData;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
@@ -11,6 +12,7 @@ import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/id
|
||||
import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response";
|
||||
import { MasterPasswordPolicyResponse } from "@bitwarden/common/auth/models/response/master-password-policy.response";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service";
|
||||
import {
|
||||
@@ -18,6 +20,7 @@ import {
|
||||
VaultTimeoutSettingsService,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
@@ -54,7 +57,7 @@ const masterKey = new SymmetricCryptoKey(
|
||||
) as MasterKey;
|
||||
const userId = Utils.newGuid() as UserId;
|
||||
const deviceId = Utils.newGuid();
|
||||
const masterPasswordPolicy = new MasterPasswordPolicyResponse({
|
||||
const masterPasswordPolicyResponse = new MasterPasswordPolicyResponse({
|
||||
EnforceOnLogin: true,
|
||||
MinLength: 8,
|
||||
});
|
||||
@@ -82,6 +85,7 @@ describe("PasswordLoginStrategy", () => {
|
||||
let vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService>;
|
||||
let kdfConfigService: MockProxy<KdfConfigService>;
|
||||
let environmentService: MockProxy<EnvironmentService>;
|
||||
let configService: MockProxy<ConfigService>;
|
||||
|
||||
let passwordLoginStrategy: PasswordLoginStrategy;
|
||||
let credentials: PasswordLoginCredentials;
|
||||
@@ -109,6 +113,7 @@ describe("PasswordLoginStrategy", () => {
|
||||
vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
|
||||
kdfConfigService = mock<KdfConfigService>();
|
||||
environmentService = mock<EnvironmentService>();
|
||||
configService = mock<ConfigService>();
|
||||
|
||||
appIdService.getAppId.mockResolvedValue(deviceId);
|
||||
tokenService.decodeAccessToken.mockResolvedValue({
|
||||
@@ -148,9 +153,10 @@ describe("PasswordLoginStrategy", () => {
|
||||
vaultTimeoutSettingsService,
|
||||
kdfConfigService,
|
||||
environmentService,
|
||||
configService,
|
||||
);
|
||||
credentials = new PasswordLoginCredentials(email, masterPassword);
|
||||
tokenResponse = identityTokenResponseFactory(masterPasswordPolicy);
|
||||
tokenResponse = identityTokenResponseFactory(masterPasswordPolicyResponse);
|
||||
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
|
||||
@@ -227,6 +233,67 @@ describe("PasswordLoginStrategy", () => {
|
||||
expect(policyService.evaluateMasterPassword).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("when given master password policies as part of the login credentials from an org invite, it combines them with the token response policies to evaluate the user's password as weak", async () => {
|
||||
const passwordStrengthScore = 0;
|
||||
|
||||
passwordStrengthService.getPasswordStrength.mockReturnValue({
|
||||
score: passwordStrengthScore,
|
||||
} as any);
|
||||
policyService.evaluateMasterPassword.mockReturnValue(false);
|
||||
tokenService.decodeAccessToken.mockResolvedValue({ sub: userId });
|
||||
|
||||
jest
|
||||
.spyOn(configService, "getFeatureFlag")
|
||||
.mockImplementation((flag: FeatureFlag) =>
|
||||
Promise.resolve(flag === FeatureFlag.PM16117_ChangeExistingPasswordRefactor),
|
||||
);
|
||||
|
||||
credentials.masterPasswordPoliciesFromOrgInvite = Object.assign(
|
||||
new MasterPasswordPolicyOptions(),
|
||||
{
|
||||
minLength: 10,
|
||||
minComplexity: 2,
|
||||
requireUpper: true,
|
||||
requireLower: true,
|
||||
requireNumbers: true,
|
||||
requireSpecial: true,
|
||||
enforceOnLogin: true,
|
||||
},
|
||||
);
|
||||
|
||||
const combinedMasterPasswordPolicyOptions = Object.assign(new MasterPasswordPolicyOptions(), {
|
||||
minLength: 10,
|
||||
minComplexity: 2,
|
||||
requireUpper: true,
|
||||
requireLower: true,
|
||||
requireNumbers: true,
|
||||
requireSpecial: true,
|
||||
enforceOnLogin: false,
|
||||
});
|
||||
|
||||
policyService.combineMasterPasswordPolicyOptions.mockReturnValue(
|
||||
combinedMasterPasswordPolicyOptions,
|
||||
);
|
||||
|
||||
await passwordLoginStrategy.logIn(credentials);
|
||||
|
||||
expect(policyService.combineMasterPasswordPolicyOptions).toHaveBeenCalledWith(
|
||||
credentials.masterPasswordPoliciesFromOrgInvite,
|
||||
MasterPasswordPolicyOptions.fromResponse(masterPasswordPolicyResponse),
|
||||
);
|
||||
|
||||
expect(policyService.evaluateMasterPassword).toHaveBeenCalledWith(
|
||||
passwordStrengthScore,
|
||||
credentials.masterPassword,
|
||||
combinedMasterPasswordPolicyOptions,
|
||||
);
|
||||
|
||||
expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith(
|
||||
ForceSetPasswordReason.WeakMasterPassword,
|
||||
userId,
|
||||
);
|
||||
});
|
||||
|
||||
it("forces the user to update their master password on successful login when it does not meet master password policy requirements", async () => {
|
||||
passwordStrengthService.getPasswordStrength.mockReturnValue({ score: 0 } as any);
|
||||
policyService.evaluateMasterPassword.mockReturnValue(false);
|
||||
@@ -251,7 +318,7 @@ describe("PasswordLoginStrategy", () => {
|
||||
TwoFactorProviders2: { 0: null },
|
||||
error: "invalid_grant",
|
||||
error_description: "Two factor required.",
|
||||
MasterPasswordPolicy: masterPasswordPolicy,
|
||||
MasterPasswordPolicy: masterPasswordPolicyResponse,
|
||||
});
|
||||
|
||||
// First login request fails requiring 2FA
|
||||
@@ -271,7 +338,7 @@ describe("PasswordLoginStrategy", () => {
|
||||
TwoFactorProviders2: { 0: null },
|
||||
error: "invalid_grant",
|
||||
error_description: "Two factor required.",
|
||||
MasterPasswordPolicy: masterPasswordPolicy,
|
||||
MasterPasswordPolicy: masterPasswordPolicyResponse,
|
||||
});
|
||||
|
||||
// First login request fails requiring 2FA
|
||||
@@ -280,7 +347,7 @@ describe("PasswordLoginStrategy", () => {
|
||||
|
||||
// Second login request succeeds
|
||||
apiService.postIdentityToken.mockResolvedValueOnce(
|
||||
identityTokenResponseFactory(masterPasswordPolicy),
|
||||
identityTokenResponseFactory(masterPasswordPolicyResponse),
|
||||
);
|
||||
await passwordLoginStrategy.logInTwoFactor({
|
||||
provider: TwoFactorProviderType.Authenticator,
|
||||
|
||||
@@ -12,6 +12,7 @@ import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/ide
|
||||
import { IdentityDeviceVerificationResponse } from "@bitwarden/common/auth/models/response/identity-device-verification.response";
|
||||
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
|
||||
import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { HashPurpose } from "@bitwarden/common/platform/enums";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
@@ -75,7 +76,7 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
||||
this.localMasterKeyHash$ = this.cache.pipe(map((state) => state.localMasterKeyHash));
|
||||
}
|
||||
|
||||
override async logIn(credentials: PasswordLoginCredentials) {
|
||||
override async logIn(credentials: PasswordLoginCredentials): Promise<AuthResult> {
|
||||
const { email, masterPassword, twoFactor } = credentials;
|
||||
|
||||
const data = new PasswordLoginStrategyData();
|
||||
@@ -163,18 +164,42 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
||||
credentials: PasswordLoginCredentials,
|
||||
authResult: AuthResult,
|
||||
): Promise<void> {
|
||||
// TODO: PM-21084 - investigate if we should be sending down masterPasswordPolicy on the IdentityDeviceVerificationResponse like we do for the IdentityTwoFactorResponse
|
||||
// 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);
|
||||
let masterPasswordPolicyOptions: MasterPasswordPolicyOptions | undefined;
|
||||
|
||||
if (!masterPasswordPolicyOptions?.enforceOnLogin) {
|
||||
return;
|
||||
if (
|
||||
await this.configService.getFeatureFlag(FeatureFlag.PM16117_ChangeExistingPasswordRefactor)
|
||||
) {
|
||||
// Get the master password policy options from both the org invite and the identity response.
|
||||
masterPasswordPolicyOptions = this.policyService.combineMasterPasswordPolicyOptions(
|
||||
credentials.masterPasswordPoliciesFromOrgInvite,
|
||||
this.getMasterPasswordPolicyOptionsFromResponse(identityResponse),
|
||||
);
|
||||
|
||||
// We deliberately do not check enforceOnLogin as existing users who are logging
|
||||
// in after getting an org invite should always be forced to set a password that
|
||||
// meets the org's policy. Org Invite -> Registration also works this way for
|
||||
// new BW users as well.
|
||||
if (
|
||||
!credentials.masterPasswordPoliciesFromOrgInvite &&
|
||||
!masterPasswordPolicyOptions?.enforceOnLogin
|
||||
) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
masterPasswordPolicyOptions =
|
||||
this.getMasterPasswordPolicyOptionsFromResponse(identityResponse);
|
||||
|
||||
if (!masterPasswordPolicyOptions?.enforceOnLogin) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If there is a policy active, evaluate the supplied password before its no longer in memory
|
||||
|
||||
@@ -139,7 +139,6 @@ describe("SsoLoginStrategy", () => {
|
||||
deviceTrustService,
|
||||
authRequestService,
|
||||
i18nService,
|
||||
configService,
|
||||
accountService,
|
||||
masterPasswordService,
|
||||
keyService,
|
||||
@@ -157,6 +156,7 @@ describe("SsoLoginStrategy", () => {
|
||||
vaultTimeoutSettingsService,
|
||||
kdfConfigService,
|
||||
environmentService,
|
||||
configService,
|
||||
);
|
||||
credentials = new SsoLoginCredentials(ssoCode, ssoCodeVerifier, ssoRedirectUrl, ssoOrgId);
|
||||
});
|
||||
|
||||
@@ -13,7 +13,6 @@ import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
@@ -74,7 +73,6 @@ export class SsoLoginStrategy extends LoginStrategy {
|
||||
private deviceTrustService: DeviceTrustServiceAbstraction,
|
||||
private authRequestService: AuthRequestServiceAbstraction,
|
||||
private i18nService: I18nService,
|
||||
private configService: ConfigService,
|
||||
...sharedDeps: ConstructorParameters<typeof LoginStrategy>
|
||||
) {
|
||||
super(...sharedDeps);
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
VaultTimeoutSettingsService,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import {
|
||||
Environment,
|
||||
EnvironmentService,
|
||||
@@ -56,6 +57,7 @@ describe("UserApiLoginStrategy", () => {
|
||||
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
|
||||
let vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService>;
|
||||
let kdfConfigService: MockProxy<KdfConfigService>;
|
||||
let configService: MockProxy<ConfigService>;
|
||||
|
||||
let apiLogInStrategy: UserApiLoginStrategy;
|
||||
let credentials: UserApiLoginCredentials;
|
||||
@@ -88,6 +90,7 @@ describe("UserApiLoginStrategy", () => {
|
||||
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
|
||||
vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
|
||||
kdfConfigService = mock<KdfConfigService>();
|
||||
configService = mock<ConfigService>();
|
||||
|
||||
appIdService.getAppId.mockResolvedValue(deviceId);
|
||||
tokenService.getTwoFactorToken.mockResolvedValue(null);
|
||||
@@ -115,6 +118,7 @@ describe("UserApiLoginStrategy", () => {
|
||||
vaultTimeoutSettingsService,
|
||||
kdfConfigService,
|
||||
environmentService,
|
||||
configService,
|
||||
);
|
||||
|
||||
credentials = new UserApiLoginCredentials(apiClientId, apiClientSecret);
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
VaultTimeoutSettingsService,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
@@ -54,6 +55,7 @@ describe("WebAuthnLoginStrategy", () => {
|
||||
let vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService>;
|
||||
let kdfConfigService: MockProxy<KdfConfigService>;
|
||||
let environmentService: MockProxy<EnvironmentService>;
|
||||
let configService: MockProxy<ConfigService>;
|
||||
|
||||
let webAuthnLoginStrategy!: WebAuthnLoginStrategy;
|
||||
|
||||
@@ -98,6 +100,7 @@ describe("WebAuthnLoginStrategy", () => {
|
||||
vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
|
||||
kdfConfigService = mock<KdfConfigService>();
|
||||
environmentService = mock<EnvironmentService>();
|
||||
configService = mock<ConfigService>();
|
||||
|
||||
tokenService.getTwoFactorToken.mockResolvedValue(null);
|
||||
appIdService.getAppId.mockResolvedValue(deviceId);
|
||||
@@ -124,6 +127,7 @@ describe("WebAuthnLoginStrategy", () => {
|
||||
vaultTimeoutSettingsService,
|
||||
kdfConfigService,
|
||||
environmentService,
|
||||
configService,
|
||||
);
|
||||
|
||||
// Create credentials
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// @ts-strict-ignore
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type";
|
||||
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
||||
import { WebAuthnLoginAssertionResponseRequest } from "@bitwarden/common/auth/services/webauthn-login/request/webauthn-login-assertion-response.request";
|
||||
@@ -15,6 +16,7 @@ export class PasswordLoginCredentials {
|
||||
public email: string,
|
||||
public masterPassword: string,
|
||||
public twoFactor?: TokenTwoFactorRequest,
|
||||
public masterPasswordPoliciesFromOrgInvite?: MasterPasswordPolicyOptions,
|
||||
) {}
|
||||
}
|
||||
|
||||
|
||||
@@ -402,6 +402,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
||||
this.vaultTimeoutSettingsService,
|
||||
this.kdfConfigService,
|
||||
this.environmentService,
|
||||
this.configService,
|
||||
];
|
||||
|
||||
return source.pipe(
|
||||
@@ -425,7 +426,6 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
||||
this.deviceTrustService,
|
||||
this.authRequestService,
|
||||
this.i18nService,
|
||||
this.configService,
|
||||
...sharedDeps,
|
||||
);
|
||||
case AuthenticationType.UserApiKey:
|
||||
|
||||
Reference in New Issue
Block a user