diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json
index 996bdcdae79..957386ba576 100644
--- a/apps/browser/src/_locales/en/messages.json
+++ b/apps/browser/src/_locales/en/messages.json
@@ -3568,6 +3568,12 @@
"requestAdminApproval": {
"message": "Request admin approval"
},
+ "unableToCompleteLogin": {
+ "message": "Unable to complete login"
+ },
+ "loginOnTrustedDeviceOrAskAdminToAssignPassword": {
+ "message": "You need to log in on a trusted device or ask your administrator to assign you a password."
+ },
"ssoIdentifierRequired": {
"message": "Organization SSO identifier is required."
},
diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts
index f27c69cf47b..53950e5da11 100644
--- a/apps/cli/src/service-container/service-container.ts
+++ b/apps/cli/src/service-container/service-container.ts
@@ -682,6 +682,7 @@ export class ServiceContainer {
this.vaultTimeoutSettingsService,
this.kdfConfigService,
this.taskSchedulerService,
+ this.configService,
);
// FIXME: CLI does not support autofill
diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json
index 991d07fb9df..703b65c35b4 100644
--- a/apps/desktop/src/locales/en/messages.json
+++ b/apps/desktop/src/locales/en/messages.json
@@ -3144,6 +3144,12 @@
"requestAdminApproval": {
"message": "Request admin approval"
},
+ "unableToCompleteLogin": {
+ "message": "Unable to complete login"
+ },
+ "loginOnTrustedDeviceOrAskAdminToAssignPassword": {
+ "message": "You need to log in on a trusted device or ask your administrator to assign you a password."
+ },
"region": {
"message": "Region"
},
diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json
index c5a7a64ee5c..ba5e4841e3d 100644
--- a/apps/web/src/locales/en/messages.json
+++ b/apps/web/src/locales/en/messages.json
@@ -8454,6 +8454,12 @@
"requestAdminApproval": {
"message": "Request admin approval"
},
+ "unableToCompleteLogin": {
+ "message": "Unable to complete login"
+ },
+ "loginOnTrustedDeviceOrAskAdminToAssignPassword": {
+ "message": "You need to log in on a trusted device or ask your administrator to assign you a password."
+ },
"trustedDeviceEncryption": {
"message": "Trusted device encryption"
},
diff --git a/libs/angular/src/auth/guards/auth.guard.spec.ts b/libs/angular/src/auth/guards/auth.guard.spec.ts
index a2e1613c6c1..1681fa2b4ea 100644
--- a/libs/angular/src/auth/guards/auth.guard.spec.ts
+++ b/libs/angular/src/auth/guards/auth.guard.spec.ts
@@ -70,10 +70,10 @@ describe("AuthGuard", () => {
{ path: "lock", component: EmptyComponent },
{ path: "set-password", component: EmptyComponent },
{ path: "set-password-jit", component: EmptyComponent },
- { path: "set-initial-password", component: EmptyComponent },
- { path: "update-temp-password", component: EmptyComponent },
+ { path: "set-initial-password", component: EmptyComponent, canActivate: [authGuard] },
+ { path: "update-temp-password", component: EmptyComponent, canActivate: [authGuard] },
{ path: "change-password", component: EmptyComponent },
- { path: "remove-password", component: EmptyComponent },
+ { path: "remove-password", component: EmptyComponent, canActivate: [authGuard] },
]),
],
providers: [
@@ -124,6 +124,34 @@ describe("AuthGuard", () => {
expect(router.url).toBe("/remove-password");
});
+ describe("given user is Locked", () => {
+ describe("given the PM16117_SetInitialPasswordRefactor feature flag is ON", () => {
+ it("should redirect to /set-initial-password when the user has ForceSetPasswordReaason.TdeOffboardingUntrustedDevice", async () => {
+ const { router } = setup(
+ AuthenticationStatus.Locked,
+ ForceSetPasswordReason.TdeOffboardingUntrustedDevice,
+ false,
+ FeatureFlag.PM16117_SetInitialPasswordRefactor,
+ );
+
+ await router.navigate(["guarded-route"]);
+ expect(router.url).toBe("/set-initial-password");
+ });
+
+ it("should allow navigation to continue to /set-initial-password when the user has ForceSetPasswordReason.TdeOffboardingUntrustedDevice", async () => {
+ const { router } = setup(
+ AuthenticationStatus.Unlocked,
+ ForceSetPasswordReason.TdeOffboardingUntrustedDevice,
+ false,
+ FeatureFlag.PM16117_SetInitialPasswordRefactor,
+ );
+
+ await router.navigate(["/set-initial-password"]);
+ expect(router.url).toContain("/set-initial-password");
+ });
+ });
+ });
+
describe("given user is Unlocked", () => {
describe("given the PM16117_SetInitialPasswordRefactor feature flag is ON", () => {
const tests = [
diff --git a/libs/angular/src/auth/guards/auth.guard.ts b/libs/angular/src/auth/guards/auth.guard.ts
index 7b8c21fef62..58ee3a59bbe 100644
--- a/libs/angular/src/auth/guards/auth.guard.ts
+++ b/libs/angular/src/auth/guards/auth.guard.ts
@@ -61,9 +61,22 @@ export const authGuard: CanActivateFn = async (
return router.createUrlTree(["/set-initial-password"]);
}
+ // TDE Offboarding on untrusted device
if (
authStatus === AuthenticationStatus.Locked &&
- forceSetPasswordReason !== ForceSetPasswordReason.SsoNewJitProvisionedUser
+ forceSetPasswordReason === ForceSetPasswordReason.TdeOffboardingUntrustedDevice &&
+ !routerState.url.includes("set-initial-password") &&
+ isSetInitialPasswordFlagOn
+ ) {
+ return router.createUrlTree(["/set-initial-password"]);
+ }
+
+ // We must add exemptions for the SsoNewJitProvisionedUser and TdeOffboardingUntrustedDevice scenarios as
+ // the "set-initial-password" route is guarded by the authGuard.
+ if (
+ authStatus === AuthenticationStatus.Locked &&
+ forceSetPasswordReason !== ForceSetPasswordReason.SsoNewJitProvisionedUser &&
+ forceSetPasswordReason !== ForceSetPasswordReason.TdeOffboardingUntrustedDevice
) {
if (routerState != null) {
messagingService.send("lockedUrl", { url: routerState.url });
@@ -91,7 +104,7 @@ export const authGuard: CanActivateFn = async (
return router.createUrlTree([route]);
}
- // TDE Offboarding
+ // TDE Offboarding on trusted device
if (
forceSetPasswordReason === ForceSetPasswordReason.TdeOffboarding &&
!routerState.url.includes("update-temp-password") &&
diff --git a/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.html b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.html
index 4956f293d1e..8033d2022f4 100644
--- a/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.html
+++ b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.html
@@ -7,28 +7,38 @@
>
} @else {
-
- {{ "resetPasswordAutoEnrollInviteWarning" | i18n }}
-
+ @if (userType === SetInitialPasswordUserType.OFFBOARDED_TDE_ORG_USER_UNTRUSTED_DEVICE) {
+
+
+ {{ "loginOnTrustedDeviceOrAskAdminToAssignPassword" | i18n }}
+
+
+ } @else {
+
+ {{ "resetPasswordAutoEnrollInviteWarning" | i18n }}
+
-
+
+ }
}
diff --git a/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.ts b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.ts
index 2de9aaf7b75..27d4c11f692 100644
--- a/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.ts
+++ b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.ts
@@ -1,6 +1,7 @@
import { CommonModule } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
+// import { NoAccess } from "libs/components/src/icon/icons";
import { firstValueFrom } from "rxjs";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
@@ -30,9 +31,11 @@ import { SyncService } from "@bitwarden/common/platform/sync";
import { UserId } from "@bitwarden/common/types/guid";
import {
AnonLayoutWrapperDataService,
+ ButtonModule,
CalloutComponent,
DialogService,
ToastService,
+ Icons,
} from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
@@ -46,7 +49,7 @@ import {
@Component({
standalone: true,
templateUrl: "set-initial-password.component.html",
- imports: [CalloutComponent, CommonModule, InputPasswordComponent, I18nPipe],
+ imports: [ButtonModule, CalloutComponent, CommonModule, InputPasswordComponent, I18nPipe],
})
export class SetInitialPasswordComponent implements OnInit {
protected inputPasswordFlow = InputPasswordFlow.SetInitialPasswordAuthedUser;
@@ -106,6 +109,14 @@ export class SetInitialPasswordComponent implements OnInit {
this.masterPasswordService.forceSetPasswordReason$(this.userId),
);
+ if (this.forceSetPasswordReason === ForceSetPasswordReason.TdeOffboardingUntrustedDevice) {
+ this.userType = SetInitialPasswordUserType.OFFBOARDED_TDE_ORG_USER_UNTRUSTED_DEVICE;
+ this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
+ pageTitle: { key: "unableToCompleteLogin" },
+ pageIcon: Icons.NoAccess,
+ });
+ }
+
if (this.forceSetPasswordReason === ForceSetPasswordReason.SsoNewJitProvisionedUser) {
this.userType = SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER;
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
diff --git a/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.service.abstraction.ts b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.service.abstraction.ts
index c167c1675c1..c1f6ba1a5ec 100644
--- a/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.service.abstraction.ts
+++ b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.service.abstraction.ts
@@ -22,9 +22,15 @@ export const _SetInitialPasswordUserType = {
/**
* A user in an org that offboarded from trusted device encryption and is now a
- * master-password-encryption org
+ * master-password-encryption org. User is on a trusted device.
*/
OFFBOARDED_TDE_ORG_USER: "offboarded_tde_org_user",
+
+ /**
+ * A user in an org that offboarded from trusted device encryption and is now a
+ * master-password-encryption org. User is on an untrusted device.
+ */
+ OFFBOARDED_TDE_ORG_USER_UNTRUSTED_DEVICE: "offboarded_tde_org_user_untrusted_device",
} as const;
type _SetInitialPasswordUserType = typeof _SetInitialPasswordUserType;
diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts
index 96a95de501e..acb1553387b 100644
--- a/libs/angular/src/services/jslib-services.module.ts
+++ b/libs/angular/src/services/jslib-services.module.ts
@@ -498,6 +498,7 @@ const safeProviders: SafeProvider[] = [
VaultTimeoutSettingsService,
KdfConfigService,
TaskSchedulerService,
+ ConfigService,
],
}),
safeProvider({
diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts
index f1b7d236fb7..dc51ce1fa04 100644
--- a/libs/auth/src/common/login-strategies/login.strategy.ts
+++ b/libs/auth/src/common/login-strategies/login.strategy.ts
@@ -265,8 +265,6 @@ export abstract class LoginStrategy {
result.resetMasterPassword = response.resetMasterPassword;
- await this.processForceSetPasswordReason(response.forcePasswordReset, userId);
-
if (response.twoFactorToken != null) {
// note: we can read email from access token b/c it was saved in saveAccountInformation
const userEmail = await this.tokenService.getEmail();
@@ -278,6 +276,9 @@ export abstract class LoginStrategy {
await this.setUserKey(response, userId);
await this.setPrivateKey(response, userId);
+ // This needs to run after the keys are set because it checks for the existence of the encrypted private key
+ await this.processForceSetPasswordReason(response.forcePasswordReset, userId);
+
this.messagingService.send("loggedIn");
return result;
diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts
index e5326a7ea97..98142003c6e 100644
--- a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts
+++ b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts
@@ -5,10 +5,12 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable";
+import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/user-decryption-options.response";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
+import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
@@ -19,6 +21,7 @@ import {
} from "@bitwarden/common/key-management/vault-timeout";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -26,6 +29,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
+import { EncryptedString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
import { CsprngArray } from "@bitwarden/common/types/csprng";
@@ -66,6 +70,7 @@ describe("SsoLoginStrategy", () => {
let vaultTimeoutSettingsService: MockProxy;
let kdfConfigService: MockProxy;
let environmentService: MockProxy;
+ let configService: MockProxy;
let ssoLoginStrategy: SsoLoginStrategy;
let credentials: SsoLoginCredentials;
@@ -102,6 +107,7 @@ describe("SsoLoginStrategy", () => {
vaultTimeoutSettingsService = mock();
kdfConfigService = mock();
environmentService = mock();
+ configService = mock();
tokenService.getTwoFactorToken.mockResolvedValue(null);
appIdService.getAppId.mockResolvedValue(deviceId);
@@ -133,6 +139,7 @@ describe("SsoLoginStrategy", () => {
deviceTrustService,
authRequestService,
i18nService,
+ configService,
accountService,
masterPasswordService,
keyService,
@@ -203,6 +210,45 @@ describe("SsoLoginStrategy", () => {
);
});
+ describe("given the PM16117_SetInitialPasswordRefactor feature flag is ON", () => {
+ beforeEach(() => {
+ configService.getFeatureFlag.mockImplementation(async (flag) => {
+ if (flag === FeatureFlag.PM16117_SetInitialPasswordRefactor) {
+ return true;
+ }
+ return false;
+ });
+ });
+
+ describe("given the user does not have the `trustedDeviceOption`, does not have a master password, is not using key connector, does not have a user key, but they DO have a `userKeyEncryptedPrivateKey`", () => {
+ it("should set the forceSetPasswordReason to TdeOffboardingUntrustedDevice", async () => {
+ // Arrange
+ const mockUserDecryptionOptions: IUserDecryptionOptionsServerResponse = {
+ HasMasterPassword: false,
+ TrustedDeviceOption: null,
+ KeyConnectorOption: null,
+ };
+ const tokenResponse = identityTokenResponseFactory(null, mockUserDecryptionOptions);
+ apiService.postIdentityToken.mockResolvedValue(tokenResponse);
+
+ keyService.userEncryptedPrivateKey$.mockReturnValue(
+ of("userKeyEncryptedPrivateKey" as EncryptedString),
+ );
+ keyService.hasUserKey.mockResolvedValue(false);
+
+ // Act
+ await ssoLoginStrategy.logIn(credentials);
+
+ // Assert
+ expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledTimes(1);
+ expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith(
+ ForceSetPasswordReason.TdeOffboardingUntrustedDevice,
+ userId,
+ );
+ });
+ });
+ });
+
describe("Trusted Device Decryption", () => {
const deviceKeyBytesLength = 64;
const mockDeviceKeyRandomBytes = new Uint8Array(deviceKeyBytesLength).buffer as CsprngArray;
diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.ts
index 4f5479cd5c4..a48ffd09503 100644
--- a/libs/auth/src/common/login-strategies/sso-login.strategy.ts
+++ b/libs/auth/src/common/login-strategies/sso-login.strategy.ts
@@ -9,9 +9,11 @@ import { SsoTokenRequest } from "@bitwarden/common/auth/models/request/identity-
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
import { HttpStatusCode } from "@bitwarden/common/enums";
+import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { UserId } from "@bitwarden/common/types/guid";
@@ -72,6 +74,7 @@ export class SsoLoginStrategy extends LoginStrategy {
private deviceTrustService: DeviceTrustServiceAbstraction,
private authRequestService: AuthRequestServiceAbstraction,
private i18nService: I18nService,
+ private configService: ConfigService,
...sharedDeps: ConstructorParameters
) {
super(...sharedDeps);
@@ -343,13 +346,38 @@ export class SsoLoginStrategy extends LoginStrategy {
tokenResponse: IdentityTokenResponse,
userId: UserId,
): Promise {
- const newSsoUser = tokenResponse.key == null;
+ const isSetInitialPasswordFlagOn = await this.configService.getFeatureFlag(
+ FeatureFlag.PM16117_SetInitialPasswordRefactor,
+ );
- if (!newSsoUser) {
- await this.keyService.setPrivateKey(
- tokenResponse.privateKey ?? (await this.createKeyPairForOldAccount(userId)),
- userId,
- );
+ if (isSetInitialPasswordFlagOn) {
+ if (tokenResponse.hasMasterKeyEncryptedUserKey()) {
+ // User has masterKeyEncryptedUserKey, so set the userKeyEncryptedPrivateKey
+ // Note: new JIT provisioned SSO users will not yet have a user asymmetric key pair
+ // and so we don't want them falling into the createKeyPairForOldAccount flow
+ await this.keyService.setPrivateKey(
+ tokenResponse.privateKey ?? (await this.createKeyPairForOldAccount(userId)),
+ userId,
+ );
+ } else if (tokenResponse.privateKey) {
+ // User doesn't have masterKeyEncryptedUserKey but they do have a userKeyEncryptedPrivateKey
+ // This is just existing TDE users or a TDE offboarder on an untrusted device
+ await this.keyService.setPrivateKey(tokenResponse.privateKey, userId);
+ }
+ // else {
+ // User could be new JIT provisioned SSO user in either a MP encryption org OR a TDE org.
+ // In either case, the user doesn't yet have a user asymmetric key pair, a user key, or a master key + master key encrypted user key.
+ // }
+ } else {
+ // A user that does not yet have a masterKeyEncryptedUserKey set is a new SSO user
+ const newSsoUser = tokenResponse.key == null;
+
+ if (!newSsoUser) {
+ await this.keyService.setPrivateKey(
+ tokenResponse.privateKey ?? (await this.createKeyPairForOldAccount(userId)),
+ userId,
+ );
+ }
}
}
@@ -389,7 +417,7 @@ export class SsoLoginStrategy extends LoginStrategy {
return false;
}
- // Check for TDE offboarding - user is being offboarded from TDE and needs to set a password
+ // Check for TDE offboarding - user is being offboarded from TDE and needs to set a password on a trusted device
if (userDecryptionOptions.trustedDeviceOption?.isTdeOffboarding) {
await this.masterPasswordService.setForceSetPasswordReason(
ForceSetPasswordReason.TdeOffboarding,
@@ -398,6 +426,39 @@ export class SsoLoginStrategy extends LoginStrategy {
return true;
}
+ // If a TDE org user in an offboarding state logs in on an untrusted device, then they will receive their existing userKeyEncryptedPrivateKey from the server, but
+ // TDE would not have been able to decrypt their user key b/c we don't send down TDE as a valid decryption option, so the user key will be unavilable here for TDE org users on untrusted devices.
+ // - UserDecryptionOptions.trustedDeviceOption is undefined -- device isn't trusted.
+ // - UserDecryptionOptions.hasMasterPassword is false -- user doesn't have a master password.
+ // - UserDecryptionOptions.UsesKeyConnector is undefined. -- they aren't using key connector
+ // - UserKey is not set after successful login -- because automatic decryption is not available
+ // - userKeyEncryptedPrivateKey is set after successful login -- this is the key differentiator between a TDE org user logging into an untrusted device and MP encryption JIT provisioned user logging in for the first time.
+ const isSetInitialPasswordFlagOn = await this.configService.getFeatureFlag(
+ FeatureFlag.PM16117_SetInitialPasswordRefactor,
+ );
+
+ if (isSetInitialPasswordFlagOn) {
+ const hasUserKeyEncryptedPrivateKey = await firstValueFrom(
+ this.keyService.userEncryptedPrivateKey$(userId),
+ );
+ const hasUserKey = await this.keyService.hasUserKey(userId);
+
+ // TODO: PM-23491 we should explore consolidating this logic into a flag on the server. It could be set when an org is switched from TDE to MP encryption for each org user.
+ if (
+ !userDecryptionOptions.trustedDeviceOption &&
+ !userDecryptionOptions.hasMasterPassword &&
+ !userDecryptionOptions.keyConnectorOption?.keyConnectorUrl &&
+ hasUserKeyEncryptedPrivateKey &&
+ !hasUserKey
+ ) {
+ await this.masterPasswordService.setForceSetPasswordReason(
+ ForceSetPasswordReason.TdeOffboardingUntrustedDevice,
+ userId,
+ );
+ return true;
+ }
+ }
+
// Check if user has permission to set password but hasn't yet
if (
!userDecryptionOptions.hasMasterPassword &&
diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts
index 981f5592621..8ddee96dd57 100644
--- a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts
+++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts
@@ -21,6 +21,7 @@ import {
VaultTimeoutSettingsService,
} from "@bitwarden/common/key-management/vault-timeout";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -75,6 +76,7 @@ describe("LoginStrategyService", () => {
let vaultTimeoutSettingsService: MockProxy;
let kdfConfigService: MockProxy;
let taskSchedulerService: MockProxy;
+ let configService: MockProxy;
let stateProvider: FakeGlobalStateProvider;
let loginStrategyCacheExpirationState: FakeGlobalState;
@@ -107,6 +109,7 @@ describe("LoginStrategyService", () => {
vaultTimeoutSettingsService = mock();
kdfConfigService = mock();
taskSchedulerService = mock();
+ configService = mock();
sut = new LoginStrategyService(
accountService,
@@ -134,6 +137,7 @@ describe("LoginStrategyService", () => {
vaultTimeoutSettingsService,
kdfConfigService,
taskSchedulerService,
+ configService,
);
loginStrategyCacheExpirationState = stateProvider.getFake(CACHE_EXPIRATION_KEY);
diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts
index a9b7ef250bc..767d52de370 100644
--- a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts
+++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts
@@ -26,6 +26,7 @@ import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/va
import { PreloginRequest } from "@bitwarden/common/models/request/prelogin.request";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -131,6 +132,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
protected vaultTimeoutSettingsService: VaultTimeoutSettingsService,
protected kdfConfigService: KdfConfigService,
protected taskSchedulerService: TaskSchedulerService,
+ protected configService: ConfigService,
) {
this.currentAuthnTypeState = this.stateProvider.get(CURRENT_LOGIN_STRATEGY_KEY);
this.loginStrategyCacheState = this.stateProvider.get(CACHE_KEY);
@@ -423,6 +425,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
this.deviceTrustService,
this.authRequestService,
this.i18nService,
+ this.configService,
...sharedDeps,
);
case AuthenticationType.UserApiKey:
diff --git a/libs/common/src/auth/models/domain/force-set-password-reason.ts b/libs/common/src/auth/models/domain/force-set-password-reason.ts
index 4a8ec8529cf..9e2069b30d6 100644
--- a/libs/common/src/auth/models/domain/force-set-password-reason.ts
+++ b/libs/common/src/auth/models/domain/force-set-password-reason.ts
@@ -37,6 +37,15 @@ export enum ForceSetPasswordReason {
*/
TdeOffboarding,
+ /**
+ * Occurs when an org admin switches the org from trusted-device-encryption to master-password-encryption,
+ * which forces the org user to set an initial password. User must not already have a master password,
+ * and they must be on an untrusted device.
+ *
+ * Calculated on client based on server flags and user state.
+ */
+ TdeOffboardingUntrustedDevice,
+
/*----------------------------
Change Existing Password
-----------------------------*/
diff --git a/libs/common/src/auth/models/response/identity-token.response.ts b/libs/common/src/auth/models/response/identity-token.response.ts
index f8c40b41bf0..2d991e9f349 100644
--- a/libs/common/src/auth/models/response/identity-token.response.ts
+++ b/libs/common/src/auth/models/response/identity-token.response.ts
@@ -17,8 +17,8 @@ export class IdentityTokenResponse extends BaseResponse {
tokenType: string;
resetMasterPassword: boolean;
- privateKey: string;
- key?: EncString;
+ privateKey: string; // userKeyEncryptedPrivateKey
+ key?: EncString; // masterKeyEncryptedUserKey
twoFactorToken: string;
kdf: KdfType;
kdfIterations: number;
@@ -62,4 +62,8 @@ export class IdentityTokenResponse extends BaseResponse {
);
}
}
+
+ hasMasterKeyEncryptedUserKey(): boolean {
+ return Boolean(this.key);
+ }
}