mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 22:03:36 +00:00
* refactor(active-user-state-refactor): [PM-12040] Remove ActiveUserState from SSO Service - First pass of work to update the state. In the middle of testing. * fix(active-user-state-refactor): [PM-12040] Remove ActiveUserState from SSO Service - Fix for jslib-services.module.ts * fix(active-user-state-refactor): [PM-12040] Remove ActiveUserState from SSO Service - Fix main.background.ts * test(active-user-state-refactor): [PM-12040] Remove ActiveUserState from SSO Service - Added simple tests * fix(active-user-state-refactor): [PM-12040] Remove ActiveUserState from SSO Service - Tiny touchups. * fix(active-user-state-refactor): [PM-12040] Remove ActiveUserState from SSO Service - Few fixes to resolve comments. * fix(active-user-state-refactor): [PM-12040] Remove ActiveUserState from SSO Service - Changed place where userId is loaded. * test(active-user-state-refactor): [PM-12040] Remove ActiveUserState from SSO Service - Fixed test.
294 lines
12 KiB
TypeScript
294 lines
12 KiB
TypeScript
// FIXME: Update this file to be type safe and remove this and next line
|
|
// @ts-strict-ignore
|
|
import { Directive, OnInit } from "@angular/core";
|
|
import { ActivatedRoute, Router } from "@angular/router";
|
|
import { firstValueFrom, of } from "rxjs";
|
|
import { filter, first, switchMap, tap } from "rxjs/operators";
|
|
|
|
import {
|
|
OrganizationUserApiService,
|
|
OrganizationUserResetPasswordEnrollmentRequest,
|
|
} from "@bitwarden/admin-console/common";
|
|
import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
|
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
|
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
|
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
|
import { OrganizationAutoEnrollStatusResponse } from "@bitwarden/common/admin-console/models/response/organization-auto-enroll-status.response";
|
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
|
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
|
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.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 { KeysRequest } from "@bitwarden/common/models/request/keys.request";
|
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
|
import { HashPurpose } from "@bitwarden/common/platform/enums";
|
|
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 { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
|
import { DialogService, ToastService } from "@bitwarden/components";
|
|
import { DEFAULT_KDF_CONFIG, KdfConfigService, KeyService } from "@bitwarden/key-management";
|
|
|
|
import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component";
|
|
|
|
@Directive()
|
|
export class SetPasswordComponent extends BaseChangePasswordComponent implements OnInit {
|
|
syncLoading = true;
|
|
showPassword = false;
|
|
hint = "";
|
|
orgSsoIdentifier: string = null;
|
|
orgId: string;
|
|
resetPasswordAutoEnroll = false;
|
|
onSuccessfulChangePassword: () => Promise<void>;
|
|
successRoute = "vault";
|
|
activeUserId: UserId;
|
|
|
|
forceSetPasswordReason: ForceSetPasswordReason = ForceSetPasswordReason.None;
|
|
ForceSetPasswordReason = ForceSetPasswordReason;
|
|
|
|
constructor(
|
|
accountService: AccountService,
|
|
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
|
i18nService: I18nService,
|
|
keyService: KeyService,
|
|
messagingService: MessagingService,
|
|
platformUtilsService: PlatformUtilsService,
|
|
private policyApiService: PolicyApiServiceAbstraction,
|
|
policyService: PolicyService,
|
|
protected router: Router,
|
|
private apiService: ApiService,
|
|
private syncService: SyncService,
|
|
private route: ActivatedRoute,
|
|
private organizationApiService: OrganizationApiServiceAbstraction,
|
|
private organizationUserApiService: OrganizationUserApiService,
|
|
private userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
|
|
private ssoLoginService: SsoLoginServiceAbstraction,
|
|
dialogService: DialogService,
|
|
kdfConfigService: KdfConfigService,
|
|
private encryptService: EncryptService,
|
|
protected toastService: ToastService,
|
|
) {
|
|
super(
|
|
i18nService,
|
|
keyService,
|
|
messagingService,
|
|
platformUtilsService,
|
|
policyService,
|
|
dialogService,
|
|
kdfConfigService,
|
|
masterPasswordService,
|
|
accountService,
|
|
toastService,
|
|
);
|
|
}
|
|
|
|
async ngOnInit() {
|
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
super.ngOnInit();
|
|
|
|
await this.syncService.fullSync(true);
|
|
this.syncLoading = false;
|
|
|
|
this.activeUserId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
|
|
|
this.forceSetPasswordReason = await firstValueFrom(
|
|
this.masterPasswordService.forceSetPasswordReason$(this.activeUserId),
|
|
);
|
|
|
|
this.route.queryParams
|
|
.pipe(
|
|
first(),
|
|
switchMap((qParams) => {
|
|
if (qParams.identifier != null) {
|
|
return of(qParams.identifier);
|
|
} else {
|
|
// Try to get orgSsoId from state as fallback
|
|
// Note: this is primarily for the TDE user w/out MP obtains admin MP reset permission scenario.
|
|
return this.ssoLoginService.getActiveUserOrganizationSsoIdentifier(this.activeUserId);
|
|
}
|
|
}),
|
|
filter((orgSsoId) => orgSsoId != null),
|
|
tap((orgSsoId: string) => {
|
|
this.orgSsoIdentifier = orgSsoId;
|
|
}),
|
|
switchMap((orgSsoId: string) => this.organizationApiService.getAutoEnrollStatus(orgSsoId)),
|
|
tap((orgAutoEnrollStatusResponse: OrganizationAutoEnrollStatusResponse) => {
|
|
this.orgId = orgAutoEnrollStatusResponse.id;
|
|
this.resetPasswordAutoEnroll = orgAutoEnrollStatusResponse.resetPasswordEnabled;
|
|
}),
|
|
switchMap((orgAutoEnrollStatusResponse: OrganizationAutoEnrollStatusResponse) =>
|
|
// Must get org id from response to get master password policy options
|
|
this.policyApiService.getMasterPasswordPolicyOptsForOrgUser(
|
|
orgAutoEnrollStatusResponse.id,
|
|
),
|
|
),
|
|
tap((masterPasswordPolicyOptions: MasterPasswordPolicyOptions) => {
|
|
this.enforcedPolicyOptions = masterPasswordPolicyOptions;
|
|
}),
|
|
)
|
|
.subscribe({
|
|
error: () => {
|
|
this.toastService.showToast({
|
|
variant: "error",
|
|
title: null,
|
|
message: this.i18nService.t("errorOccurred"),
|
|
});
|
|
},
|
|
});
|
|
}
|
|
|
|
async setupSubmitActions() {
|
|
this.kdfConfig = DEFAULT_KDF_CONFIG;
|
|
return true;
|
|
}
|
|
|
|
async performSubmitActions(
|
|
masterPasswordHash: string,
|
|
masterKey: MasterKey,
|
|
userKey: [UserKey, EncString],
|
|
) {
|
|
let keysRequest: KeysRequest | null = null;
|
|
let newKeyPair: [string, EncString] | null = null;
|
|
|
|
if (
|
|
this.forceSetPasswordReason !=
|
|
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission
|
|
) {
|
|
// Existing JIT provisioned user in a MP encryption org setting first password
|
|
// Users in this state will not already have a user asymmetric key pair so must create it for them
|
|
// We don't want to re-create the user key pair if the user already has one (TDE user case)
|
|
|
|
// in case we 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$(this.activeUserId),
|
|
)) as Uint8Array;
|
|
const existingUserPublicKey = await firstValueFrom(
|
|
this.keyService.userPublicKey$(this.activeUserId),
|
|
);
|
|
if (existingUserPrivateKey != null && existingUserPublicKey != null) {
|
|
const existingUserPublicKeyB64 = Utils.fromBufferToB64(existingUserPublicKey);
|
|
newKeyPair = [
|
|
existingUserPublicKeyB64,
|
|
await this.encryptService.encrypt(existingUserPrivateKey, userKey[0]),
|
|
];
|
|
} else {
|
|
newKeyPair = await this.keyService.makeKeyPair(userKey[0]);
|
|
}
|
|
keysRequest = new KeysRequest(newKeyPair[0], newKeyPair[1].encryptedString);
|
|
}
|
|
|
|
const request = new SetPasswordRequest(
|
|
masterPasswordHash,
|
|
userKey[1].encryptedString,
|
|
this.hint,
|
|
this.orgSsoIdentifier,
|
|
keysRequest,
|
|
this.kdfConfig.kdfType, //always PBKDF2 --> see this.setupSubmitActions
|
|
this.kdfConfig.iterations,
|
|
);
|
|
try {
|
|
if (this.resetPasswordAutoEnroll) {
|
|
this.formPromise = this.apiService
|
|
.setPassword(request)
|
|
.then(async () => {
|
|
await this.onSetPasswordSuccess(masterKey, userKey, newKeyPair);
|
|
return this.organizationApiService.getKeys(this.orgId);
|
|
})
|
|
.then(async (response) => {
|
|
if (response == null) {
|
|
throw new Error(this.i18nService.t("resetPasswordOrgKeysError"));
|
|
}
|
|
const publicKey = Utils.fromB64ToArray(response.publicKey);
|
|
|
|
// RSA Encrypt user key with organization public key
|
|
const userKey = await this.keyService.getUserKey();
|
|
const encryptedUserKey = await this.encryptService.rsaEncrypt(userKey.key, publicKey);
|
|
|
|
const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest();
|
|
resetRequest.masterPasswordHash = masterPasswordHash;
|
|
resetRequest.resetPasswordKey = encryptedUserKey.encryptedString;
|
|
|
|
return this.organizationUserApiService.putOrganizationUserResetPasswordEnrollment(
|
|
this.orgId,
|
|
this.activeUserId,
|
|
resetRequest,
|
|
);
|
|
});
|
|
} else {
|
|
this.formPromise = this.apiService.setPassword(request).then(async () => {
|
|
await this.onSetPasswordSuccess(masterKey, userKey, newKeyPair);
|
|
});
|
|
}
|
|
|
|
await this.formPromise;
|
|
|
|
if (this.onSuccessfulChangePassword != null) {
|
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
this.onSuccessfulChangePassword();
|
|
} else {
|
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
this.router.navigate([this.successRoute]);
|
|
}
|
|
} catch {
|
|
this.toastService.showToast({
|
|
variant: "error",
|
|
title: null,
|
|
message: this.i18nService.t("errorOccurred"),
|
|
});
|
|
}
|
|
}
|
|
|
|
togglePassword(confirmField: boolean) {
|
|
this.showPassword = !this.showPassword;
|
|
document.getElementById(confirmField ? "masterPasswordRetype" : "masterPassword").focus();
|
|
}
|
|
|
|
protected async onSetPasswordSuccess(
|
|
masterKey: MasterKey,
|
|
userKey: [UserKey, EncString],
|
|
keyPair: [string, EncString] | null,
|
|
) {
|
|
// Clear force set password reason to allow navigation back to vault.
|
|
await this.masterPasswordService.setForceSetPasswordReason(
|
|
ForceSetPasswordReason.None,
|
|
this.activeUserId,
|
|
);
|
|
|
|
// User now has a password so update account decryption options in state
|
|
const userDecryptionOpts = await firstValueFrom(
|
|
this.userDecryptionOptionsService.userDecryptionOptions$,
|
|
);
|
|
userDecryptionOpts.hasMasterPassword = true;
|
|
await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts);
|
|
await this.kdfConfigService.setKdfConfig(this.activeUserId, this.kdfConfig);
|
|
await this.masterPasswordService.setMasterKey(masterKey, this.activeUserId);
|
|
await this.keyService.setUserKey(userKey[0], this.activeUserId);
|
|
|
|
// Set private key only for new JIT provisioned users in MP encryption orgs
|
|
// Existing TDE users will have private key set on sync or on login
|
|
if (
|
|
keyPair !== null &&
|
|
this.forceSetPasswordReason !=
|
|
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission
|
|
) {
|
|
await this.keyService.setPrivateKey(keyPair[1].encryptedString, this.activeUserId);
|
|
}
|
|
|
|
const localMasterKeyHash = await this.keyService.hashMasterKey(
|
|
this.masterPassword,
|
|
masterKey,
|
|
HashPurpose.LocalAuthorization,
|
|
);
|
|
await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, this.activeUserId);
|
|
}
|
|
}
|