-
{{ "ssoCompleteRegistration" | i18n }}
+
+ {{ "orgPermissionsUpdatedMustSetPassword" | i18n }}
+
+
+
+ {{ "orgRequiresYouToSetPassword" | i18n }}
+
+
Promise;
successRoute = "vault";
+ forceSetPasswordReason: ForceSetPasswordReason = ForceSetPasswordReason.None;
+ ForceSetPasswordReason = ForceSetPasswordReason;
+
constructor(
i18nService: I18nService,
cryptoService: CryptoService,
@@ -67,30 +74,49 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
}
async ngOnInit() {
+ super.ngOnInit();
+
await this.syncService.fullSync(true);
this.syncLoading = false;
- // eslint-disable-next-line rxjs/no-async-subscribe
- this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
- if (qParams.identifier != null) {
- this.identifier = qParams.identifier;
- }
- });
+ this.forceSetPasswordReason = await this.stateService.getForceSetPasswordReason();
- // Automatic Enrollment Detection
- if (this.identifier != null) {
- try {
- const response = await this.organizationApiService.getAutoEnrollStatus(this.identifier);
- this.orgId = response.id;
- this.resetPasswordAutoEnroll = response.resetPasswordEnabled;
- this.enforcedPolicyOptions =
- await this.policyApiService.getMasterPasswordPoliciesForInvitedUsers(this.orgId);
- } catch {
- this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred"));
- }
- }
-
- super.ngOnInit();
+ 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.stateService.getUserSsoOrganizationIdentifier();
+ }
+ }),
+ 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.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred"));
+ },
+ });
}
async setupSubmitActions() {
@@ -104,13 +130,26 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
masterKey: MasterKey,
userKey: [UserKey, EncString]
) {
- const newKeyPair = await this.cryptoService.makeKeyPair(userKey[0]);
+ 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)
+ newKeyPair = await this.cryptoService.makeKeyPair(userKey[0]);
+ keysRequest = new KeysRequest(newKeyPair[0], newKeyPair[1].encryptedString);
+ }
+
const request = new SetPasswordRequest(
masterPasswordHash,
userKey[1].encryptedString,
this.hint,
- this.identifier,
- new KeysRequest(newKeyPair[0], newKeyPair[1].encryptedString),
+ this.orgSsoIdentifier,
+ keysRequest,
this.kdf,
this.kdfConfig.iterations,
this.kdfConfig.memory,
@@ -171,13 +210,32 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
protected async onSetPasswordSuccess(
masterKey: MasterKey,
userKey: [UserKey, EncString],
- keyPair: [string, EncString]
+ keyPair: [string, EncString] | null
) {
+ // Clear force set password reason to allow navigation back to vault.
+ await this.stateService.setForceSetPasswordReason(ForceSetPasswordReason.None);
+
+ // User now has a password so update account decryption options in state
+ const acctDecryptionOpts: AccountDecryptionOptions =
+ await this.stateService.getAccountDecryptionOptions();
+
+ acctDecryptionOpts.hasMasterPassword = true;
+ await this.stateService.setAccountDecryptionOptions(acctDecryptionOpts);
+
await this.stateService.setKdfType(this.kdf);
await this.stateService.setKdfConfig(this.kdfConfig);
await this.cryptoService.setMasterKey(masterKey);
await this.cryptoService.setUserKey(userKey[0]);
- await this.cryptoService.setPrivateKey(keyPair[1].encryptedString);
+
+ // 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.cryptoService.setPrivateKey(keyPair[1].encryptedString);
+ }
const localMasterKeyHash = await this.cryptoService.hashMasterKey(
this.masterPassword,
diff --git a/libs/angular/src/auth/components/sso.component.spec.ts b/libs/angular/src/auth/components/sso.component.spec.ts
index 6d81b3d61e..3667e62905 100644
--- a/libs/angular/src/auth/components/sso.component.spec.ts
+++ b/libs/angular/src/auth/components/sso.component.spec.ts
@@ -8,7 +8,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
-import { ForceResetPasswordReason } from "@bitwarden/common/auth/models/domain/force-reset-password-reason";
+import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { KeyConnectorUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/key-connector-user-decryption-option";
import { TrustedDeviceUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
@@ -345,16 +345,32 @@ describe("SsoComponent", () => {
mockAuthService.logIn.mockResolvedValue(authResult);
});
- testChangePasswordOnSuccessfulLogin();
- testOnSuccessfulLoginChangePasswordNavigate();
+ it("navigates to the component's defined trustedDeviceEncRoute route and sets correct flag when onSuccessfulLoginTdeNavigate is undefined ", async () => {
+ await _component.logIn(code, codeVerifier, orgIdFromState);
+ expect(mockAuthService.logIn).toHaveBeenCalledTimes(1);
+
+ expect(mockStateService.setForceSetPasswordReason).toHaveBeenCalledWith(
+ ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission
+ );
+
+ expect(mockOnSuccessfulLoginTdeNavigate).not.toHaveBeenCalled();
+
+ expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
+ expect(mockRouter.navigate).toHaveBeenCalledWith(
+ [_component.trustedDeviceEncRoute],
+ undefined
+ );
+
+ expect(mockLogService.error).not.toHaveBeenCalled();
+ });
});
describe("Given Trusted Device Encryption is enabled, user doesn't need to set a MP, and forcePasswordReset is required", () => {
[
- ForceResetPasswordReason.AdminForcePasswordReset,
+ ForceSetPasswordReason.AdminForcePasswordReset,
// ForceResetPasswordReason.WeakMasterPassword, -- not possible in SSO flow as set client side
].forEach((forceResetPasswordReason) => {
- const reasonString = ForceResetPasswordReason[forceResetPasswordReason];
+ const reasonString = ForceSetPasswordReason[forceResetPasswordReason];
let authResult;
beforeEach(() => {
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
@@ -362,7 +378,7 @@ describe("SsoComponent", () => {
);
authResult = new AuthResult();
- authResult.forcePasswordReset = ForceResetPasswordReason.AdminForcePasswordReset;
+ authResult.forcePasswordReset = ForceSetPasswordReason.AdminForcePasswordReset;
mockAuthService.logIn.mockResolvedValue(authResult);
});
@@ -379,7 +395,7 @@ describe("SsoComponent", () => {
);
authResult = new AuthResult();
- authResult.forcePasswordReset = ForceResetPasswordReason.None;
+ authResult.forcePasswordReset = ForceSetPasswordReason.None;
mockAuthService.logIn.mockResolvedValue(authResult);
});
@@ -448,10 +464,10 @@ describe("SsoComponent", () => {
describe("Force Master Password Reset scenarios", () => {
[
- ForceResetPasswordReason.AdminForcePasswordReset,
+ ForceSetPasswordReason.AdminForcePasswordReset,
// ForceResetPasswordReason.WeakMasterPassword, -- not possible in SSO flow as set client side
].forEach((forceResetPasswordReason) => {
- const reasonString = ForceResetPasswordReason[forceResetPasswordReason];
+ const reasonString = ForceSetPasswordReason[forceResetPasswordReason];
beforeEach(() => {
// use standard user with MP because this test is not concerned with password reset.
@@ -477,7 +493,7 @@ describe("SsoComponent", () => {
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
mockAcctDecryptionOpts.withMasterPassword
);
- authResult.forcePasswordReset = ForceResetPasswordReason.None;
+ authResult.forcePasswordReset = ForceSetPasswordReason.None;
mockAuthService.logIn.mockResolvedValue(authResult);
});
diff --git a/libs/angular/src/auth/components/sso.component.ts b/libs/angular/src/auth/components/sso.component.ts
index 8909c1c1c4..fe02304a4e 100644
--- a/libs/angular/src/auth/components/sso.component.ts
+++ b/libs/angular/src/auth/components/sso.component.ts
@@ -5,7 +5,7 @@ import { first } from "rxjs/operators";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
-import { ForceResetPasswordReason } from "@bitwarden/common/auth/models/domain/force-reset-password-reason";
+import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { SsoLoginCredentials } from "@bitwarden/common/auth/models/domain/login-credentials";
import { TrustedDeviceUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
import { SsoPreValidateResponse } from "@bitwarden/common/auth/models/response/sso-pre-validate.response";
@@ -201,12 +201,19 @@ export class SsoComponent {
// Everything after the 2FA check is considered a successful login
// Just have to figure out where to send the user
- // Save off the OrgSsoIdentifier for use in the TDE flows
+ // Save off the OrgSsoIdentifier for use in the TDE flows (or elsewhere)
// - TDE login decryption options component
// - Browser SSO on extension open
// Note: you cannot set this in state before 2FA b/c there won't be an account in state.
await this.stateService.setUserSsoOrganizationIdentifier(orgSsoIdentifier);
+ // Users enrolled in admin acct recovery can be forced to set a new password after
+ // having the admin set a temp password for them (affects TDE & standard users)
+ if (authResult.forcePasswordReset == ForceSetPasswordReason.AdminForcePasswordReset) {
+ // Weak password is not a valid scenario here b/c we cannot have evaluated a MP yet
+ return await this.handleForcePasswordReset(orgSsoIdentifier);
+ }
+
const tdeEnabled = await this.isTrustedDeviceEncEnabled(
acctDecryptionOpts.trustedDeviceOption
);
@@ -231,12 +238,6 @@ export class SsoComponent {
return await this.handleChangePasswordRequired(orgSsoIdentifier);
}
- // Users enrolled in admin acct recovery can be forced to set a new password after
- // having the admin set a temp password for them
- if (authResult.forcePasswordReset == ForceResetPasswordReason.AdminForcePasswordReset) {
- return await this.handleForcePasswordReset(orgSsoIdentifier);
- }
-
// Standard SSO login success case
return await this.handleSuccessfulLogin();
} catch (e) {
@@ -277,12 +278,12 @@ export class SsoComponent {
!acctDecryptionOpts.hasMasterPassword &&
acctDecryptionOpts.trustedDeviceOption.hasManageResetPasswordPermission
) {
- // Change implies going no password -> password in this case
- return await this.handleChangePasswordRequired(orgIdentifier);
- }
-
- if (authResult.forcePasswordReset !== ForceResetPasswordReason.None) {
- return await this.handleForcePasswordReset(orgIdentifier);
+ // Set flag so that auth guard can redirect to set password screen after decryption (trusted or untrusted device)
+ // Note: we cannot directly navigate in this scenario as we are in a pre-decryption state, and
+ // if you try to set a new MP before decrypting, you will invalidate the user's data by making a new user key.
+ await this.stateService.setForceSetPasswordReason(
+ ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission
+ );
}
if (this.onSuccessfulLoginTde != null) {
diff --git a/libs/angular/src/auth/components/two-factor.component.spec.ts b/libs/angular/src/auth/components/two-factor.component.spec.ts
index 9e147f3357..49bc25d2b6 100644
--- a/libs/angular/src/auth/components/two-factor.component.spec.ts
+++ b/libs/angular/src/auth/components/two-factor.component.spec.ts
@@ -10,7 +10,7 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
-import { ForceResetPasswordReason } from "@bitwarden/common/auth/models/domain/force-reset-password-reason";
+import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { KeyConnectorUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/key-connector-user-decryption-option";
import { TrustedDeviceUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
@@ -310,10 +310,10 @@ describe("TwoFactorComponent", () => {
describe("Force Master Password Reset scenarios", () => {
[
- ForceResetPasswordReason.AdminForcePasswordReset,
- ForceResetPasswordReason.WeakMasterPassword,
+ ForceSetPasswordReason.AdminForcePasswordReset,
+ ForceSetPasswordReason.WeakMasterPassword,
].forEach((forceResetPasswordReason) => {
- const reasonString = ForceResetPasswordReason[forceResetPasswordReason];
+ const reasonString = ForceSetPasswordReason[forceResetPasswordReason];
beforeEach(() => {
// use standard user with MP because this test is not concerned with password reset.
@@ -389,15 +389,30 @@ describe("TwoFactorComponent", () => {
mockAuthService.logInTwoFactor.mockResolvedValue(authResult);
});
- testChangePasswordOnSuccessfulLogin();
+ it("navigates to the component's defined trusted device encryption route and sets correct flag when user doesn't have a MP and key connector isn't enabled", async () => {
+ // Act
+ await component.doSubmit();
+
+ // Assert
+
+ expect(mockStateService.setForceSetPasswordReason).toHaveBeenCalledWith(
+ ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission
+ );
+
+ expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
+ expect(mockRouter.navigate).toHaveBeenCalledWith(
+ [_component.trustedDeviceEncRoute],
+ undefined
+ );
+ });
});
describe("Given Trusted Device Encryption is enabled, user doesn't need to set a MP, and forcePasswordReset is required", () => {
[
- ForceResetPasswordReason.AdminForcePasswordReset,
- ForceResetPasswordReason.WeakMasterPassword,
+ ForceSetPasswordReason.AdminForcePasswordReset,
+ ForceSetPasswordReason.WeakMasterPassword,
].forEach((forceResetPasswordReason) => {
- const reasonString = ForceResetPasswordReason[forceResetPasswordReason];
+ const reasonString = ForceSetPasswordReason[forceResetPasswordReason];
beforeEach(() => {
// use standard user with MP because this test is not concerned with password reset.
@@ -422,7 +437,7 @@ describe("TwoFactorComponent", () => {
);
authResult = new AuthResult();
- authResult.forcePasswordReset = ForceResetPasswordReason.None;
+ authResult.forcePasswordReset = ForceSetPasswordReason.None;
mockAuthService.logInTwoFactor.mockResolvedValue(authResult);
});
diff --git a/libs/angular/src/auth/components/two-factor.component.ts b/libs/angular/src/auth/components/two-factor.component.ts
index 756218a1e1..303c0adc16 100644
--- a/libs/angular/src/auth/components/two-factor.component.ts
+++ b/libs/angular/src/auth/components/two-factor.component.ts
@@ -11,7 +11,7 @@ import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
-import { ForceResetPasswordReason } from "@bitwarden/common/auth/models/domain/force-reset-password-reason";
+import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { TrustedDeviceUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
import { TwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/two-factor-email.request";
@@ -239,9 +239,13 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
// - TDE login decryption options component
// - Browser SSO on extension open
await this.stateService.setUserSsoOrganizationIdentifier(this.orgIdentifier);
-
this.loginService.clearValues();
+ // note: this flow affects both TDE & standard users
+ if (this.isForcePasswordResetRequired(authResult)) {
+ return await this.handleForcePasswordReset(this.orgIdentifier);
+ }
+
const acctDecryptionOpts: AccountDecryptionOptions =
await this.stateService.getAccountDecryptionOptions();
@@ -264,12 +268,6 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
return await this.handleChangePasswordRequired(this.orgIdentifier);
}
- // Users can be forced to reset their password via an admin or org policy
- // disallowing weak passwords
- if (authResult.forcePasswordReset !== ForceResetPasswordReason.None) {
- return await this.handleForcePasswordReset(this.orgIdentifier);
- }
-
return await this.handleSuccessfulLogin();
}
@@ -298,16 +296,12 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
!acctDecryptionOpts.hasMasterPassword &&
acctDecryptionOpts.trustedDeviceOption.hasManageResetPasswordPermission
) {
- // Change implies going no password -> password in this case
- return await this.handleChangePasswordRequired(orgIdentifier);
- }
-
- // Users can be forced to reset their password via an admin or org policy disallowing weak passwords
- // Note: this is different from SSO component login flow as a user can
- // login with MP and then have to pass 2FA to finish login and we can actually
- // evaluate if they have a weak password at this time.
- if (authResult.forcePasswordReset !== ForceResetPasswordReason.None) {
- return await this.handleForcePasswordReset(orgIdentifier);
+ // Set flag so that auth guard can redirect to set password screen after decryption (trusted or untrusted device)
+ // Note: we cannot directly navigate to the set password screen in this scenario as we are in a pre-decryption state, and
+ // if you try to set a new MP before decrypting, you will invalidate the user's data by making a new user key.
+ await this.stateService.setForceSetPasswordReason(
+ ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission
+ );
}
if (this.onSuccessfulLoginTde != null) {
@@ -332,6 +326,25 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
});
}
+ /**
+ * Determines if a user needs to reset their password based on certain conditions.
+ * Users can be forced to reset their password via an admin or org policy disallowing weak passwords.
+ * Note: this is different from the SSO component login flow as a user can
+ * login with MP and then have to pass 2FA to finish login and we can actually
+ * evaluate if they have a weak password at that time.
+ *
+ * @param {AuthResult} authResult - The authentication result.
+ * @returns {boolean} Returns true if a password reset is required, false otherwise.
+ */
+ private isForcePasswordResetRequired(authResult: AuthResult): boolean {
+ const forceResetReasons = [
+ ForceSetPasswordReason.AdminForcePasswordReset,
+ ForceSetPasswordReason.WeakMasterPassword,
+ ];
+
+ return forceResetReasons.includes(authResult.forcePasswordReset);
+ }
+
private async handleForcePasswordReset(orgIdentifier: string) {
this.router.navigate([this.forcePasswordResetRoute], {
queryParams: {
diff --git a/libs/angular/src/auth/components/update-temp-password.component.ts b/libs/angular/src/auth/components/update-temp-password.component.ts
index 7bb65ab68d..4c27bfa4f0 100644
--- a/libs/angular/src/auth/components/update-temp-password.component.ts
+++ b/libs/angular/src/auth/components/update-temp-password.component.ts
@@ -6,7 +6,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
-import { ForceResetPasswordReason } from "@bitwarden/common/auth/models/domain/force-reset-password-reason";
+import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
import { UpdateTempPasswordRequest } from "@bitwarden/common/auth/models/request/update-temp-password.request";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
@@ -30,7 +30,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent {
key: string;
enforcedPolicyOptions: MasterPasswordPolicyOptions;
showPassword = false;
- reason: ForceResetPasswordReason = ForceResetPasswordReason.None;
+ reason: ForceSetPasswordReason = ForceSetPasswordReason.None;
verification: Verification = {
type: VerificationType.MasterPassword,
secret: "",
@@ -39,7 +39,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent {
onSuccessfulChangePassword: () => Promise;
get requireCurrentPassword(): boolean {
- return this.reason === ForceResetPasswordReason.WeakMasterPassword;
+ return this.reason === ForceSetPasswordReason.WeakMasterPassword;
}
constructor(
@@ -72,10 +72,10 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent {
async ngOnInit() {
await this.syncService.fullSync(true);
- this.reason = await this.stateService.getForcePasswordResetReason();
+ this.reason = await this.stateService.getForceSetPasswordReason();
// If we somehow end up here without a reason, go back to the home page
- if (this.reason == ForceResetPasswordReason.None) {
+ if (this.reason == ForceSetPasswordReason.None) {
this.router.navigate(["/"]);
return;
}
@@ -84,7 +84,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent {
}
get masterPasswordWarningText(): string {
- return this.reason == ForceResetPasswordReason.WeakMasterPassword
+ return this.reason == ForceSetPasswordReason.WeakMasterPassword
? this.i18nService.t("updateWeakMasterPasswordWarning")
: this.i18nService.t("updateMasterPasswordWarning");
}
@@ -146,10 +146,10 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent {
) {
try {
switch (this.reason) {
- case ForceResetPasswordReason.AdminForcePasswordReset:
+ case ForceSetPasswordReason.AdminForcePasswordReset:
this.formPromise = this.updateTempPassword(masterPasswordHash, userKey);
break;
- case ForceResetPasswordReason.WeakMasterPassword:
+ case ForceSetPasswordReason.WeakMasterPassword:
this.formPromise = this.updatePassword(masterPasswordHash, userKey);
break;
}
@@ -161,7 +161,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent {
this.i18nService.t("updatedMasterPassword")
);
- await this.stateService.setForcePasswordResetReason(ForceResetPasswordReason.None);
+ await this.stateService.setForceSetPasswordReason(ForceSetPasswordReason.None);
if (this.onSuccessfulChangePassword != null) {
this.onSuccessfulChangePassword();
diff --git a/libs/angular/src/auth/guards/auth.guard.ts b/libs/angular/src/auth/guards/auth.guard.ts
index b8535da937..705ec02335 100644
--- a/libs/angular/src/auth/guards/auth.guard.ts
+++ b/libs/angular/src/auth/guards/auth.guard.ts
@@ -4,7 +4,7 @@ import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
-import { ForceResetPasswordReason } from "@bitwarden/common/auth/models/domain/force-reset-password-reason";
+import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
@@ -40,9 +40,19 @@ export class AuthGuard implements CanActivate {
return this.router.createUrlTree(["/remove-password"]);
}
+ const forceSetPasswordReason = await this.stateService.getForceSetPasswordReason();
+
if (
- !routerState.url.includes("update-temp-password") &&
- (await this.stateService.getForcePasswordResetReason()) != ForceResetPasswordReason.None
+ forceSetPasswordReason ===
+ ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission &&
+ !routerState.url.includes("set-password")
+ ) {
+ return this.router.createUrlTree(["/set-password"]);
+ }
+
+ if (
+ forceSetPasswordReason !== ForceSetPasswordReason.None &&
+ !routerState.url.includes("update-temp-password")
) {
return this.router.createUrlTree(["/update-temp-password"]);
}
diff --git a/libs/common/src/admin-console/abstractions/policy/policy-api.service.abstraction.ts b/libs/common/src/admin-console/abstractions/policy/policy-api.service.abstraction.ts
index a941569563..ef042b0d5a 100644
--- a/libs/common/src/admin-console/abstractions/policy/policy-api.service.abstraction.ts
+++ b/libs/common/src/admin-console/abstractions/policy/policy-api.service.abstraction.ts
@@ -14,10 +14,7 @@ export class PolicyApiServiceAbstraction {
email: string,
organizationUserId: string
) => Promise>;
- getPoliciesByInvitedUser: (
- organizationId: string,
- userId: string
- ) => Promise>;
- getMasterPasswordPoliciesForInvitedUsers: (orgId: string) => Promise;
+
+ getMasterPasswordPolicyOptsForOrgUser: (orgId: string) => Promise;
putPolicy: (organizationId: string, type: PolicyType, request: PolicyRequest) => Promise;
}
diff --git a/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts b/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts
index e43b124dcd..0683bbd585 100644
--- a/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts
+++ b/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts
@@ -30,6 +30,7 @@ export abstract class PolicyService {
policies: Policy[],
orgId: string
) => [ResetPasswordPolicyOptions, boolean];
+ mapPolicyFromResponse: (policyResponse: PolicyResponse) => Policy;
mapPoliciesFromToken: (policiesResponse: ListResponse) => Policy[];
policyAppliesToUser: (
policyType: PolicyType,
diff --git a/libs/common/src/admin-console/services/policy/policy-api.service.ts b/libs/common/src/admin-console/services/policy/policy-api.service.ts
index c0a572ae8d..648bd53dcb 100644
--- a/libs/common/src/admin-console/services/policy/policy-api.service.ts
+++ b/libs/common/src/admin-console/services/policy/policy-api.service.ts
@@ -1,6 +1,8 @@
import { firstValueFrom } from "rxjs";
import { ApiService } from "../../../abstractions/api.service";
+import { HttpStatusCode } from "../../../enums";
+import { ErrorResponse } from "../../../models/response/error.response";
import { ListResponse } from "../../../models/response/list.response";
import { StateService } from "../../../platform/abstractions/state.service";
import { Utils } from "../../../platform/misc/utils";
@@ -65,27 +67,47 @@ export class PolicyApiService implements PolicyApiServiceAbstraction {
return new ListResponse(r, PolicyResponse);
}
- async getPoliciesByInvitedUser(
- organizationId: string,
- userId: string
- ): Promise> {
- const r = await this.apiService.send(
+ private async getMasterPasswordPolicyResponseForOrgUser(
+ organizationId: string
+ ): Promise {
+ const response = await this.apiService.send(
"GET",
- "/organizations/" + organizationId + "/policies/invited-user?" + "userId=" + userId,
+ "/organizations/" + organizationId + "/policies/master-password",
null,
- false,
+ true,
true
);
- return new ListResponse(r, PolicyResponse);
+
+ return new PolicyResponse(response);
}
- async getMasterPasswordPoliciesForInvitedUsers(
+ async getMasterPasswordPolicyOptsForOrgUser(
orgId: string
- ): Promise {
- const userId = await this.stateService.getUserId();
- const response = await this.getPoliciesByInvitedUser(orgId, userId);
- const policies = await this.policyService.mapPoliciesFromToken(response);
- return await firstValueFrom(this.policyService.masterPasswordPolicyOptions$(policies));
+ ): Promise {
+ try {
+ const masterPasswordPolicyResponse = await this.getMasterPasswordPolicyResponseForOrgUser(
+ orgId
+ );
+
+ const masterPasswordPolicy = this.policyService.mapPolicyFromResponse(
+ masterPasswordPolicyResponse
+ );
+
+ if (!masterPasswordPolicy) {
+ return null;
+ }
+
+ return await firstValueFrom(
+ this.policyService.masterPasswordPolicyOptions$([masterPasswordPolicy])
+ );
+ } catch (error) {
+ // If policy not found, return null
+ if (error instanceof ErrorResponse && error.statusCode === HttpStatusCode.NotFound) {
+ return null;
+ }
+ // otherwise rethrow error
+ throw error;
+ }
}
async putPolicy(organizationId: string, type: PolicyType, request: PolicyRequest): Promise {
diff --git a/libs/common/src/admin-console/services/policy/policy.service.ts b/libs/common/src/admin-console/services/policy/policy.service.ts
index 4ef3356c18..d57a7fef3c 100644
--- a/libs/common/src/admin-console/services/policy/policy.service.ts
+++ b/libs/common/src/admin-console/services/policy/policy.service.ts
@@ -218,13 +218,17 @@ export class PolicyService implements InternalPolicyServiceAbstraction {
return [resetPasswordPolicyOptions, policy?.enabled ?? false];
}
+ mapPolicyFromResponse(policyResponse: PolicyResponse): Policy {
+ const policyData = new PolicyData(policyResponse);
+ return new Policy(policyData);
+ }
+
mapPoliciesFromToken(policiesResponse: ListResponse): Policy[] {
- if (policiesResponse == null || policiesResponse.data == null) {
+ if (policiesResponse?.data == null) {
return null;
}
- const policiesData = policiesResponse.data.map((p) => new PolicyData(p));
- return policiesData.map((p) => new Policy(p));
+ return policiesResponse.data.map((response) => this.mapPolicyFromResponse(response));
}
async policyAppliesToUser(
diff --git a/libs/common/src/auth/login-strategies/login.strategy.spec.ts b/libs/common/src/auth/login-strategies/login.strategy.spec.ts
index d691f934d4..4e7e7b216a 100644
--- a/libs/common/src/auth/login-strategies/login.strategy.spec.ts
+++ b/libs/common/src/auth/login-strategies/login.strategy.spec.ts
@@ -33,7 +33,7 @@ import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service";
import { TwoFactorProviderType } from "../enums/two-factor-provider-type";
import { AuthResult } from "../models/domain/auth-result";
-import { ForceResetPasswordReason } from "../models/domain/force-reset-password-reason";
+import { ForceSetPasswordReason } from "../models/domain/force-set-password-reason";
import { PasswordLoginCredentials } from "../models/domain/login-credentials";
import { PasswordTokenRequest } from "../models/request/identity-token/password-token.request";
import { TokenTwoFactorRequest } from "../models/request/identity-token/token-two-factor.request";
@@ -229,7 +229,7 @@ describe("LoginStrategy", () => {
const result = await passwordLoginStrategy.logIn(credentials);
expect(result).toEqual({
- forcePasswordReset: ForceResetPasswordReason.AdminForcePasswordReset,
+ forcePasswordReset: ForceSetPasswordReason.AdminForcePasswordReset,
resetMasterPassword: true,
twoFactorProviders: null,
captchaSiteKey: "",
diff --git a/libs/common/src/auth/login-strategies/login.strategy.ts b/libs/common/src/auth/login-strategies/login.strategy.ts
index 09e3641f54..fb328c865c 100644
--- a/libs/common/src/auth/login-strategies/login.strategy.ts
+++ b/libs/common/src/auth/login-strategies/login.strategy.ts
@@ -18,7 +18,7 @@ import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service";
import { TwoFactorProviderType } from "../enums/two-factor-provider-type";
import { AuthResult } from "../models/domain/auth-result";
-import { ForceResetPasswordReason } from "../models/domain/force-reset-password-reason";
+import { ForceSetPasswordReason } from "../models/domain/force-set-password-reason";
import {
AuthRequestLoginCredentials,
PasswordLoginCredentials,
@@ -166,7 +166,7 @@ export abstract class LoginStrategy {
// Convert boolean to enum
if (response.forcePasswordReset) {
- result.forcePasswordReset = ForceResetPasswordReason.AdminForcePasswordReset;
+ result.forcePasswordReset = ForceSetPasswordReason.AdminForcePasswordReset;
}
// Must come before setting keys, user key needs email to update additional keys
diff --git a/libs/common/src/auth/login-strategies/password-login.strategy.spec.ts b/libs/common/src/auth/login-strategies/password-login.strategy.spec.ts
index 03f2a26b1c..a0f8091bca 100644
--- a/libs/common/src/auth/login-strategies/password-login.strategy.spec.ts
+++ b/libs/common/src/auth/login-strategies/password-login.strategy.spec.ts
@@ -24,7 +24,7 @@ import { AuthService } from "../abstractions/auth.service";
import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service";
import { TwoFactorProviderType } from "../enums/two-factor-provider-type";
-import { ForceResetPasswordReason } from "../models/domain/force-reset-password-reason";
+import { ForceSetPasswordReason } from "../models/domain/force-set-password-reason";
import { PasswordLoginCredentials } from "../models/domain/login-credentials";
import { IdentityTokenResponse } from "../models/response/identity-token.response";
import { IdentityTwoFactorResponse } from "../models/response/identity-two-factor.response";
@@ -154,7 +154,7 @@ describe("PasswordLoginStrategy", () => {
const result = await passwordLoginStrategy.logIn(credentials);
expect(policyService.evaluateMasterPassword).not.toHaveBeenCalled();
- expect(result.forcePasswordReset).toEqual(ForceResetPasswordReason.None);
+ expect(result.forcePasswordReset).toEqual(ForceSetPasswordReason.None);
});
it("does not force the user to update their master password when it meets requirements", async () => {
@@ -164,7 +164,7 @@ describe("PasswordLoginStrategy", () => {
const result = await passwordLoginStrategy.logIn(credentials);
expect(policyService.evaluateMasterPassword).toHaveBeenCalled();
- expect(result.forcePasswordReset).toEqual(ForceResetPasswordReason.None);
+ expect(result.forcePasswordReset).toEqual(ForceSetPasswordReason.None);
});
it("forces the user to update their master password on successful login when it does not meet master password policy requirements", async () => {
@@ -174,10 +174,10 @@ describe("PasswordLoginStrategy", () => {
const result = await passwordLoginStrategy.logIn(credentials);
expect(policyService.evaluateMasterPassword).toHaveBeenCalled();
- expect(stateService.setForcePasswordResetReason).toHaveBeenCalledWith(
- ForceResetPasswordReason.WeakMasterPassword
+ expect(stateService.setForceSetPasswordReason).toHaveBeenCalledWith(
+ ForceSetPasswordReason.WeakMasterPassword
);
- expect(result.forcePasswordReset).toEqual(ForceResetPasswordReason.WeakMasterPassword);
+ expect(result.forcePasswordReset).toEqual(ForceSetPasswordReason.WeakMasterPassword);
});
it("forces the user to update their master password on successful 2FA login when it does not meet master password policy requirements", async () => {
@@ -210,12 +210,12 @@ describe("PasswordLoginStrategy", () => {
);
// First login attempt should not save the force password reset options
- expect(firstResult.forcePasswordReset).toEqual(ForceResetPasswordReason.None);
+ expect(firstResult.forcePasswordReset).toEqual(ForceSetPasswordReason.None);
// Second login attempt should save the force password reset options and return in result
- expect(stateService.setForcePasswordResetReason).toHaveBeenCalledWith(
- ForceResetPasswordReason.WeakMasterPassword
+ expect(stateService.setForceSetPasswordReason).toHaveBeenCalledWith(
+ ForceSetPasswordReason.WeakMasterPassword
);
- expect(secondResult.forcePasswordReset).toEqual(ForceResetPasswordReason.WeakMasterPassword);
+ expect(secondResult.forcePasswordReset).toEqual(ForceSetPasswordReason.WeakMasterPassword);
});
});
diff --git a/libs/common/src/auth/login-strategies/password-login.strategy.ts b/libs/common/src/auth/login-strategies/password-login.strategy.ts
index 788116cf11..6475bbe5a7 100644
--- a/libs/common/src/auth/login-strategies/password-login.strategy.ts
+++ b/libs/common/src/auth/login-strategies/password-login.strategy.ts
@@ -14,7 +14,7 @@ import { AuthService } from "../abstractions/auth.service";
import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service";
import { AuthResult } from "../models/domain/auth-result";
-import { ForceResetPasswordReason } from "../models/domain/force-reset-password-reason";
+import { ForceSetPasswordReason } from "../models/domain/force-set-password-reason";
import { PasswordLoginCredentials } from "../models/domain/login-credentials";
import { PasswordTokenRequest } from "../models/request/identity-token/password-token.request";
import { TokenTwoFactorRequest } from "../models/request/identity-token/token-two-factor.request";
@@ -42,7 +42,7 @@ export class PasswordLoginStrategy extends LoginStrategy {
* Options to track if the user needs to update their password due to a password that does not meet an organization's
* master password policy.
*/
- private forcePasswordResetReason: ForceResetPasswordReason = ForceResetPasswordReason.None;
+ private forcePasswordResetReason: ForceSetPasswordReason = ForceSetPasswordReason.None;
constructor(
cryptoService: CryptoService,
@@ -82,9 +82,9 @@ export class PasswordLoginStrategy extends LoginStrategy {
if (
!result.requiresTwoFactor &&
!result.requiresCaptcha &&
- this.forcePasswordResetReason != ForceResetPasswordReason.None
+ this.forcePasswordResetReason != ForceSetPasswordReason.None
) {
- await this.stateService.setForcePasswordResetReason(this.forcePasswordResetReason);
+ await this.stateService.setForceSetPasswordReason(this.forcePasswordResetReason);
result.forcePasswordReset = this.forcePasswordResetReason;
}
@@ -128,13 +128,13 @@ export class PasswordLoginStrategy extends LoginStrategy {
if (!meetsRequirements) {
if (authResult.requiresCaptcha || authResult.requiresTwoFactor) {
// Save the flag to this strategy for later use as the master password is about to pass out of scope
- this.forcePasswordResetReason = ForceResetPasswordReason.WeakMasterPassword;
+ this.forcePasswordResetReason = ForceSetPasswordReason.WeakMasterPassword;
} else {
// Authentication was successful, save the force update password options with the state service
- await this.stateService.setForcePasswordResetReason(
- ForceResetPasswordReason.WeakMasterPassword
+ await this.stateService.setForceSetPasswordReason(
+ ForceSetPasswordReason.WeakMasterPassword
);
- authResult.forcePasswordReset = ForceResetPasswordReason.WeakMasterPassword;
+ authResult.forcePasswordReset = ForceSetPasswordReason.WeakMasterPassword;
}
}
}
diff --git a/libs/common/src/auth/login-strategies/sso-login.strategy.ts b/libs/common/src/auth/login-strategies/sso-login.strategy.ts
index 9c550b833f..f285676751 100644
--- a/libs/common/src/auth/login-strategies/sso-login.strategy.ts
+++ b/libs/common/src/auth/login-strategies/sso-login.strategy.ts
@@ -14,7 +14,7 @@ import { DeviceTrustCryptoServiceAbstraction } from "../abstractions/device-trus
import { KeyConnectorService } from "../abstractions/key-connector.service";
import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service";
-import { ForceResetPasswordReason } from "../models/domain/force-reset-password-reason";
+import { ForceSetPasswordReason } from "../models/domain/force-set-password-reason";
import { SsoLoginCredentials } from "../models/domain/login-credentials";
import { SsoTokenRequest } from "../models/request/identity-token/sso-token.request";
import { IdentityTokenResponse } from "../models/response/identity-token.response";
@@ -75,8 +75,8 @@ export class SsoLoginStrategy extends LoginStrategy {
this.ssoEmail2FaSessionToken = ssoAuthResult.ssoEmail2FaSessionToken;
// Auth guard currently handles redirects for this.
- if (ssoAuthResult.forcePasswordReset == ForceResetPasswordReason.AdminForcePasswordReset) {
- await this.stateService.setForcePasswordResetReason(ssoAuthResult.forcePasswordReset);
+ if (ssoAuthResult.forcePasswordReset == ForceSetPasswordReason.AdminForcePasswordReset) {
+ await this.stateService.setForceSetPasswordReason(ssoAuthResult.forcePasswordReset);
}
return ssoAuthResult;
diff --git a/libs/common/src/auth/models/domain/auth-result.ts b/libs/common/src/auth/models/domain/auth-result.ts
index 6900cba1c4..16f58c6459 100644
--- a/libs/common/src/auth/models/domain/auth-result.ts
+++ b/libs/common/src/auth/models/domain/auth-result.ts
@@ -1,7 +1,7 @@
import { Utils } from "../../../platform/misc/utils";
import { TwoFactorProviderType } from "../../enums/two-factor-provider-type";
-import { ForceResetPasswordReason } from "./force-reset-password-reason";
+import { ForceSetPasswordReason } from "./force-set-password-reason";
export class AuthResult {
captchaSiteKey = "";
@@ -13,7 +13,7 @@ export class AuthResult {
* */
resetMasterPassword = false;
- forcePasswordReset: ForceResetPasswordReason = ForceResetPasswordReason.None;
+ forcePasswordReset: ForceSetPasswordReason = ForceSetPasswordReason.None;
twoFactorProviders: Map = null;
ssoEmail2FaSessionToken?: string;
email: string;
diff --git a/libs/common/src/auth/models/domain/force-reset-password-reason.ts b/libs/common/src/auth/models/domain/force-set-password-reason.ts
similarity index 65%
rename from libs/common/src/auth/models/domain/force-reset-password-reason.ts
rename to libs/common/src/auth/models/domain/force-set-password-reason.ts
index 99e461c2ea..a6b407d2f1 100644
--- a/libs/common/src/auth/models/domain/force-reset-password-reason.ts
+++ b/libs/common/src/auth/models/domain/force-set-password-reason.ts
@@ -1,8 +1,8 @@
/*
- * This enum is used to determine if a user should be forced to reset their password
+ * This enum is used to determine if a user should be forced to initially set or reset their password
* on login (server flag) or unlock via MP (client evaluation).
*/
-export enum ForceResetPasswordReason {
+export enum ForceSetPasswordReason {
/**
* A password reset should not be forced.
*/
@@ -20,4 +20,10 @@ export enum ForceResetPasswordReason {
* Only set client side b/c server can't evaluate MP.
*/
WeakMasterPassword,
+
+ /**
+ * Occurs when a TDE user without a password obtains the password reset permission.
+ * Set post login & decryption client side and by server in sync (to catch logged in users).
+ */
+ TdeUserWithoutPasswordHasPasswordResetPermission,
}
diff --git a/libs/common/src/auth/models/request/set-password.request.ts b/libs/common/src/auth/models/request/set-password.request.ts
index 1fa6b4c7d2..30fa05662f 100644
--- a/libs/common/src/auth/models/request/set-password.request.ts
+++ b/libs/common/src/auth/models/request/set-password.request.ts
@@ -5,7 +5,7 @@ export class SetPasswordRequest {
masterPasswordHash: string;
key: string;
masterPasswordHint: string;
- keys: KeysRequest;
+ keys: KeysRequest | null;
kdf: KdfType;
kdfIterations: number;
kdfMemory?: number;
@@ -17,7 +17,7 @@ export class SetPasswordRequest {
key: string,
masterPasswordHint: string,
orgIdentifier: string,
- keys: KeysRequest,
+ keys: KeysRequest | null,
kdf: KdfType,
kdfIterations: number,
kdfMemory?: number,
diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts
index 571dad6478..a40f6b36b1 100644
--- a/libs/common/src/platform/abstractions/state.service.ts
+++ b/libs/common/src/platform/abstractions/state.service.ts
@@ -7,7 +7,7 @@ import { ProviderData } from "../../admin-console/models/data/provider.data";
import { Policy } from "../../admin-console/models/domain/policy";
import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-req-storable";
import { EnvironmentUrls } from "../../auth/models/domain/environment-urls";
-import { ForceResetPasswordReason } from "../../auth/models/domain/force-reset-password-reason";
+import { ForceSetPasswordReason } from "../../auth/models/domain/force-set-password-reason";
import { KdfConfig } from "../../auth/models/domain/kdf-config";
import { BiometricKey } from "../../auth/types/biometric-key";
import { KdfType, ThemeType, UriMatchType } from "../../enums";
@@ -391,9 +391,9 @@ export abstract class StateService {
setEverHadUserKey: (value: boolean, options?: StorageOptions) => Promise;
getEverBeenUnlocked: (options?: StorageOptions) => Promise;
setEverBeenUnlocked: (value: boolean, options?: StorageOptions) => Promise;
- getForcePasswordResetReason: (options?: StorageOptions) => Promise;
- setForcePasswordResetReason: (
- value: ForceResetPasswordReason,
+ getForceSetPasswordReason: (options?: StorageOptions) => Promise;
+ setForceSetPasswordReason: (
+ value: ForceSetPasswordReason,
options?: StorageOptions
) => Promise;
getInstalledVersion: (options?: StorageOptions) => Promise;
diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts
index 6d85d6501f..2583c07461 100644
--- a/libs/common/src/platform/models/domain/account.ts
+++ b/libs/common/src/platform/models/domain/account.ts
@@ -8,7 +8,7 @@ import { Policy } from "../../../admin-console/models/domain/policy";
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
import { AdminAuthRequestStorable } from "../../../auth/models/domain/admin-auth-req-storable";
import { EnvironmentUrls } from "../../../auth/models/domain/environment-urls";
-import { ForceResetPasswordReason } from "../../../auth/models/domain/force-reset-password-reason";
+import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
import { KeyConnectorUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/key-connector-user-decryption-option";
import { TrustedDeviceUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
import { IdentityTokenResponse } from "../../../auth/models/response/identity-token.response";
@@ -194,7 +194,7 @@ export class AccountProfile {
entityType?: string;
everHadUserKey?: boolean;
everBeenUnlocked?: boolean;
- forcePasswordResetReason?: ForceResetPasswordReason;
+ forceSetPasswordReason?: ForceSetPasswordReason;
hasPremiumPersonally?: boolean;
hasPremiumFromOrganization?: boolean;
lastSync?: string;
diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts
index 78bfddd6e9..d9dcb93a36 100644
--- a/libs/common/src/platform/services/state.service.ts
+++ b/libs/common/src/platform/services/state.service.ts
@@ -10,7 +10,7 @@ import { AccountService } from "../../auth/abstractions/account.service";
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-req-storable";
import { EnvironmentUrls } from "../../auth/models/domain/environment-urls";
-import { ForceResetPasswordReason } from "../../auth/models/domain/force-reset-password-reason";
+import { ForceSetPasswordReason } from "../../auth/models/domain/force-set-password-reason";
import { KdfConfig } from "../../auth/models/domain/kdf-config";
import { BiometricKey } from "../../auth/types/biometric-key";
import {
@@ -2072,24 +2072,24 @@ export class StateService<
);
}
- async getForcePasswordResetReason(options?: StorageOptions): Promise {
+ async getForceSetPasswordReason(options?: StorageOptions): Promise {
return (
(
await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())
)
- )?.profile?.forcePasswordResetReason ?? ForceResetPasswordReason.None
+ )?.profile?.forceSetPasswordReason ?? ForceSetPasswordReason.None
);
}
- async setForcePasswordResetReason(
- value: ForceResetPasswordReason,
+ async setForceSetPasswordReason(
+ value: ForceSetPasswordReason,
options?: StorageOptions
): Promise {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())
);
- account.profile.forcePasswordResetReason = value;
+ account.profile.forceSetPasswordReason = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())
diff --git a/libs/common/src/vault/services/sync/sync.service.ts b/libs/common/src/vault/services/sync/sync.service.ts
index c8bd274a4d..7368d82f73 100644
--- a/libs/common/src/vault/services/sync/sync.service.ts
+++ b/libs/common/src/vault/services/sync/sync.service.ts
@@ -3,12 +3,13 @@ import { SettingsService } from "../../../abstractions/settings.service";
import { InternalOrganizationServiceAbstraction } from "../../../admin-console/abstractions/organization/organization.service.abstraction";
import { InternalPolicyService } from "../../../admin-console/abstractions/policy/policy.service.abstraction";
import { ProviderService } from "../../../admin-console/abstractions/provider.service";
+import { OrganizationUserType } from "../../../admin-console/enums";
import { OrganizationData } from "../../../admin-console/models/data/organization.data";
import { PolicyData } from "../../../admin-console/models/data/policy.data";
import { ProviderData } from "../../../admin-console/models/data/provider.data";
import { PolicyResponse } from "../../../admin-console/models/response/policy.response";
import { KeyConnectorService } from "../../../auth/abstractions/key-connector.service";
-import { ForceResetPasswordReason } from "../../../auth/models/domain/force-reset-password-reason";
+import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
import { DomainsResponse } from "../../../models/response/domains.response";
import {
SyncCipherNotification,
@@ -21,6 +22,7 @@ import { LogService } from "../../../platform/abstractions/log.service";
import { MessagingService } from "../../../platform/abstractions/messaging.service";
import { StateService } from "../../../platform/abstractions/state.service";
import { sequentialize } from "../../../platform/misc/sequentialize";
+import { AccountDecryptionOptions } from "../../../platform/models/domain/account";
import { SendData } from "../../../tools/send/models/data/send.data";
import { SendResponse } from "../../../tools/send/models/response/send.response";
import { SendApiService } from "../../../tools/send/services/send-api.service.abstraction";
@@ -314,12 +316,7 @@ export class SyncService implements SyncServiceAbstraction {
await this.stateService.setHasPremiumFromOrganization(response.premiumFromOrganization);
await this.keyConnectorService.setUsesKeyConnector(response.usesKeyConnector);
- // The `forcePasswordReset` flag indicates an admin has reset the user's password and must be updated
- if (response.forcePasswordReset) {
- await this.stateService.setForcePasswordResetReason(
- ForceResetPasswordReason.AdminForcePasswordReset
- );
- }
+ await this.setForceSetPasswordReasonIfNeeded(response);
await this.syncProfileOrganizations(response);
@@ -338,6 +335,45 @@ export class SyncService implements SyncServiceAbstraction {
}
}
+ private async setForceSetPasswordReasonIfNeeded(profileResponse: ProfileResponse) {
+ // The `forcePasswordReset` flag indicates an admin has reset the user's password and must be updated
+ if (profileResponse.forcePasswordReset) {
+ await this.stateService.setForceSetPasswordReason(
+ ForceSetPasswordReason.AdminForcePasswordReset
+ );
+ }
+
+ const acctDecryptionOpts: AccountDecryptionOptions =
+ await this.stateService.getAccountDecryptionOptions();
+
+ // Even though TDE users should only be in a single org (per single org policy), check
+ // through all orgs for the manageResetPassword permission. If they have it in any org,
+ // they should be forced to set a password.
+ let hasManageResetPasswordPermission = false;
+ for (const org of profileResponse.organizations) {
+ const isAdmin = org.type === OrganizationUserType.Admin;
+ const isOwner = org.type === OrganizationUserType.Owner;
+
+ // Note: apparently permissions only come down populated for custom roles.
+ if (isAdmin || isOwner || (org.permissions && org.permissions.manageResetPassword)) {
+ hasManageResetPasswordPermission = true;
+ break;
+ }
+ }
+
+ if (
+ acctDecryptionOpts.trustedDeviceOption !== undefined &&
+ !acctDecryptionOpts.hasMasterPassword &&
+ hasManageResetPasswordPermission
+ ) {
+ // TDE user w/out MP went from having no password reset permission to having it.
+ // Must set the force password reset reason so the auth guard will redirect to the set password page.
+ await this.stateService.setForceSetPasswordReason(
+ ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission
+ );
+ }
+ }
+
private async syncProfileOrganizations(response: ProfileResponse) {
const organizations: { [id: string]: OrganizationData } = {};
response.organizations.forEach((o) => {