mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 06:13:38 +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": {
|
"emailVerificationRequiredDesc": {
|
||||||
"message": "You must verify your email to use this feature. You can verify your email in the web vault."
|
"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": {
|
"updatedMasterPassword": {
|
||||||
"message": "Updated master password"
|
"message": "Updated master password"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import {
|
|||||||
tdeDecryptionRequiredGuard,
|
tdeDecryptionRequiredGuard,
|
||||||
unauthGuardFn,
|
unauthGuardFn,
|
||||||
} from "@bitwarden/angular/auth/guards";
|
} 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 {
|
import {
|
||||||
DevicesIcon,
|
DevicesIcon,
|
||||||
LoginComponent,
|
LoginComponent,
|
||||||
@@ -38,6 +40,7 @@ import {
|
|||||||
UserLockIcon,
|
UserLockIcon,
|
||||||
VaultIcon,
|
VaultIcon,
|
||||||
} from "@bitwarden/auth/angular";
|
} from "@bitwarden/auth/angular";
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, Icons } from "@bitwarden/components";
|
import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, Icons } from "@bitwarden/components";
|
||||||
import { LockComponent } from "@bitwarden/key-management-ui";
|
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",
|
path: "login",
|
||||||
canActivate: [unauthGuardFn(unauthRouteOverrides), IntroCarouselGuard],
|
canActivate: [unauthGuardFn(unauthRouteOverrides), IntroCarouselGuard],
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import {
|
|||||||
tdeDecryptionRequiredGuard,
|
tdeDecryptionRequiredGuard,
|
||||||
unauthGuardFn,
|
unauthGuardFn,
|
||||||
} from "@bitwarden/angular/auth/guards";
|
} 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 { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route";
|
||||||
import {
|
import {
|
||||||
LoginComponent,
|
LoginComponent,
|
||||||
@@ -315,6 +317,14 @@ const routes: Routes = [
|
|||||||
},
|
},
|
||||||
} satisfies AnonLayoutWrapperData,
|
} satisfies AnonLayoutWrapperData,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "set-initial-password",
|
||||||
|
canActivate: [canAccessFeature(FeatureFlag.PM16117_SetInitialPasswordRefactor), authGuard],
|
||||||
|
component: SetInitialPasswordComponent,
|
||||||
|
data: {
|
||||||
|
maxWidth: "lg",
|
||||||
|
} satisfies AnonLayoutWrapperData,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "2fa",
|
path: "2fa",
|
||||||
canActivate: [unauthGuardFn(), TwoFactorAuthGuard],
|
canActivate: [unauthGuardFn(), TwoFactorAuthGuard],
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Router } from "@angular/router";
|
|||||||
import { Subject, merge } from "rxjs";
|
import { Subject, merge } from "rxjs";
|
||||||
|
|
||||||
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
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 { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
|
||||||
import {
|
import {
|
||||||
SECURE_STORAGE,
|
SECURE_STORAGE,
|
||||||
@@ -140,6 +141,7 @@ import { DesktopSetPasswordJitService } from "./desktop-set-password-jit.service
|
|||||||
import { InitService } from "./init.service";
|
import { InitService } from "./init.service";
|
||||||
import { NativeMessagingManifestService } from "./native-messaging-manifest.service";
|
import { NativeMessagingManifestService } from "./native-messaging-manifest.service";
|
||||||
import { RendererCryptoFunctionService } from "./renderer-crypto-function.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");
|
const RELOAD_CALLBACK = new SafeInjectionToken<() => any>("RELOAD_CALLBACK");
|
||||||
|
|
||||||
@@ -392,6 +394,23 @@ const safeProviders: SafeProvider[] = [
|
|||||||
InternalUserDecryptionOptionsServiceAbstraction,
|
InternalUserDecryptionOptionsServiceAbstraction,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: SetInitialPasswordService,
|
||||||
|
useClass: DesktopSetInitialPasswordService,
|
||||||
|
deps: [
|
||||||
|
ApiService,
|
||||||
|
EncryptService,
|
||||||
|
I18nServiceAbstraction,
|
||||||
|
KdfConfigService,
|
||||||
|
KeyService,
|
||||||
|
MasterPasswordApiService,
|
||||||
|
InternalMasterPasswordServiceAbstraction,
|
||||||
|
OrganizationApiServiceAbstraction,
|
||||||
|
OrganizationUserApiService,
|
||||||
|
InternalUserDecryptionOptionsServiceAbstraction,
|
||||||
|
MessagingServiceAbstraction,
|
||||||
|
],
|
||||||
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: SsoUrlService,
|
provide: SsoUrlService,
|
||||||
useClass: 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": {
|
"passwordConfirmationDesc": {
|
||||||
"message": "This action is protected. To continue, please re-enter your master password to verify your identity."
|
"message": "This action is protected. To continue, please re-enter your master password to verify your identity."
|
||||||
},
|
},
|
||||||
|
"masterPasswordSuccessfullySet": {
|
||||||
|
"message": "Master password successfully set"
|
||||||
|
},
|
||||||
"updatedMasterPassword": {
|
"updatedMasterPassword": {
|
||||||
"message": "Updated master password"
|
"message": "Updated master password"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export * from "./change-password";
|
|||||||
export * from "./login";
|
export * from "./login";
|
||||||
export * from "./login-decryption-options";
|
export * from "./login-decryption-options";
|
||||||
export * from "./webauthn-login";
|
export * from "./webauthn-login";
|
||||||
|
export * from "./password-management";
|
||||||
export * from "./set-password-jit";
|
export * from "./set-password-jit";
|
||||||
export * from "./registration";
|
export * from "./registration";
|
||||||
export * from "./two-factor-auth";
|
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,
|
OrganizationUserApiService,
|
||||||
CollectionService,
|
CollectionService,
|
||||||
} from "@bitwarden/admin-console/common";
|
} 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 { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
|
||||||
import {
|
import {
|
||||||
CLIENT_TYPE,
|
CLIENT_TYPE,
|
||||||
@@ -117,6 +118,7 @@ import {
|
|||||||
WebLoginDecryptionOptionsService,
|
WebLoginDecryptionOptionsService,
|
||||||
WebTwoFactorAuthDuoComponentService,
|
WebTwoFactorAuthDuoComponentService,
|
||||||
LinkSsoService,
|
LinkSsoService,
|
||||||
|
WebSetInitialPasswordService,
|
||||||
} from "../auth";
|
} from "../auth";
|
||||||
import { WebSsoComponentService } from "../auth/core/services/login/web-sso-component.service";
|
import { WebSsoComponentService } from "../auth/core/services/login/web-sso-component.service";
|
||||||
import { AcceptOrganizationInviteService } from "../auth/organization-invite/accept-organization.service";
|
import { AcceptOrganizationInviteService } from "../auth/organization-invite/accept-organization.service";
|
||||||
@@ -283,6 +285,24 @@ const safeProviders: SafeProvider[] = [
|
|||||||
InternalUserDecryptionOptionsServiceAbstraction,
|
InternalUserDecryptionOptionsServiceAbstraction,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: SetInitialPasswordService,
|
||||||
|
useClass: WebSetInitialPasswordService,
|
||||||
|
deps: [
|
||||||
|
ApiService,
|
||||||
|
EncryptService,
|
||||||
|
I18nServiceAbstraction,
|
||||||
|
KdfConfigService,
|
||||||
|
KeyServiceAbstraction,
|
||||||
|
MasterPasswordApiService,
|
||||||
|
InternalMasterPasswordServiceAbstraction,
|
||||||
|
OrganizationApiServiceAbstraction,
|
||||||
|
OrganizationUserApiService,
|
||||||
|
InternalUserDecryptionOptionsServiceAbstraction,
|
||||||
|
AcceptOrganizationInviteService,
|
||||||
|
RouterService,
|
||||||
|
],
|
||||||
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: AppIdService,
|
provide: AppIdService,
|
||||||
useClass: DefaultAppIdService,
|
useClass: DefaultAppIdService,
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
unauthGuardFn,
|
unauthGuardFn,
|
||||||
activeAuthGuard,
|
activeAuthGuard,
|
||||||
} from "@bitwarden/angular/auth/guards";
|
} 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 {
|
import {
|
||||||
PasswordHintComponent,
|
PasswordHintComponent,
|
||||||
RegistrationFinishComponent,
|
RegistrationFinishComponent,
|
||||||
@@ -36,6 +38,7 @@ import {
|
|||||||
NewDeviceVerificationComponent,
|
NewDeviceVerificationComponent,
|
||||||
DeviceVerificationIcon,
|
DeviceVerificationIcon,
|
||||||
} from "@bitwarden/auth/angular";
|
} from "@bitwarden/auth/angular";
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, Icons } from "@bitwarden/components";
|
import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, Icons } from "@bitwarden/components";
|
||||||
import { LockComponent } from "@bitwarden/key-management-ui";
|
import { LockComponent } from "@bitwarden/key-management-ui";
|
||||||
import { VaultIcons } from "@bitwarden/vault";
|
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",
|
path: "set-password-jit",
|
||||||
component: SetPasswordJitComponent,
|
component: SetPasswordJitComponent,
|
||||||
|
|||||||
@@ -6065,6 +6065,9 @@
|
|||||||
"add": {
|
"add": {
|
||||||
"message": "Add"
|
"message": "Add"
|
||||||
},
|
},
|
||||||
|
"masterPasswordSuccessfullySet": {
|
||||||
|
"message": "Master password successfully set"
|
||||||
|
},
|
||||||
"updatedMasterPassword": {
|
"updatedMasterPassword": {
|
||||||
"message": "Master password saved"
|
"message": "Master password saved"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -39,7 +39,32 @@ export const authGuard: CanActivateFn = async (
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authStatus === AuthenticationStatus.Locked) {
|
const userId = (await firstValueFrom(accountService.activeAccount$)).id;
|
||||||
|
const forceSetPasswordReason = await firstValueFrom(
|
||||||
|
masterPasswordService.forceSetPasswordReason$(userId),
|
||||||
|
);
|
||||||
|
|
||||||
|
const isSetInitialPasswordFlagOn = await configService.getFeatureFlag(
|
||||||
|
FeatureFlag.PM16117_SetInitialPasswordRefactor,
|
||||||
|
);
|
||||||
|
const isChangePasswordFlagOn = await configService.getFeatureFlag(
|
||||||
|
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
|
||||||
|
);
|
||||||
|
|
||||||
|
// User JIT provisioned into a master-password-encryption org
|
||||||
|
if (
|
||||||
|
authStatus === AuthenticationStatus.Locked &&
|
||||||
|
forceSetPasswordReason === ForceSetPasswordReason.SsoNewJitProvisionedUser &&
|
||||||
|
!routerState.url.includes("set-initial-password") &&
|
||||||
|
isSetInitialPasswordFlagOn
|
||||||
|
) {
|
||||||
|
return router.createUrlTree(["/set-initial-password"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
authStatus === AuthenticationStatus.Locked &&
|
||||||
|
forceSetPasswordReason !== ForceSetPasswordReason.SsoNewJitProvisionedUser
|
||||||
|
) {
|
||||||
if (routerState != null) {
|
if (routerState != null) {
|
||||||
messagingService.send("lockedUrl", { url: routerState.url });
|
messagingService.send("lockedUrl", { url: routerState.url });
|
||||||
}
|
}
|
||||||
@@ -55,18 +80,6 @@ export const authGuard: CanActivateFn = async (
|
|||||||
return router.createUrlTree(["/remove-password"]);
|
return router.createUrlTree(["/remove-password"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const userId = (await firstValueFrom(accountService.activeAccount$)).id;
|
|
||||||
const forceSetPasswordReason = await firstValueFrom(
|
|
||||||
masterPasswordService.forceSetPasswordReason$(userId),
|
|
||||||
);
|
|
||||||
|
|
||||||
const isSetInitialPasswordFlagOn = await configService.getFeatureFlag(
|
|
||||||
FeatureFlag.PM16117_SetInitialPasswordRefactor,
|
|
||||||
);
|
|
||||||
const isChangePasswordFlagOn = await configService.getFeatureFlag(
|
|
||||||
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
|
|
||||||
);
|
|
||||||
|
|
||||||
// TDE org user has "manage account recovery" permission
|
// TDE org user has "manage account recovery" permission
|
||||||
if (
|
if (
|
||||||
forceSetPasswordReason ===
|
forceSetPasswordReason ===
|
||||||
|
|||||||
@@ -0,0 +1,248 @@
|
|||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
OrganizationUserApiService,
|
||||||
|
OrganizationUserResetPasswordEnrollmentRequest,
|
||||||
|
} from "@bitwarden/admin-console/common";
|
||||||
|
// 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 { 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";
|
||||||
|
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 { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||||
|
import { KdfConfigService, KeyService, KdfConfig } from "@bitwarden/key-management";
|
||||||
|
|
||||||
|
import {
|
||||||
|
SetInitialPasswordService,
|
||||||
|
SetInitialPasswordCredentials,
|
||||||
|
SetInitialPasswordUserType,
|
||||||
|
} from "./set-initial-password.service.abstraction";
|
||||||
|
|
||||||
|
export class 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,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async setInitialPassword(
|
||||||
|
credentials: SetInitialPasswordCredentials,
|
||||||
|
userType: SetInitialPasswordUserType,
|
||||||
|
userId: UserId,
|
||||||
|
): Promise<void> {
|
||||||
|
const {
|
||||||
|
newMasterKey,
|
||||||
|
newServerMasterKeyHash,
|
||||||
|
newLocalMasterKeyHash,
|
||||||
|
newPasswordHint,
|
||||||
|
kdfConfig,
|
||||||
|
orgSsoIdentifier,
|
||||||
|
orgId,
|
||||||
|
resetPasswordAutoEnroll,
|
||||||
|
} = credentials;
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(credentials)) {
|
||||||
|
if (value == null) {
|
||||||
|
throw new Error(`${key} not found. Could not set password.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (userId == null) {
|
||||||
|
throw new Error("userId not found. Could not set password.");
|
||||||
|
}
|
||||||
|
if (userType == null) {
|
||||||
|
throw new Error("userType not found. Could not set password.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const masterKeyEncryptedUserKey = await this.makeMasterKeyEncryptedUserKey(
|
||||||
|
newMasterKey,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
if (masterKeyEncryptedUserKey == null || !masterKeyEncryptedUserKey[1].encryptedString) {
|
||||||
|
throw new Error("masterKeyEncryptedUserKey not found. Could not set password.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let keyPair: [string, EncString] | null = null;
|
||||||
|
let keysRequest: KeysRequest | null = null;
|
||||||
|
|
||||||
|
if (userType === SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER) {
|
||||||
|
/**
|
||||||
|
* A user being JIT provisioned into a MP encryption org does not yet have a user
|
||||||
|
* asymmetric key pair, so we create it for them here.
|
||||||
|
*
|
||||||
|
* Sidenote:
|
||||||
|
* In the case of a TDE user whose permissions require that they have a MP - that user
|
||||||
|
* will already have a user asymmetric key pair by this point, so we skip this if-block
|
||||||
|
* so that we don't create a new key pair for them.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Extra safety check (see description on https://github.com/bitwarden/clients/pull/10180):
|
||||||
|
// In case we have have a local private key and are not sure whether it has been posted to the server,
|
||||||
|
// we post the local private key instead of generating a new one
|
||||||
|
const existingUserPrivateKey = (await firstValueFrom(
|
||||||
|
this.keyService.userPrivateKey$(userId),
|
||||||
|
)) as Uint8Array;
|
||||||
|
|
||||||
|
const existingUserPublicKey = await firstValueFrom(this.keyService.userPublicKey$(userId));
|
||||||
|
|
||||||
|
if (existingUserPrivateKey != null && existingUserPublicKey != null) {
|
||||||
|
const existingUserPublicKeyB64 = Utils.fromBufferToB64(existingUserPublicKey);
|
||||||
|
|
||||||
|
// Existing key pair
|
||||||
|
keyPair = [
|
||||||
|
existingUserPublicKeyB64,
|
||||||
|
await this.encryptService.wrapDecapsulationKey(
|
||||||
|
existingUserPrivateKey,
|
||||||
|
masterKeyEncryptedUserKey[0],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
// New key pair
|
||||||
|
keyPair = await this.keyService.makeKeyPair(masterKeyEncryptedUserKey[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyPair == null) {
|
||||||
|
throw new Error("keyPair not found. Could not set password.");
|
||||||
|
}
|
||||||
|
if (!keyPair[1].encryptedString) {
|
||||||
|
throw new Error("encrypted private key not found. Could not set password.");
|
||||||
|
}
|
||||||
|
|
||||||
|
keysRequest = new KeysRequest(keyPair[0], keyPair[1].encryptedString);
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = new SetPasswordRequest(
|
||||||
|
newServerMasterKeyHash,
|
||||||
|
masterKeyEncryptedUserKey[1].encryptedString,
|
||||||
|
newPasswordHint,
|
||||||
|
orgSsoIdentifier,
|
||||||
|
keysRequest,
|
||||||
|
kdfConfig.kdfType,
|
||||||
|
kdfConfig.iterations,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.masterPasswordApiService.setPassword(request);
|
||||||
|
|
||||||
|
// Clear force set password reason to allow navigation back to vault.
|
||||||
|
await this.masterPasswordService.setForceSetPasswordReason(ForceSetPasswordReason.None, userId);
|
||||||
|
|
||||||
|
// User now has a password so update account decryption options in state
|
||||||
|
await this.updateAccountDecryptionProperties(
|
||||||
|
newMasterKey,
|
||||||
|
kdfConfig,
|
||||||
|
masterKeyEncryptedUserKey,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the private key only for new JIT provisioned users in MP encryption orgs.
|
||||||
|
* (Existing TDE users will have their private key set on sync or on login.)
|
||||||
|
*/
|
||||||
|
if (keyPair != null && userType === SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER) {
|
||||||
|
if (!keyPair[1].encryptedString) {
|
||||||
|
throw new Error("encrypted private key not found. Could not set private key in state.");
|
||||||
|
}
|
||||||
|
await this.keyService.setPrivateKey(keyPair[1].encryptedString, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.masterPasswordService.setMasterKeyHash(newLocalMasterKeyHash, userId);
|
||||||
|
|
||||||
|
if (resetPasswordAutoEnroll) {
|
||||||
|
await this.handleResetPasswordAutoEnroll(newServerMasterKeyHash, orgId, userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async makeMasterKeyEncryptedUserKey(
|
||||||
|
masterKey: MasterKey,
|
||||||
|
userId: UserId,
|
||||||
|
): Promise<[UserKey, EncString]> {
|
||||||
|
let masterKeyEncryptedUserKey: [UserKey, EncString] | null = null;
|
||||||
|
|
||||||
|
const userKey = await firstValueFrom(this.keyService.userKey$(userId));
|
||||||
|
|
||||||
|
if (userKey == null) {
|
||||||
|
masterKeyEncryptedUserKey = await this.keyService.makeUserKey(masterKey);
|
||||||
|
} else {
|
||||||
|
masterKeyEncryptedUserKey = await this.keyService.encryptUserKeyWithMasterKey(masterKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
return masterKeyEncryptedUserKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async updateAccountDecryptionProperties(
|
||||||
|
masterKey: MasterKey,
|
||||||
|
kdfConfig: KdfConfig,
|
||||||
|
masterKeyEncryptedUserKey: [UserKey, EncString],
|
||||||
|
userId: UserId,
|
||||||
|
) {
|
||||||
|
const userDecryptionOpts = await firstValueFrom(
|
||||||
|
this.userDecryptionOptionsService.userDecryptionOptions$,
|
||||||
|
);
|
||||||
|
userDecryptionOpts.hasMasterPassword = true;
|
||||||
|
await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts);
|
||||||
|
await this.kdfConfigService.setKdfConfig(userId, kdfConfig);
|
||||||
|
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
||||||
|
await this.keyService.setUserKey(masterKeyEncryptedUserKey[0], userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleResetPasswordAutoEnroll(
|
||||||
|
masterKeyHash: string,
|
||||||
|
orgId: string,
|
||||||
|
userId: UserId,
|
||||||
|
) {
|
||||||
|
const organizationKeys = await this.organizationApiService.getKeys(orgId);
|
||||||
|
|
||||||
|
if (organizationKeys == null) {
|
||||||
|
throw new Error(
|
||||||
|
"Organization keys response is null. Could not handle reset password auto enroll.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const orgPublicKey = Utils.fromB64ToArray(organizationKeys.publicKey);
|
||||||
|
const userKey = await firstValueFrom(this.keyService.userKey$(userId));
|
||||||
|
|
||||||
|
if (userKey == null) {
|
||||||
|
throw new Error("userKey not found. Could not handle reset password auto enroll.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// RSA encrypt user key with organization public key
|
||||||
|
const orgPublicKeyEncryptedUserKey = await this.encryptService.encapsulateKeyUnsigned(
|
||||||
|
userKey,
|
||||||
|
orgPublicKey,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (orgPublicKeyEncryptedUserKey == null || !orgPublicKeyEncryptedUserKey.encryptedString) {
|
||||||
|
throw new Error(
|
||||||
|
"orgPublicKeyEncryptedUserKey not found. Could not handle reset password auto enroll.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const enrollmentRequest = new OrganizationUserResetPasswordEnrollmentRequest();
|
||||||
|
enrollmentRequest.masterPasswordHash = masterKeyHash;
|
||||||
|
enrollmentRequest.resetPasswordKey = orgPublicKeyEncryptedUserKey.encryptedString;
|
||||||
|
|
||||||
|
await this.organizationUserApiService.putOrganizationUserResetPasswordEnrollment(
|
||||||
|
orgId,
|
||||||
|
userId,
|
||||||
|
enrollmentRequest,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,633 @@
|
|||||||
|
import { MockProxy, mock } from "jest-mock-extended";
|
||||||
|
import { BehaviorSubject, of } from "rxjs";
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
OrganizationUserApiService,
|
||||||
|
OrganizationUserResetPasswordEnrollmentRequest,
|
||||||
|
} from "@bitwarden/admin-console/common";
|
||||||
|
// 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 {
|
||||||
|
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";
|
||||||
|
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||||
|
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 { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
import { EncryptedString, 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, UserPrivateKey, UserPublicKey } from "@bitwarden/common/types/key";
|
||||||
|
import { DEFAULT_KDF_CONFIG, KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||||
|
|
||||||
|
import { DefaultSetInitialPasswordService } from "./default-set-initial-password.service.implementation";
|
||||||
|
import {
|
||||||
|
SetInitialPasswordCredentials,
|
||||||
|
SetInitialPasswordService,
|
||||||
|
SetInitialPasswordUserType,
|
||||||
|
} from "./set-initial-password.service.abstraction";
|
||||||
|
|
||||||
|
describe("DefaultSetInitialPasswordService", () => {
|
||||||
|
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>;
|
||||||
|
|
||||||
|
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>();
|
||||||
|
|
||||||
|
sut = new DefaultSetInitialPasswordService(
|
||||||
|
apiService,
|
||||||
|
encryptService,
|
||||||
|
i18nService,
|
||||||
|
kdfConfigService,
|
||||||
|
keyService,
|
||||||
|
masterPasswordApiService,
|
||||||
|
masterPasswordService,
|
||||||
|
organizationApiService,
|
||||||
|
organizationUserApiService,
|
||||||
|
userDecryptionOptionsService,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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 existingUserPublicKey: UserPublicKey;
|
||||||
|
let existingUserPrivateKey: UserPrivateKey;
|
||||||
|
let userKeyEncryptedPrivateKey: EncString;
|
||||||
|
|
||||||
|
let keyPair: [string, EncString];
|
||||||
|
let keysRequest: KeysRequest;
|
||||||
|
|
||||||
|
let organizationKeys: OrganizationKeysResponse;
|
||||||
|
let orgPublicKeyEncryptedUserKey: EncString;
|
||||||
|
|
||||||
|
let userDecryptionOptions: UserDecryptionOptions;
|
||||||
|
let userDecryptionOptionsSubject: BehaviorSubject<UserDecryptionOptions>;
|
||||||
|
let setPasswordRequest: SetPasswordRequest;
|
||||||
|
|
||||||
|
let enrollmentRequest: OrganizationUserResetPasswordEnrollmentRequest;
|
||||||
|
|
||||||
|
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];
|
||||||
|
|
||||||
|
existingUserPublicKey = Utils.fromB64ToArray("existingUserPublicKey") as UserPublicKey;
|
||||||
|
existingUserPrivateKey = Utils.fromB64ToArray("existingUserPrivateKey") as UserPrivateKey;
|
||||||
|
userKeyEncryptedPrivateKey = new EncString("userKeyEncryptedPrivateKey");
|
||||||
|
|
||||||
|
keyPair = ["publicKey", new EncString("privateKey")];
|
||||||
|
keysRequest = new KeysRequest(keyPair[0], keyPair[1].encryptedString);
|
||||||
|
|
||||||
|
organizationKeys = {
|
||||||
|
privateKey: "orgPrivateKey",
|
||||||
|
publicKey: "orgPublicKey",
|
||||||
|
} as OrganizationKeysResponse;
|
||||||
|
orgPublicKeyEncryptedUserKey = new EncString("orgPublicKeyEncryptedUserKey");
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
|
||||||
|
enrollmentRequest = new OrganizationUserResetPasswordEnrollmentRequest();
|
||||||
|
enrollmentRequest.masterPasswordHash = credentials.newServerMasterKeyHash;
|
||||||
|
enrollmentRequest.resetPasswordKey = orgPublicKeyEncryptedUserKey.encryptedString;
|
||||||
|
});
|
||||||
|
|
||||||
|
interface MockConfig {
|
||||||
|
userType: SetInitialPasswordUserType;
|
||||||
|
userHasUserKey: boolean;
|
||||||
|
userHasLocalKeyPair: boolean;
|
||||||
|
resetPasswordAutoEnroll: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultMockConfig: MockConfig = {
|
||||||
|
userType: SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER,
|
||||||
|
userHasUserKey: true,
|
||||||
|
userHasLocalKeyPair: false,
|
||||||
|
resetPasswordAutoEnroll: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
function setupMocks(config: MockConfig = defaultMockConfig) {
|
||||||
|
// Mock makeMasterKeyEncryptedUserKey() values
|
||||||
|
if (config.userHasUserKey) {
|
||||||
|
keyService.userKey$.mockReturnValue(of(userKey));
|
||||||
|
keyService.encryptUserKeyWithMasterKey.mockResolvedValue(masterKeyEncryptedUserKey);
|
||||||
|
} else {
|
||||||
|
keyService.userKey$.mockReturnValue(of(null));
|
||||||
|
keyService.makeUserKey.mockResolvedValue(masterKeyEncryptedUserKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock keyPair values
|
||||||
|
if (config.userType === SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER) {
|
||||||
|
if (config.userHasLocalKeyPair) {
|
||||||
|
keyService.userPrivateKey$.mockReturnValue(of(existingUserPrivateKey));
|
||||||
|
keyService.userPublicKey$.mockReturnValue(of(existingUserPublicKey));
|
||||||
|
encryptService.wrapDecapsulationKey.mockResolvedValue(userKeyEncryptedPrivateKey);
|
||||||
|
} else {
|
||||||
|
keyService.userPrivateKey$.mockReturnValue(of(null));
|
||||||
|
keyService.userPublicKey$.mockReturnValue(of(null));
|
||||||
|
keyService.makeKeyPair.mockResolvedValue(keyPair);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock handleResetPasswordAutoEnroll() values
|
||||||
|
if (config.resetPasswordAutoEnroll) {
|
||||||
|
organizationApiService.getKeys.mockResolvedValue(organizationKeys);
|
||||||
|
encryptService.encapsulateKeyUnsigned.mockResolvedValue(orgPublicKeyEncryptedUserKey);
|
||||||
|
keyService.userKey$.mockReturnValue(of(userKey));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("general error handling", () => {
|
||||||
|
[
|
||||||
|
"newMasterKey",
|
||||||
|
"newServerMasterKeyHash",
|
||||||
|
"newLocalMasterKeyHash",
|
||||||
|
"newPasswordHint",
|
||||||
|
"kdfConfig",
|
||||||
|
"orgSsoIdentifier",
|
||||||
|
"orgId",
|
||||||
|
"resetPasswordAutoEnroll",
|
||||||
|
].forEach((key) => {
|
||||||
|
it(`should throw if ${key} is not provided on the SetInitialPasswordCredentials object`, async () => {
|
||||||
|
// Arrange
|
||||||
|
const invalidCredentials: SetInitialPasswordCredentials = {
|
||||||
|
...credentials,
|
||||||
|
[key]: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const promise = sut.setInitialPassword(invalidCredentials, userType, userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await expect(promise).rejects.toThrow(`${key} not found. Could not set password.`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
["userId", "userType"].forEach((param) => {
|
||||||
|
it(`should throw if ${param} was not passed in`, async () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const promise = sut.setInitialPassword(
|
||||||
|
credentials,
|
||||||
|
param === "userType" ? null : userType,
|
||||||
|
param === "userId" ? null : userId,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await expect(promise).rejects.toThrow(`${param} not found. Could not set password.`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
userType = SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given the user has an existing local key pair", () => {
|
||||||
|
it("should NOT create a brand new key pair for the user", async () => {
|
||||||
|
// Arrange
|
||||||
|
setPasswordRequest.keys = {
|
||||||
|
encryptedPrivateKey: userKeyEncryptedPrivateKey.encryptedString,
|
||||||
|
publicKey: Utils.fromBufferToB64(existingUserPublicKey),
|
||||||
|
};
|
||||||
|
|
||||||
|
setupMocks({ ...defaultMockConfig, userHasLocalKeyPair: true });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sut.setInitialPassword(credentials, userType, userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||||
|
expect(keyService.userPrivateKey$).toHaveBeenCalledWith(userId);
|
||||||
|
expect(keyService.userPublicKey$).toHaveBeenCalledWith(userId);
|
||||||
|
expect(encryptService.wrapDecapsulationKey).toHaveBeenCalledWith(
|
||||||
|
existingUserPrivateKey,
|
||||||
|
masterKeyEncryptedUserKey[0],
|
||||||
|
);
|
||||||
|
expect(keyService.makeKeyPair).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given the user has a userKey", () => {
|
||||||
|
it("should successfully set an initial password", async () => {
|
||||||
|
// Arrange
|
||||||
|
setupMocks();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sut.setInitialPassword(credentials, userType, userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given the user does NOT have a userKey", () => {
|
||||||
|
it("should successfully set an initial password", async () => {
|
||||||
|
// Arrange
|
||||||
|
setupMocks({ ...defaultMockConfig, userHasUserKey: false });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sut.setInitialPassword(credentials, userType, userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw if a key pair is not found", async () => {
|
||||||
|
// Arrange
|
||||||
|
keyPair = null;
|
||||||
|
|
||||||
|
setupMocks();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const promise = sut.setInitialPassword(credentials, userType, userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await expect(promise).rejects.toThrow("keyPair not found. Could not set password.");
|
||||||
|
expect(masterPasswordApiService.setPassword).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw if an encrypted private key is not found", async () => {
|
||||||
|
// Arrange
|
||||||
|
keyPair[1].encryptedString = "" as EncryptedString;
|
||||||
|
|
||||||
|
setupMocks();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const promise = sut.setInitialPassword(credentials, userType, userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await expect(promise).rejects.toThrow(
|
||||||
|
"encrypted private key not found. Could not set password.",
|
||||||
|
);
|
||||||
|
expect(masterPasswordApiService.setPassword).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given the initial password has been successfully set", () => {
|
||||||
|
it("should clear the ForceSetPasswordReason by setting it to None", async () => {
|
||||||
|
// Arrange
|
||||||
|
setupMocks();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sut.setInitialPassword(credentials, userType, userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||||
|
expect(masterPasswordService.setForceSetPasswordReason).toHaveBeenCalledWith(
|
||||||
|
ForceSetPasswordReason.None,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update account decryption properties", async () => {
|
||||||
|
// Arrange
|
||||||
|
setupMocks();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sut.setInitialPassword(credentials, userType, userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||||
|
expect(userDecryptionOptionsService.setUserDecryptionOptions).toHaveBeenCalledWith(
|
||||||
|
userDecryptionOptions,
|
||||||
|
);
|
||||||
|
expect(kdfConfigService.setKdfConfig).toHaveBeenCalledWith(userId, credentials.kdfConfig);
|
||||||
|
expect(masterPasswordService.setMasterKey).toHaveBeenCalledWith(
|
||||||
|
credentials.newMasterKey,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
expect(keyService.setUserKey).toHaveBeenCalledWith(masterKeyEncryptedUserKey[0], userId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set the private key to state", async () => {
|
||||||
|
// Arrange
|
||||||
|
setupMocks();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sut.setInitialPassword(credentials, userType, userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||||
|
expect(keyService.setPrivateKey).toHaveBeenCalledWith(keyPair[1].encryptedString, userId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set the local master key hash to state", async () => {
|
||||||
|
// Arrange
|
||||||
|
setupMocks();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sut.setInitialPassword(credentials, userType, userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||||
|
expect(masterPasswordService.setMasterKeyHash).toHaveBeenCalledWith(
|
||||||
|
credentials.newLocalMasterKeyHash,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given resetPasswordAutoEnroll is true", () => {
|
||||||
|
it(`should handle reset password (account recovery) auto enroll`, async () => {
|
||||||
|
// Arrange
|
||||||
|
credentials.resetPasswordAutoEnroll = true;
|
||||||
|
|
||||||
|
setupMocks({ ...defaultMockConfig, resetPasswordAutoEnroll: true });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sut.setInitialPassword(credentials, userType, userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||||
|
expect(
|
||||||
|
organizationUserApiService.putOrganizationUserResetPasswordEnrollment,
|
||||||
|
).toHaveBeenCalledWith(credentials.orgId, userId, enrollmentRequest);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw if organization keys are not found", async () => {
|
||||||
|
// Arrange
|
||||||
|
credentials.resetPasswordAutoEnroll = true;
|
||||||
|
organizationKeys = null;
|
||||||
|
|
||||||
|
setupMocks({ ...defaultMockConfig, resetPasswordAutoEnroll: true });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const promise = sut.setInitialPassword(credentials, userType, userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await expect(promise).rejects.toThrow(
|
||||||
|
"Organization keys response is null. Could not handle reset password auto enroll.",
|
||||||
|
);
|
||||||
|
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||||
|
expect(
|
||||||
|
organizationUserApiService.putOrganizationUserResetPasswordEnrollment,
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
["orgPublicKeyEncryptedUserKey", "orgPublicKeyEncryptedUserKey.encryptedString"].forEach(
|
||||||
|
(property) => {
|
||||||
|
it("should throw if orgPublicKeyEncryptedUserKey is not found", async () => {
|
||||||
|
// Arrange
|
||||||
|
credentials.resetPasswordAutoEnroll = true;
|
||||||
|
|
||||||
|
if (property === "orgPublicKeyEncryptedUserKey") {
|
||||||
|
orgPublicKeyEncryptedUserKey = null;
|
||||||
|
} else {
|
||||||
|
orgPublicKeyEncryptedUserKey.encryptedString = "" as EncryptedString;
|
||||||
|
}
|
||||||
|
|
||||||
|
setupMocks({ ...defaultMockConfig, resetPasswordAutoEnroll: true });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const promise = sut.setInitialPassword(credentials, userType, userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await expect(promise).rejects.toThrow(
|
||||||
|
"orgPublicKeyEncryptedUserKey not found. Could not handle reset password auto enroll.",
|
||||||
|
);
|
||||||
|
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(
|
||||||
|
setPasswordRequest,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
organizationUserApiService.putOrganizationUserResetPasswordEnrollment,
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given resetPasswordAutoEnroll is false", () => {
|
||||||
|
it(`should NOT handle reset password (account recovery) auto enroll`, async () => {
|
||||||
|
// Arrange
|
||||||
|
credentials.resetPasswordAutoEnroll = false;
|
||||||
|
|
||||||
|
setupMocks();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sut.setInitialPassword(credentials, userType, userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||||
|
expect(
|
||||||
|
organizationUserApiService.putOrganizationUserResetPasswordEnrollment,
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given SetInitialPasswordUserType.TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
userType = SetInitialPasswordUserType.TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP;
|
||||||
|
setPasswordRequest.keys = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should NOT generate a keyPair", async () => {
|
||||||
|
// Arrange
|
||||||
|
setupMocks({ ...defaultMockConfig, userType });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sut.setInitialPassword(credentials, userType, userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(keyService.userPrivateKey$).not.toHaveBeenCalled();
|
||||||
|
expect(keyService.userPublicKey$).not.toHaveBeenCalled();
|
||||||
|
expect(encryptService.wrapDecapsulationKey).not.toHaveBeenCalled();
|
||||||
|
expect(keyService.makeKeyPair).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given the user has a userKey", () => {
|
||||||
|
it("should successfully set an initial password", async () => {
|
||||||
|
// Arrange
|
||||||
|
setupMocks({ ...defaultMockConfig, userType });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sut.setInitialPassword(credentials, userType, userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given the user does NOT have a userKey", () => {
|
||||||
|
it("should successfully set an initial password", async () => {
|
||||||
|
// Arrange
|
||||||
|
setupMocks({ ...defaultMockConfig, userType });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sut.setInitialPassword(credentials, userType, userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given the initial password has been successfully set", () => {
|
||||||
|
it("should clear the ForceSetPasswordReason by setting it to None", async () => {
|
||||||
|
// Arrange
|
||||||
|
setupMocks({ ...defaultMockConfig, userType });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sut.setInitialPassword(credentials, userType, userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||||
|
expect(masterPasswordService.setForceSetPasswordReason).toHaveBeenCalledWith(
|
||||||
|
ForceSetPasswordReason.None,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update account decryption properties", async () => {
|
||||||
|
// Arrange
|
||||||
|
setupMocks({ ...defaultMockConfig, userType });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sut.setInitialPassword(credentials, userType, userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||||
|
expect(userDecryptionOptionsService.setUserDecryptionOptions).toHaveBeenCalledWith(
|
||||||
|
userDecryptionOptions,
|
||||||
|
);
|
||||||
|
expect(kdfConfigService.setKdfConfig).toHaveBeenCalledWith(userId, credentials.kdfConfig);
|
||||||
|
expect(masterPasswordService.setMasterKey).toHaveBeenCalledWith(
|
||||||
|
credentials.newMasterKey,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
expect(keyService.setUserKey).toHaveBeenCalledWith(masterKeyEncryptedUserKey[0], userId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should NOT set the private key to state", async () => {
|
||||||
|
// Arrange
|
||||||
|
setupMocks({ ...defaultMockConfig, userType });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sut.setInitialPassword(credentials, userType, userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||||
|
expect(keyService.setPrivateKey).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set the local master key hash to state", async () => {
|
||||||
|
// Arrange
|
||||||
|
setupMocks({ ...defaultMockConfig, userType });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sut.setInitialPassword(credentials, userType, userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||||
|
expect(masterPasswordService.setMasterKeyHash).toHaveBeenCalledWith(
|
||||||
|
credentials.newLocalMasterKeyHash,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given resetPasswordAutoEnroll is true", () => {
|
||||||
|
it(`should handle reset password (account recovery) auto enroll`, async () => {
|
||||||
|
// Arrange
|
||||||
|
credentials.resetPasswordAutoEnroll = true;
|
||||||
|
|
||||||
|
setupMocks({ ...defaultMockConfig, userType, resetPasswordAutoEnroll: true });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sut.setInitialPassword(credentials, userType, userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||||
|
expect(
|
||||||
|
organizationUserApiService.putOrganizationUserResetPasswordEnrollment,
|
||||||
|
).toHaveBeenCalledWith(credentials.orgId, userId, enrollmentRequest);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given resetPasswordAutoEnroll is false", () => {
|
||||||
|
it(`should NOT handle reset password (account recovery) auto enroll`, async () => {
|
||||||
|
// Arrange
|
||||||
|
setupMocks({ ...defaultMockConfig, userType });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sut.setInitialPassword(credentials, userType, userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||||
|
expect(
|
||||||
|
organizationUserApiService.putOrganizationUserResetPasswordEnrollment,
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
@if (initializing) {
|
||||||
|
<div class="tw-flex tw-items-center tw-justify-center">
|
||||||
|
<i
|
||||||
|
class="bwi bwi-spinner bwi-spin bwi-3x"
|
||||||
|
title="{{ 'loading' | i18n }}"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<bit-callout
|
||||||
|
*ngIf="resetPasswordAutoEnroll"
|
||||||
|
type="warning"
|
||||||
|
title="{{ 'resetPasswordPolicyAutoEnroll' | i18n }}"
|
||||||
|
>
|
||||||
|
{{ "resetPasswordAutoEnrollInviteWarning" | i18n }}
|
||||||
|
</bit-callout>
|
||||||
|
|
||||||
|
<auth-input-password
|
||||||
|
[flow]="inputPasswordFlow"
|
||||||
|
[email]="email"
|
||||||
|
[userId]="userId"
|
||||||
|
[loading]="submitting"
|
||||||
|
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
|
||||||
|
[primaryButtonText]="{ key: 'createAccount' }"
|
||||||
|
[secondaryButtonText]="{ key: 'logOut' }"
|
||||||
|
(onPasswordFormSubmit)="handlePasswordFormSubmit($event)"
|
||||||
|
(onSecondaryButtonClick)="logout()"
|
||||||
|
></auth-input-password>
|
||||||
|
}
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import { Component, OnInit } from "@angular/core";
|
||||||
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
InputPasswordComponent,
|
||||||
|
InputPasswordFlow,
|
||||||
|
PasswordInputResult,
|
||||||
|
} from "@bitwarden/auth/angular";
|
||||||
|
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||||
|
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||||
|
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||||
|
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||||
|
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 { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||||
|
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import {
|
||||||
|
AnonLayoutWrapperDataService,
|
||||||
|
CalloutComponent,
|
||||||
|
DialogService,
|
||||||
|
ToastService,
|
||||||
|
} from "@bitwarden/components";
|
||||||
|
import { I18nPipe } from "@bitwarden/ui-common";
|
||||||
|
|
||||||
|
import {
|
||||||
|
SetInitialPasswordCredentials,
|
||||||
|
SetInitialPasswordService,
|
||||||
|
SetInitialPasswordUserType,
|
||||||
|
} from "./set-initial-password.service.abstraction";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
templateUrl: "set-initial-password.component.html",
|
||||||
|
imports: [CalloutComponent, CommonModule, InputPasswordComponent, I18nPipe],
|
||||||
|
})
|
||||||
|
export class SetInitialPasswordComponent implements OnInit {
|
||||||
|
protected inputPasswordFlow = InputPasswordFlow.SetInitialPasswordAuthedUser;
|
||||||
|
|
||||||
|
protected email?: string;
|
||||||
|
protected forceSetPasswordReason?: ForceSetPasswordReason;
|
||||||
|
protected initializing = true;
|
||||||
|
protected masterPasswordPolicyOptions: MasterPasswordPolicyOptions | null = null;
|
||||||
|
protected orgId?: string;
|
||||||
|
protected orgSsoIdentifier?: string;
|
||||||
|
protected resetPasswordAutoEnroll?: boolean;
|
||||||
|
protected submitting = false;
|
||||||
|
protected userId?: UserId;
|
||||||
|
protected userType?: SetInitialPasswordUserType;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private accountService: AccountService,
|
||||||
|
private activatedRoute: ActivatedRoute,
|
||||||
|
private anonLayoutWrapperDataService: AnonLayoutWrapperDataService,
|
||||||
|
private dialogService: DialogService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||||
|
private messagingService: MessagingService,
|
||||||
|
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||||
|
private policyApiService: PolicyApiServiceAbstraction,
|
||||||
|
private router: Router,
|
||||||
|
private setInitialPasswordService: SetInitialPasswordService,
|
||||||
|
private ssoLoginService: SsoLoginServiceAbstraction,
|
||||||
|
private syncService: SyncService,
|
||||||
|
private toastService: ToastService,
|
||||||
|
private validationService: ValidationService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
await this.syncService.fullSync(true);
|
||||||
|
|
||||||
|
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||||
|
this.userId = activeAccount?.id;
|
||||||
|
this.email = activeAccount?.email;
|
||||||
|
|
||||||
|
await this.determineUserType();
|
||||||
|
await this.handleQueryParams();
|
||||||
|
|
||||||
|
this.initializing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async determineUserType() {
|
||||||
|
if (!this.userId) {
|
||||||
|
throw new Error("userId not found. Could not determine user type.");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.forceSetPasswordReason = await firstValueFrom(
|
||||||
|
this.masterPasswordService.forceSetPasswordReason$(this.userId),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.forceSetPasswordReason ===
|
||||||
|
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission
|
||||||
|
) {
|
||||||
|
this.userType = SetInitialPasswordUserType.TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP;
|
||||||
|
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
|
||||||
|
pageTitle: { key: "setMasterPassword" },
|
||||||
|
pageSubtitle: { key: "orgPermissionsUpdatedMustSetPassword" },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.userType = SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER;
|
||||||
|
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
|
||||||
|
pageTitle: { key: "joinOrganization" },
|
||||||
|
pageSubtitle: { key: "finishJoiningThisOrganizationBySettingAMasterPassword" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleQueryParams() {
|
||||||
|
if (!this.userId) {
|
||||||
|
throw new Error("userId not found. Could not handle query params.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const qParams = await firstValueFrom(this.activatedRoute.queryParams);
|
||||||
|
|
||||||
|
this.orgSsoIdentifier =
|
||||||
|
qParams.identifier ??
|
||||||
|
(await this.ssoLoginService.getActiveUserOrganizationSsoIdentifier(this.userId));
|
||||||
|
|
||||||
|
if (this.orgSsoIdentifier != null) {
|
||||||
|
try {
|
||||||
|
const autoEnrollStatus = await this.organizationApiService.getAutoEnrollStatus(
|
||||||
|
this.orgSsoIdentifier,
|
||||||
|
);
|
||||||
|
this.orgId = autoEnrollStatus.id;
|
||||||
|
this.resetPasswordAutoEnroll = autoEnrollStatus.resetPasswordEnabled;
|
||||||
|
this.masterPasswordPolicyOptions =
|
||||||
|
await this.policyApiService.getMasterPasswordPolicyOptsForOrgUser(this.orgId);
|
||||||
|
} catch {
|
||||||
|
this.toastService.showToast({
|
||||||
|
variant: "error",
|
||||||
|
title: "",
|
||||||
|
message: this.i18nService.t("errorOccurred"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async handlePasswordFormSubmit(passwordInputResult: PasswordInputResult) {
|
||||||
|
this.submitting = true;
|
||||||
|
|
||||||
|
if (!passwordInputResult.newMasterKey) {
|
||||||
|
throw new Error("newMasterKey not found. Could not set initial password.");
|
||||||
|
}
|
||||||
|
if (!passwordInputResult.newServerMasterKeyHash) {
|
||||||
|
throw new Error("newServerMasterKeyHash not found. Could not set initial password.");
|
||||||
|
}
|
||||||
|
if (!passwordInputResult.newLocalMasterKeyHash) {
|
||||||
|
throw new Error("newLocalMasterKeyHash not found. Could not set initial password.");
|
||||||
|
}
|
||||||
|
// newPasswordHint can have an empty string as a valid value, so we specifically check for null or undefined
|
||||||
|
if (passwordInputResult.newPasswordHint == null) {
|
||||||
|
throw new Error("newPasswordHint not found. Could not set initial password.");
|
||||||
|
}
|
||||||
|
if (!passwordInputResult.kdfConfig) {
|
||||||
|
throw new Error("kdfConfig not found. Could not set initial password.");
|
||||||
|
}
|
||||||
|
if (!this.userId) {
|
||||||
|
throw new Error("userId not found. Could not set initial password.");
|
||||||
|
}
|
||||||
|
if (!this.userType) {
|
||||||
|
throw new Error("userType not found. Could not set initial password.");
|
||||||
|
}
|
||||||
|
if (!this.orgSsoIdentifier) {
|
||||||
|
throw new Error("orgSsoIdentifier not found. Could not set initial password.");
|
||||||
|
}
|
||||||
|
if (!this.orgId) {
|
||||||
|
throw new Error("orgId not found. Could not set initial password.");
|
||||||
|
}
|
||||||
|
// resetPasswordAutoEnroll can have `false` as a valid value, so we specifically check for null or undefined
|
||||||
|
if (this.resetPasswordAutoEnroll == null) {
|
||||||
|
throw new Error("resetPasswordAutoEnroll not found. Could not set initial password.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const credentials: SetInitialPasswordCredentials = {
|
||||||
|
newMasterKey: passwordInputResult.newMasterKey,
|
||||||
|
newServerMasterKeyHash: passwordInputResult.newServerMasterKeyHash,
|
||||||
|
newLocalMasterKeyHash: passwordInputResult.newLocalMasterKeyHash,
|
||||||
|
newPasswordHint: passwordInputResult.newPasswordHint,
|
||||||
|
kdfConfig: passwordInputResult.kdfConfig,
|
||||||
|
orgSsoIdentifier: this.orgSsoIdentifier,
|
||||||
|
orgId: this.orgId,
|
||||||
|
resetPasswordAutoEnroll: this.resetPasswordAutoEnroll,
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.setInitialPasswordService.setInitialPassword(
|
||||||
|
credentials,
|
||||||
|
this.userType,
|
||||||
|
this.userId,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.showSuccessToastByUserType();
|
||||||
|
|
||||||
|
this.submitting = false;
|
||||||
|
await this.router.navigate(["vault"]);
|
||||||
|
} catch (e) {
|
||||||
|
this.validationService.showError(e);
|
||||||
|
this.submitting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private showSuccessToastByUserType() {
|
||||||
|
if (this.userType === SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER) {
|
||||||
|
this.toastService.showToast({
|
||||||
|
variant: "success",
|
||||||
|
title: "",
|
||||||
|
message: this.i18nService.t("accountSuccessfullyCreated"),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.toastService.showToast({
|
||||||
|
variant: "success",
|
||||||
|
title: "",
|
||||||
|
message: this.i18nService.t("inviteAccepted"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.userType ===
|
||||||
|
SetInitialPasswordUserType.TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP
|
||||||
|
) {
|
||||||
|
this.toastService.showToast({
|
||||||
|
variant: "success",
|
||||||
|
title: "",
|
||||||
|
message: this.i18nService.t("masterPasswordSuccessfullySet"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async logout() {
|
||||||
|
const confirmed = await this.dialogService.openSimpleDialog({
|
||||||
|
title: { key: "logOut" },
|
||||||
|
content: { key: "logOutConfirmation" },
|
||||||
|
acceptButtonText: { key: "logOut" },
|
||||||
|
type: "warning",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (confirmed) {
|
||||||
|
this.messagingService.send("logout");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import { MasterKey } from "@bitwarden/common/types/key";
|
||||||
|
import { KdfConfig } from "@bitwarden/key-management";
|
||||||
|
|
||||||
|
export const _SetInitialPasswordUserType = {
|
||||||
|
/**
|
||||||
|
* A user being "just-in-time" (JIT) provisioned into a master-password-encryption org
|
||||||
|
*/
|
||||||
|
JIT_PROVISIONED_MP_ORG_USER: "jit_provisioned_mp_org_user",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Could be one of two scenarios:
|
||||||
|
* 1. A user being "just-in-time" (JIT) provisioned into a trusted-device-encryption org
|
||||||
|
* with the reset password permission granted ("manage account recovery"), which requires
|
||||||
|
* that the user sets a master password
|
||||||
|
* 2. An user in a trusted-device-encryption org whose permissions were upgraded to include
|
||||||
|
* the reset password permission ("manage account recovery"), which requires that the user
|
||||||
|
* sets a master password
|
||||||
|
*/
|
||||||
|
TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP:
|
||||||
|
"tde_org_user_reset_password_permission_requires_mp",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
type _SetInitialPasswordUserType = typeof _SetInitialPasswordUserType;
|
||||||
|
|
||||||
|
export type SetInitialPasswordUserType =
|
||||||
|
_SetInitialPasswordUserType[keyof _SetInitialPasswordUserType];
|
||||||
|
export const SetInitialPasswordUserType: Readonly<{
|
||||||
|
[K in keyof typeof _SetInitialPasswordUserType]: SetInitialPasswordUserType;
|
||||||
|
}> = Object.freeze(_SetInitialPasswordUserType);
|
||||||
|
|
||||||
|
export interface SetInitialPasswordCredentials {
|
||||||
|
newMasterKey: MasterKey;
|
||||||
|
newServerMasterKeyHash: string;
|
||||||
|
newLocalMasterKeyHash: string;
|
||||||
|
newPasswordHint: string;
|
||||||
|
kdfConfig: KdfConfig;
|
||||||
|
orgSsoIdentifier: string;
|
||||||
|
orgId: string;
|
||||||
|
resetPasswordAutoEnroll: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles setting an initial password for an existing authed user.
|
||||||
|
*
|
||||||
|
* To see the different scenarios where an existing authed user needs to set an
|
||||||
|
* initial password, see {@link SetInitialPasswordUserType}
|
||||||
|
*/
|
||||||
|
export abstract class SetInitialPasswordService {
|
||||||
|
/**
|
||||||
|
* Sets an initial password for an existing authed user who is either:
|
||||||
|
* - {@link SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER}
|
||||||
|
* - {@link SetInitialPasswordUserType.TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP}
|
||||||
|
*
|
||||||
|
* @param credentials An object of the credentials needed to set the initial password
|
||||||
|
* @throws If any property on the `credentials` object is null or undefined, or if a
|
||||||
|
* masterKeyEncryptedUserKey or newKeyPair could not be created.
|
||||||
|
*/
|
||||||
|
abstract setInitialPassword: (
|
||||||
|
credentials: SetInitialPasswordCredentials,
|
||||||
|
userType: SetInitialPasswordUserType,
|
||||||
|
userId: UserId,
|
||||||
|
) => Promise<void>;
|
||||||
|
}
|
||||||
@@ -339,6 +339,8 @@ import {
|
|||||||
VaultExportServiceAbstraction,
|
VaultExportServiceAbstraction,
|
||||||
} from "@bitwarden/vault-export-core";
|
} from "@bitwarden/vault-export-core";
|
||||||
|
|
||||||
|
import { DefaultSetInitialPasswordService } from "../auth/password-management/set-initial-password/default-set-initial-password.service.implementation";
|
||||||
|
import { SetInitialPasswordService } from "../auth/password-management/set-initial-password/set-initial-password.service.abstraction";
|
||||||
import { DeviceTrustToastService as DeviceTrustToastServiceAbstraction } from "../auth/services/device-trust-toast.service.abstraction";
|
import { DeviceTrustToastService as DeviceTrustToastServiceAbstraction } from "../auth/services/device-trust-toast.service.abstraction";
|
||||||
import { DeviceTrustToastService } from "../auth/services/device-trust-toast.service.implementation";
|
import { DeviceTrustToastService } from "../auth/services/device-trust-toast.service.implementation";
|
||||||
import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service";
|
import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service";
|
||||||
@@ -1419,6 +1421,22 @@ const safeProviders: SafeProvider[] = [
|
|||||||
InternalUserDecryptionOptionsServiceAbstraction,
|
InternalUserDecryptionOptionsServiceAbstraction,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: SetInitialPasswordService,
|
||||||
|
useClass: DefaultSetInitialPasswordService,
|
||||||
|
deps: [
|
||||||
|
ApiServiceAbstraction,
|
||||||
|
EncryptService,
|
||||||
|
I18nServiceAbstraction,
|
||||||
|
KdfConfigService,
|
||||||
|
KeyService,
|
||||||
|
MasterPasswordApiServiceAbstraction,
|
||||||
|
InternalMasterPasswordServiceAbstraction,
|
||||||
|
OrganizationApiServiceAbstraction,
|
||||||
|
OrganizationUserApiService,
|
||||||
|
InternalUserDecryptionOptionsServiceAbstraction,
|
||||||
|
],
|
||||||
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: DefaultServerSettingsService,
|
provide: DefaultServerSettingsService,
|
||||||
useClass: DefaultServerSettingsService,
|
useClass: DefaultServerSettingsService,
|
||||||
|
|||||||
Reference in New Issue
Block a user