mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 15:53:27 +00:00
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`
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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<VaultTimeoutSettingsService>;
|
||||
let kdfConfigService: MockProxy<KdfConfigService>;
|
||||
let environmentService: MockProxy<EnvironmentService>;
|
||||
let configService: MockProxy<ConfigService>;
|
||||
|
||||
let ssoLoginStrategy: SsoLoginStrategy;
|
||||
let credentials: SsoLoginCredentials;
|
||||
@@ -102,6 +107,7 @@ describe("SsoLoginStrategy", () => {
|
||||
vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
|
||||
kdfConfigService = mock<KdfConfigService>();
|
||||
environmentService = mock<EnvironmentService>();
|
||||
configService = mock<ConfigService>();
|
||||
|
||||
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;
|
||||
|
||||
@@ -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<typeof LoginStrategy>
|
||||
) {
|
||||
super(...sharedDeps);
|
||||
@@ -343,13 +346,38 @@ export class SsoLoginStrategy extends LoginStrategy {
|
||||
tokenResponse: IdentityTokenResponse,
|
||||
userId: UserId,
|
||||
): Promise<void> {
|
||||
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 &&
|
||||
|
||||
@@ -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<VaultTimeoutSettingsService>;
|
||||
let kdfConfigService: MockProxy<KdfConfigService>;
|
||||
let taskSchedulerService: MockProxy<TaskSchedulerService>;
|
||||
let configService: MockProxy<ConfigService>;
|
||||
|
||||
let stateProvider: FakeGlobalStateProvider;
|
||||
let loginStrategyCacheExpirationState: FakeGlobalState<Date | null>;
|
||||
@@ -107,6 +109,7 @@ describe("LoginStrategyService", () => {
|
||||
vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
|
||||
kdfConfigService = mock<KdfConfigService>();
|
||||
taskSchedulerService = mock<TaskSchedulerService>();
|
||||
configService = mock<ConfigService>();
|
||||
|
||||
sut = new LoginStrategyService(
|
||||
accountService,
|
||||
@@ -134,6 +137,7 @@ describe("LoginStrategyService", () => {
|
||||
vaultTimeoutSettingsService,
|
||||
kdfConfigService,
|
||||
taskSchedulerService,
|
||||
configService,
|
||||
);
|
||||
|
||||
loginStrategyCacheExpirationState = stateProvider.getFake(CACHE_EXPIRATION_KEY);
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user