From b9f930a609f1e7a006f8f08b3ba18192341bc168 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Tue, 8 Jul 2025 12:58:03 -0400 Subject: [PATCH] fix(tde-offboarding): Auth/PM-19165 - Handle TDE offboarding on an untrusted device with warning message (#15430) When a user logs in via SSO after their org has offboarded from TDE, we now show them a helpful error message stating that they must either login on a Trusted device, or ask their admin to assign them a password. Feature flag: `PM16117_SetInitialPasswordRefactor` --- apps/browser/src/_locales/en/messages.json | 6 ++ .../service-container/service-container.ts | 1 + apps/desktop/src/locales/en/messages.json | 6 ++ apps/web/src/locales/en/messages.json | 6 ++ .../src/auth/guards/auth.guard.spec.ts | 34 ++++++++- libs/angular/src/auth/guards/auth.guard.ts | 17 ++++- .../set-initial-password.component.html | 56 ++++++++------ .../set-initial-password.component.ts | 13 +++- ...et-initial-password.service.abstraction.ts | 8 +- .../src/services/jslib-services.module.ts | 1 + .../common/login-strategies/login.strategy.ts | 5 +- .../sso-login.strategy.spec.ts | 46 ++++++++++++ .../login-strategies/sso-login.strategy.ts | 75 +++++++++++++++++-- .../login-strategy.service.spec.ts | 4 + .../login-strategy.service.ts | 3 + .../domain/force-set-password-reason.ts | 9 +++ .../response/identity-token.response.ts | 8 +- 17 files changed, 257 insertions(+), 41 deletions(-) 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); + } }