mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 07:43:35 +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:
@@ -2919,6 +2919,9 @@
|
||||
"emailVerificationRequiredDesc": {
|
||||
"message": "You must verify your email to use this feature. You can verify your email in the web vault."
|
||||
},
|
||||
"masterPasswordSuccessfullySet": {
|
||||
"message": "Master password successfully set"
|
||||
},
|
||||
"updatedMasterPassword": {
|
||||
"message": "Updated master password"
|
||||
},
|
||||
|
||||
@@ -15,6 +15,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 {
|
||||
DevicesIcon,
|
||||
LoginComponent,
|
||||
@@ -38,6 +40,7 @@ import {
|
||||
UserLockIcon,
|
||||
VaultIcon,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, Icons } from "@bitwarden/components";
|
||||
import { LockComponent } from "@bitwarden/key-management-ui";
|
||||
|
||||
@@ -376,6 +379,14 @@ const routes: Routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "set-initial-password",
|
||||
canActivate: [canAccessFeature(FeatureFlag.PM16117_SetInitialPasswordRefactor), authGuard],
|
||||
component: SetInitialPasswordComponent,
|
||||
data: {
|
||||
elevation: 1,
|
||||
} satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
path: "login",
|
||||
canActivate: [unauthGuardFn(unauthRouteOverrides), IntroCarouselGuard],
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@ export * from "./change-password";
|
||||
export * from "./login";
|
||||
export * from "./login-decryption-options";
|
||||
export * from "./webauthn-login";
|
||||
export * from "./password-management";
|
||||
export * from "./set-password-jit";
|
||||
export * from "./registration";
|
||||
export * from "./two-factor-auth";
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./set-initial-password/web-set-initial-password.service";
|
||||
@@ -0,0 +1,208 @@
|
||||
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 { 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 { AcceptOrganizationInviteService } from "@bitwarden/web-vault/app/auth/organization-invite/accept-organization.service";
|
||||
import { RouterService } from "@bitwarden/web-vault/app/core";
|
||||
|
||||
import { WebSetInitialPasswordService } from "./web-set-initial-password.service";
|
||||
|
||||
describe("WebSetInitialPasswordService", () => {
|
||||
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 acceptOrganizationInviteService: MockProxy<AcceptOrganizationInviteService>;
|
||||
let routerService: MockProxy<RouterService>;
|
||||
|
||||
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>();
|
||||
acceptOrganizationInviteService = mock<AcceptOrganizationInviteService>();
|
||||
routerService = mock<RouterService>();
|
||||
|
||||
sut = new WebSetInitialPasswordService(
|
||||
apiService,
|
||||
encryptService,
|
||||
i18nService,
|
||||
kdfConfigService,
|
||||
keyService,
|
||||
masterPasswordApiService,
|
||||
masterPasswordService,
|
||||
organizationApiService,
|
||||
organizationUserApiService,
|
||||
userDecryptionOptionsService,
|
||||
acceptOrganizationInviteService,
|
||||
routerService,
|
||||
);
|
||||
});
|
||||
|
||||
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 call routerService.getAndClearLoginRedirectUrl()", async () => {
|
||||
// Arrange
|
||||
setupMocks();
|
||||
|
||||
// Act
|
||||
await sut.setInitialPassword(credentials, userType, userId);
|
||||
|
||||
// Assert
|
||||
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||
expect(routerService.getAndClearLoginRedirectUrl).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should call acceptOrganizationInviteService.clearOrganizationInvitation()", async () => {
|
||||
// Arrange
|
||||
setupMocks();
|
||||
|
||||
// Act
|
||||
await sut.setInitialPassword(credentials, userType, userId);
|
||||
|
||||
// Assert
|
||||
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||
expect(acceptOrganizationInviteService.clearOrganizationInvitation).toHaveBeenCalledTimes(
|
||||
1,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given the initial password was NOT successfully set (due to some error in setInitialPassword())", () => {
|
||||
it("should NOT call routerService.getAndClearLoginRedirectUrl()", 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(routerService.getAndClearLoginRedirectUrl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should NOT call acceptOrganizationInviteService.clearOrganizationInvitation()", 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(acceptOrganizationInviteService.clearOrganizationInvitation).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
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 { UserId } from "@bitwarden/common/types/guid";
|
||||
import { KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
import { AcceptOrganizationInviteService } from "@bitwarden/web-vault/app/auth/organization-invite/accept-organization.service";
|
||||
import { RouterService } from "@bitwarden/web-vault/app/core";
|
||||
|
||||
export class WebSetInitialPasswordService
|
||||
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 acceptOrganizationInviteService: AcceptOrganizationInviteService,
|
||||
private routerService: RouterService,
|
||||
) {
|
||||
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);
|
||||
|
||||
/**
|
||||
* TODO: Investigate refactoring the following logic in https://bitwarden.atlassian.net/browse/PM-22615
|
||||
* ---
|
||||
* When a user has been invited to an org, they can be accepted into the org in two different ways:
|
||||
*
|
||||
* 1) By clicking the email invite link, which triggers the normal AcceptOrganizationComponent flow
|
||||
* a. This flow sets an org invite in state
|
||||
* b. However, if the user does not already have an account AND the org has SSO enabled AND the require
|
||||
* SSO policy enabled, the AcceptOrganizationComponent will send the user to /sso to accelerate
|
||||
* the user through the SSO JIT provisioning process (see #2 below)
|
||||
*
|
||||
* 2) By logging in via SSO, which triggers the JIT provisioning process
|
||||
* a. This flow does NOT (itself) set an org invite in state
|
||||
* b. The set initial password process on the server accepts the user into the org after successfully
|
||||
* setting the password (see server - SetInitialMasterPasswordCommand.cs)
|
||||
*
|
||||
* If a user clicks the email link but gets accelerated through the SSO JIT process (see 1b),
|
||||
* the SSO JIT process will accept the user into the org upon setting their initial password (see 2b),
|
||||
* at which point we must remember to clear the deep linked URL used for accepting the org invite, as well
|
||||
* as clear the org invite itself that was originally set in state by the AcceptOrganizationComponent.
|
||||
*/
|
||||
await this.routerService.getAndClearLoginRedirectUrl();
|
||||
await this.acceptOrganizationInviteService.clearOrganizationInvitation();
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
OrganizationUserApiService,
|
||||
CollectionService,
|
||||
} 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 {
|
||||
CLIENT_TYPE,
|
||||
@@ -117,6 +118,7 @@ import {
|
||||
WebLoginDecryptionOptionsService,
|
||||
WebTwoFactorAuthDuoComponentService,
|
||||
LinkSsoService,
|
||||
WebSetInitialPasswordService,
|
||||
} from "../auth";
|
||||
import { WebSsoComponentService } from "../auth/core/services/login/web-sso-component.service";
|
||||
import { AcceptOrganizationInviteService } from "../auth/organization-invite/accept-organization.service";
|
||||
@@ -283,6 +285,24 @@ const safeProviders: SafeProvider[] = [
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SetInitialPasswordService,
|
||||
useClass: WebSetInitialPasswordService,
|
||||
deps: [
|
||||
ApiService,
|
||||
EncryptService,
|
||||
I18nServiceAbstraction,
|
||||
KdfConfigService,
|
||||
KeyServiceAbstraction,
|
||||
MasterPasswordApiService,
|
||||
InternalMasterPasswordServiceAbstraction,
|
||||
OrganizationApiServiceAbstraction,
|
||||
OrganizationUserApiService,
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
AcceptOrganizationInviteService,
|
||||
RouterService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AppIdService,
|
||||
useClass: DefaultAppIdService,
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
unauthGuardFn,
|
||||
activeAuthGuard,
|
||||
} 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 {
|
||||
PasswordHintComponent,
|
||||
RegistrationFinishComponent,
|
||||
@@ -36,6 +38,7 @@ import {
|
||||
NewDeviceVerificationComponent,
|
||||
DeviceVerificationIcon,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, Icons } from "@bitwarden/components";
|
||||
import { LockComponent } from "@bitwarden/key-management-ui";
|
||||
import { VaultIcons } from "@bitwarden/vault";
|
||||
@@ -305,6 +308,14 @@ const routes: Routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "set-initial-password",
|
||||
canActivate: [canAccessFeature(FeatureFlag.PM16117_SetInitialPasswordRefactor), authGuard],
|
||||
component: SetInitialPasswordComponent,
|
||||
data: {
|
||||
maxWidth: "lg",
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
},
|
||||
{
|
||||
path: "set-password-jit",
|
||||
component: SetPasswordJitComponent,
|
||||
|
||||
@@ -6065,6 +6065,9 @@
|
||||
"add": {
|
||||
"message": "Add"
|
||||
},
|
||||
"masterPasswordSuccessfullySet": {
|
||||
"message": "Master password successfully set"
|
||||
},
|
||||
"updatedMasterPassword": {
|
||||
"message": "Master password saved"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user