1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-06 18:43:25 +00:00

feat:(set-initial-password): [Auth/PM-18457] Create SetInitialPasswordComponent (#14186)

Creates a `SetInitialPasswordComponent` to be used in scenarios where an existing and authed user must set an initial password.

Feature Flag: `PM16117_SetInitialPasswordRefactor`
This commit is contained in:
rr-bw
2025-06-30 12:39:53 -07:00
committed by GitHub
parent f9d0e6fe4a
commit 5639668d3f
21 changed files with 1876 additions and 13 deletions

View File

@@ -14,6 +14,8 @@ import {
tdeDecryptionRequiredGuard,
unauthGuardFn,
} from "@bitwarden/angular/auth/guards";
import { SetInitialPasswordComponent } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.component";
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route";
import {
LoginComponent,
@@ -315,6 +317,14 @@ const routes: Routes = [
},
} satisfies AnonLayoutWrapperData,
},
{
path: "set-initial-password",
canActivate: [canAccessFeature(FeatureFlag.PM16117_SetInitialPasswordRefactor), authGuard],
component: SetInitialPasswordComponent,
data: {
maxWidth: "lg",
} satisfies AnonLayoutWrapperData,
},
{
path: "2fa",
canActivate: [unauthGuardFn(), TwoFactorAuthGuard],

View File

@@ -5,6 +5,7 @@ import { Router } from "@angular/router";
import { Subject, merge } from "rxjs";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { SetInitialPasswordService } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.service.abstraction";
import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
import {
SECURE_STORAGE,
@@ -140,6 +141,7 @@ import { DesktopSetPasswordJitService } from "./desktop-set-password-jit.service
import { InitService } from "./init.service";
import { NativeMessagingManifestService } from "./native-messaging-manifest.service";
import { RendererCryptoFunctionService } from "./renderer-crypto-function.service";
import { DesktopSetInitialPasswordService } from "./set-initial-password/desktop-set-initial-password.service";
const RELOAD_CALLBACK = new SafeInjectionToken<() => any>("RELOAD_CALLBACK");
@@ -392,6 +394,23 @@ const safeProviders: SafeProvider[] = [
InternalUserDecryptionOptionsServiceAbstraction,
],
}),
safeProvider({
provide: SetInitialPasswordService,
useClass: DesktopSetInitialPasswordService,
deps: [
ApiService,
EncryptService,
I18nServiceAbstraction,
KdfConfigService,
KeyService,
MasterPasswordApiService,
InternalMasterPasswordServiceAbstraction,
OrganizationApiServiceAbstraction,
OrganizationUserApiService,
InternalUserDecryptionOptionsServiceAbstraction,
MessagingServiceAbstraction,
],
}),
safeProvider({
provide: SsoUrlService,
useClass: SsoUrlService,

View File

@@ -0,0 +1,177 @@
import { MockProxy, mock } from "jest-mock-extended";
import { BehaviorSubject, of } from "rxjs";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import {
SetInitialPasswordCredentials,
SetInitialPasswordService,
SetInitialPasswordUserType,
} from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.service.abstraction";
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 { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "@bitwarden/common/types/csprng";
import { UserId } from "@bitwarden/common/types/guid";
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
import { DEFAULT_KDF_CONFIG, KdfConfigService, KeyService } from "@bitwarden/key-management";
import { DesktopSetInitialPasswordService } from "./desktop-set-initial-password.service";
describe("DesktopSetInitialPasswordService", () => {
let sut: SetInitialPasswordService;
let apiService: MockProxy<ApiService>;
let encryptService: MockProxy<EncryptService>;
let i18nService: MockProxy<I18nService>;
let kdfConfigService: MockProxy<KdfConfigService>;
let keyService: MockProxy<KeyService>;
let masterPasswordApiService: MockProxy<MasterPasswordApiService>;
let masterPasswordService: MockProxy<InternalMasterPasswordServiceAbstraction>;
let organizationApiService: MockProxy<OrganizationApiServiceAbstraction>;
let organizationUserApiService: MockProxy<OrganizationUserApiService>;
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
let messagingService: MockProxy<MessagingService>;
beforeEach(() => {
apiService = mock<ApiService>();
encryptService = mock<EncryptService>();
i18nService = mock<I18nService>();
kdfConfigService = mock<KdfConfigService>();
keyService = mock<KeyService>();
masterPasswordApiService = mock<MasterPasswordApiService>();
masterPasswordService = mock<InternalMasterPasswordServiceAbstraction>();
organizationApiService = mock<OrganizationApiServiceAbstraction>();
organizationUserApiService = mock<OrganizationUserApiService>();
userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>();
messagingService = mock<MessagingService>();
sut = new DesktopSetInitialPasswordService(
apiService,
encryptService,
i18nService,
kdfConfigService,
keyService,
masterPasswordApiService,
masterPasswordService,
organizationApiService,
organizationUserApiService,
userDecryptionOptionsService,
messagingService,
);
});
it("should instantiate", () => {
expect(sut).not.toBeFalsy();
});
describe("setInitialPassword(...)", () => {
// Mock function parameters
let credentials: SetInitialPasswordCredentials;
let userType: SetInitialPasswordUserType;
let userId: UserId;
// Mock other function data
let userKey: UserKey;
let userKeyEncString: EncString;
let masterKeyEncryptedUserKey: [UserKey, EncString];
let keyPair: [string, EncString];
let keysRequest: KeysRequest;
let userDecryptionOptions: UserDecryptionOptions;
let userDecryptionOptionsSubject: BehaviorSubject<UserDecryptionOptions>;
let setPasswordRequest: SetPasswordRequest;
beforeEach(() => {
// Mock function parameters
credentials = {
newMasterKey: new SymmetricCryptoKey(new Uint8Array(32).buffer as CsprngArray) as MasterKey,
newServerMasterKeyHash: "newServerMasterKeyHash",
newLocalMasterKeyHash: "newLocalMasterKeyHash",
newPasswordHint: "newPasswordHint",
kdfConfig: DEFAULT_KDF_CONFIG,
orgSsoIdentifier: "orgSsoIdentifier",
orgId: "orgId",
resetPasswordAutoEnroll: false,
};
userId = "userId" as UserId;
userType = SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER;
// Mock other function data
userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
userKeyEncString = new EncString("masterKeyEncryptedUserKey");
masterKeyEncryptedUserKey = [userKey, userKeyEncString];
keyPair = ["publicKey", new EncString("privateKey")];
keysRequest = new KeysRequest(keyPair[0], keyPair[1].encryptedString);
userDecryptionOptions = new UserDecryptionOptions({ hasMasterPassword: true });
userDecryptionOptionsSubject = new BehaviorSubject(userDecryptionOptions);
userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject;
setPasswordRequest = new SetPasswordRequest(
credentials.newServerMasterKeyHash,
masterKeyEncryptedUserKey[1].encryptedString,
credentials.newPasswordHint,
credentials.orgSsoIdentifier,
keysRequest,
credentials.kdfConfig.kdfType,
credentials.kdfConfig.iterations,
);
});
function setupMocks() {
// Mock makeMasterKeyEncryptedUserKey() values
keyService.userKey$.mockReturnValue(of(userKey));
keyService.encryptUserKeyWithMasterKey.mockResolvedValue(masterKeyEncryptedUserKey);
// Mock keyPair values
keyService.userPrivateKey$.mockReturnValue(of(null));
keyService.userPublicKey$.mockReturnValue(of(null));
keyService.makeKeyPair.mockResolvedValue(keyPair);
}
describe("given the initial password was successfully set", () => {
it("should send a 'redrawMenu' message", async () => {
// Arrange
setupMocks();
// Act
await sut.setInitialPassword(credentials, userType, userId);
// Assert
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
expect(messagingService.send).toHaveBeenCalledTimes(1);
expect(messagingService.send).toHaveBeenCalledWith("redrawMenu");
});
});
describe("given the initial password was NOT successfully set (due to some error in setInitialPassword())", () => {
it("should NOT send a 'redrawMenu' message", async () => {
// Arrange
credentials.newMasterKey = null; // will trigger an error in setInitialPassword()
setupMocks();
// Act
const promise = sut.setInitialPassword(credentials, userType, userId);
// Assert
await expect(promise).rejects.toThrow();
expect(masterPasswordApiService.setPassword).not.toHaveBeenCalled();
expect(messagingService.send).not.toHaveBeenCalled();
});
});
});
});

View File

@@ -0,0 +1,59 @@
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { DefaultSetInitialPasswordService } from "@bitwarden/angular/auth/password-management/set-initial-password/default-set-initial-password.service.implementation";
import {
SetInitialPasswordCredentials,
SetInitialPasswordService,
SetInitialPasswordUserType,
} from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.service.abstraction";
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 { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { UserId } from "@bitwarden/common/types/guid";
import { KdfConfigService, KeyService } from "@bitwarden/key-management";
export class DesktopSetInitialPasswordService
extends DefaultSetInitialPasswordService
implements SetInitialPasswordService
{
constructor(
protected apiService: ApiService,
protected encryptService: EncryptService,
protected i18nService: I18nService,
protected kdfConfigService: KdfConfigService,
protected keyService: KeyService,
protected masterPasswordApiService: MasterPasswordApiService,
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
protected organizationApiService: OrganizationApiServiceAbstraction,
protected organizationUserApiService: OrganizationUserApiService,
protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
private messagingService: MessagingService,
) {
super(
apiService,
encryptService,
i18nService,
kdfConfigService,
keyService,
masterPasswordApiService,
masterPasswordService,
organizationApiService,
organizationUserApiService,
userDecryptionOptionsService,
);
}
override async setInitialPassword(
credentials: SetInitialPasswordCredentials,
userType: SetInitialPasswordUserType,
userId: UserId,
) {
await super.setInitialPassword(credentials, userType, userId);
this.messagingService.send("redrawMenu");
}
}

View File

@@ -2392,6 +2392,9 @@
"passwordConfirmationDesc": {
"message": "This action is protected. To continue, please re-enter your master password to verify your identity."
},
"masterPasswordSuccessfullySet": {
"message": "Master password successfully set"
},
"updatedMasterPassword": {
"message": "Updated master password"
},