1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 07:43:35 +00:00

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

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

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

View File

@@ -2919,6 +2919,9 @@
"emailVerificationRequiredDesc": {
"message": "You must verify your email to use this feature. You can verify your email in the web vault."
},
"masterPasswordSuccessfullySet": {
"message": "Master password successfully set"
},
"updatedMasterPassword": {
"message": "Updated master password"
},

View File

@@ -15,6 +15,8 @@ import {
tdeDecryptionRequiredGuard,
unauthGuardFn,
} from "@bitwarden/angular/auth/guards";
import { SetInitialPasswordComponent } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.component";
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
import {
DevicesIcon,
LoginComponent,
@@ -38,6 +40,7 @@ import {
UserLockIcon,
VaultIcon,
} from "@bitwarden/auth/angular";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, Icons } from "@bitwarden/components";
import { LockComponent } from "@bitwarden/key-management-ui";
@@ -376,6 +379,14 @@ const routes: Routes = [
},
],
},
{
path: "set-initial-password",
canActivate: [canAccessFeature(FeatureFlag.PM16117_SetInitialPasswordRefactor), authGuard],
component: SetInitialPasswordComponent,
data: {
elevation: 1,
} satisfies RouteDataProperties,
},
{
path: "login",
canActivate: [unauthGuardFn(unauthRouteOverrides), IntroCarouselGuard],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ export * from "./change-password";
export * from "./login";
export * from "./login-decryption-options";
export * from "./webauthn-login";
export * from "./password-management";
export * from "./set-password-jit";
export * from "./registration";
export * from "./two-factor-auth";

View File

@@ -0,0 +1 @@
export * from "./set-initial-password/web-set-initial-password.service";

View File

@@ -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();
});
});
});
});

View File

@@ -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();
}
}

View File

@@ -10,6 +10,7 @@ import {
OrganizationUserApiService,
CollectionService,
} from "@bitwarden/admin-console/common";
import { SetInitialPasswordService } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.service.abstraction";
import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
import {
CLIENT_TYPE,
@@ -117,6 +118,7 @@ import {
WebLoginDecryptionOptionsService,
WebTwoFactorAuthDuoComponentService,
LinkSsoService,
WebSetInitialPasswordService,
} from "../auth";
import { WebSsoComponentService } from "../auth/core/services/login/web-sso-component.service";
import { AcceptOrganizationInviteService } from "../auth/organization-invite/accept-organization.service";
@@ -283,6 +285,24 @@ const safeProviders: SafeProvider[] = [
InternalUserDecryptionOptionsServiceAbstraction,
],
}),
safeProvider({
provide: SetInitialPasswordService,
useClass: WebSetInitialPasswordService,
deps: [
ApiService,
EncryptService,
I18nServiceAbstraction,
KdfConfigService,
KeyServiceAbstraction,
MasterPasswordApiService,
InternalMasterPasswordServiceAbstraction,
OrganizationApiServiceAbstraction,
OrganizationUserApiService,
InternalUserDecryptionOptionsServiceAbstraction,
AcceptOrganizationInviteService,
RouterService,
],
}),
safeProvider({
provide: AppIdService,
useClass: DefaultAppIdService,

View File

@@ -10,6 +10,8 @@ import {
unauthGuardFn,
activeAuthGuard,
} from "@bitwarden/angular/auth/guards";
import { SetInitialPasswordComponent } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.component";
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
import {
PasswordHintComponent,
RegistrationFinishComponent,
@@ -36,6 +38,7 @@ import {
NewDeviceVerificationComponent,
DeviceVerificationIcon,
} from "@bitwarden/auth/angular";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, Icons } from "@bitwarden/components";
import { LockComponent } from "@bitwarden/key-management-ui";
import { VaultIcons } from "@bitwarden/vault";
@@ -305,6 +308,14 @@ const routes: Routes = [
},
],
},
{
path: "set-initial-password",
canActivate: [canAccessFeature(FeatureFlag.PM16117_SetInitialPasswordRefactor), authGuard],
component: SetInitialPasswordComponent,
data: {
maxWidth: "lg",
} satisfies AnonLayoutWrapperData,
},
{
path: "set-password-jit",
component: SetPasswordJitComponent,

View File

@@ -6065,6 +6065,9 @@
"add": {
"message": "Add"
},
"masterPasswordSuccessfullySet": {
"message": "Master password successfully set"
},
"updatedMasterPassword": {
"message": "Master password saved"
},