1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-12 06:13:38 +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:
Jared Snider
2025-07-08 12:58:03 -04:00
committed by GitHub
parent 3da58e1752
commit b9f930a609
17 changed files with 257 additions and 41 deletions

View File

@@ -3568,6 +3568,12 @@
"requestAdminApproval": { "requestAdminApproval": {
"message": "Request admin approval" "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": { "ssoIdentifierRequired": {
"message": "Organization SSO identifier is required." "message": "Organization SSO identifier is required."
}, },

View File

@@ -682,6 +682,7 @@ export class ServiceContainer {
this.vaultTimeoutSettingsService, this.vaultTimeoutSettingsService,
this.kdfConfigService, this.kdfConfigService,
this.taskSchedulerService, this.taskSchedulerService,
this.configService,
); );
// FIXME: CLI does not support autofill // FIXME: CLI does not support autofill

View File

@@ -3144,6 +3144,12 @@
"requestAdminApproval": { "requestAdminApproval": {
"message": "Request admin approval" "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": { "region": {
"message": "Region" "message": "Region"
}, },

View File

@@ -8454,6 +8454,12 @@
"requestAdminApproval": { "requestAdminApproval": {
"message": "Request admin approval" "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": { "trustedDeviceEncryption": {
"message": "Trusted device encryption" "message": "Trusted device encryption"
}, },

View File

@@ -70,10 +70,10 @@ describe("AuthGuard", () => {
{ path: "lock", component: EmptyComponent }, { path: "lock", component: EmptyComponent },
{ path: "set-password", component: EmptyComponent }, { path: "set-password", component: EmptyComponent },
{ path: "set-password-jit", component: EmptyComponent }, { path: "set-password-jit", component: EmptyComponent },
{ path: "set-initial-password", component: EmptyComponent }, { path: "set-initial-password", component: EmptyComponent, canActivate: [authGuard] },
{ path: "update-temp-password", component: EmptyComponent }, { path: "update-temp-password", component: EmptyComponent, canActivate: [authGuard] },
{ path: "change-password", component: EmptyComponent }, { path: "change-password", component: EmptyComponent },
{ path: "remove-password", component: EmptyComponent }, { path: "remove-password", component: EmptyComponent, canActivate: [authGuard] },
]), ]),
], ],
providers: [ providers: [
@@ -124,6 +124,34 @@ describe("AuthGuard", () => {
expect(router.url).toBe("/remove-password"); 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 user is Unlocked", () => {
describe("given the PM16117_SetInitialPasswordRefactor feature flag is ON", () => { describe("given the PM16117_SetInitialPasswordRefactor feature flag is ON", () => {
const tests = [ const tests = [

View File

@@ -61,9 +61,22 @@ export const authGuard: CanActivateFn = async (
return router.createUrlTree(["/set-initial-password"]); return router.createUrlTree(["/set-initial-password"]);
} }
// TDE Offboarding on untrusted device
if ( if (
authStatus === AuthenticationStatus.Locked && 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) { if (routerState != null) {
messagingService.send("lockedUrl", { url: routerState.url }); messagingService.send("lockedUrl", { url: routerState.url });
@@ -91,7 +104,7 @@ export const authGuard: CanActivateFn = async (
return router.createUrlTree([route]); return router.createUrlTree([route]);
} }
// TDE Offboarding // TDE Offboarding on trusted device
if ( if (
forceSetPasswordReason === ForceSetPasswordReason.TdeOffboarding && forceSetPasswordReason === ForceSetPasswordReason.TdeOffboarding &&
!routerState.url.includes("update-temp-password") && !routerState.url.includes("update-temp-password") &&

View File

@@ -7,28 +7,38 @@
></i> ></i>
</div> </div>
} @else { } @else {
<bit-callout @if (userType === SetInitialPasswordUserType.OFFBOARDED_TDE_ORG_USER_UNTRUSTED_DEVICE) {
*ngIf="resetPasswordAutoEnroll" <div class="tw-mt-4"></div>
type="warning" <bit-callout type="warning">
title="{{ 'resetPasswordPolicyAutoEnroll' | i18n }}" {{ "loginOnTrustedDeviceOrAskAdminToAssignPassword" | i18n }}
> </bit-callout>
{{ "resetPasswordAutoEnrollInviteWarning" | i18n }} <button type="button" bitButton block buttonType="secondary" (click)="logout()">
</bit-callout> {{ "logOut" | i18n }}
</button>
} @else {
<bit-callout
*ngIf="resetPasswordAutoEnroll"
type="warning"
title="{{ 'resetPasswordPolicyAutoEnroll' | i18n }}"
>
{{ "resetPasswordAutoEnrollInviteWarning" | i18n }}
</bit-callout>
<auth-input-password <auth-input-password
[flow]="inputPasswordFlow" [flow]="inputPasswordFlow"
[email]="email" [email]="email"
[userId]="userId" [userId]="userId"
[loading]="submitting" [loading]="submitting"
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions" [masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
[primaryButtonText]="{ [primaryButtonText]="{
key: key:
userType === SetInitialPasswordUserType.OFFBOARDED_TDE_ORG_USER userType === SetInitialPasswordUserType.OFFBOARDED_TDE_ORG_USER
? 'setPassword' ? 'setPassword'
: 'createAccount', : 'createAccount',
}" }"
[secondaryButtonText]="{ key: 'logOut' }" [secondaryButtonText]="{ key: 'logOut' }"
(onPasswordFormSubmit)="handlePasswordFormSubmit($event)" (onPasswordFormSubmit)="handlePasswordFormSubmit($event)"
(onSecondaryButtonClick)="logout()" (onSecondaryButtonClick)="logout()"
></auth-input-password> ></auth-input-password>
}
} }

View File

@@ -1,6 +1,7 @@
import { CommonModule } from "@angular/common"; import { CommonModule } from "@angular/common";
import { Component, OnInit } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
// import { NoAccess } from "libs/components/src/icon/icons";
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // 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 { UserId } from "@bitwarden/common/types/guid";
import { import {
AnonLayoutWrapperDataService, AnonLayoutWrapperDataService,
ButtonModule,
CalloutComponent, CalloutComponent,
DialogService, DialogService,
ToastService, ToastService,
Icons,
} from "@bitwarden/components"; } from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common"; import { I18nPipe } from "@bitwarden/ui-common";
@@ -46,7 +49,7 @@ import {
@Component({ @Component({
standalone: true, standalone: true,
templateUrl: "set-initial-password.component.html", templateUrl: "set-initial-password.component.html",
imports: [CalloutComponent, CommonModule, InputPasswordComponent, I18nPipe], imports: [ButtonModule, CalloutComponent, CommonModule, InputPasswordComponent, I18nPipe],
}) })
export class SetInitialPasswordComponent implements OnInit { export class SetInitialPasswordComponent implements OnInit {
protected inputPasswordFlow = InputPasswordFlow.SetInitialPasswordAuthedUser; protected inputPasswordFlow = InputPasswordFlow.SetInitialPasswordAuthedUser;
@@ -106,6 +109,14 @@ export class SetInitialPasswordComponent implements OnInit {
this.masterPasswordService.forceSetPasswordReason$(this.userId), 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) { if (this.forceSetPasswordReason === ForceSetPasswordReason.SsoNewJitProvisionedUser) {
this.userType = SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER; this.userType = SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER;
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({ this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({

View File

@@ -22,9 +22,15 @@ export const _SetInitialPasswordUserType = {
/** /**
* A user in an org that offboarded from trusted device encryption and is now a * 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", 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; } as const;
type _SetInitialPasswordUserType = typeof _SetInitialPasswordUserType; type _SetInitialPasswordUserType = typeof _SetInitialPasswordUserType;

View File

@@ -498,6 +498,7 @@ const safeProviders: SafeProvider[] = [
VaultTimeoutSettingsService, VaultTimeoutSettingsService,
KdfConfigService, KdfConfigService,
TaskSchedulerService, TaskSchedulerService,
ConfigService,
], ],
}), }),
safeProvider({ safeProvider({

View File

@@ -265,8 +265,6 @@ export abstract class LoginStrategy {
result.resetMasterPassword = response.resetMasterPassword; result.resetMasterPassword = response.resetMasterPassword;
await this.processForceSetPasswordReason(response.forcePasswordReset, userId);
if (response.twoFactorToken != null) { if (response.twoFactorToken != null) {
// note: we can read email from access token b/c it was saved in saveAccountInformation // note: we can read email from access token b/c it was saved in saveAccountInformation
const userEmail = await this.tokenService.getEmail(); const userEmail = await this.tokenService.getEmail();
@@ -278,6 +276,9 @@ export abstract class LoginStrategy {
await this.setUserKey(response, userId); await this.setUserKey(response, userId);
await this.setPrivateKey(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"); this.messagingService.send("loggedIn");
return result; return result;

View File

@@ -5,10 +5,12 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable"; 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 { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.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 { 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 { 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 { 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 { 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 { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
@@ -19,6 +21,7 @@ import {
} from "@bitwarden/common/key-management/vault-timeout"; } from "@bitwarden/common/key-management/vault-timeout";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; 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 { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; 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 { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
import { CsprngArray } from "@bitwarden/common/types/csprng"; import { CsprngArray } from "@bitwarden/common/types/csprng";
@@ -66,6 +70,7 @@ describe("SsoLoginStrategy", () => {
let vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService>; let vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService>;
let kdfConfigService: MockProxy<KdfConfigService>; let kdfConfigService: MockProxy<KdfConfigService>;
let environmentService: MockProxy<EnvironmentService>; let environmentService: MockProxy<EnvironmentService>;
let configService: MockProxy<ConfigService>;
let ssoLoginStrategy: SsoLoginStrategy; let ssoLoginStrategy: SsoLoginStrategy;
let credentials: SsoLoginCredentials; let credentials: SsoLoginCredentials;
@@ -102,6 +107,7 @@ describe("SsoLoginStrategy", () => {
vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>(); vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
kdfConfigService = mock<KdfConfigService>(); kdfConfigService = mock<KdfConfigService>();
environmentService = mock<EnvironmentService>(); environmentService = mock<EnvironmentService>();
configService = mock<ConfigService>();
tokenService.getTwoFactorToken.mockResolvedValue(null); tokenService.getTwoFactorToken.mockResolvedValue(null);
appIdService.getAppId.mockResolvedValue(deviceId); appIdService.getAppId.mockResolvedValue(deviceId);
@@ -133,6 +139,7 @@ describe("SsoLoginStrategy", () => {
deviceTrustService, deviceTrustService,
authRequestService, authRequestService,
i18nService, i18nService,
configService,
accountService, accountService,
masterPasswordService, masterPasswordService,
keyService, 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", () => { describe("Trusted Device Decryption", () => {
const deviceKeyBytesLength = 64; const deviceKeyBytesLength = 64;
const mockDeviceKeyRandomBytes = new Uint8Array(deviceKeyBytesLength).buffer as CsprngArray; const mockDeviceKeyRandomBytes = new Uint8Array(deviceKeyBytesLength).buffer as CsprngArray;

View File

@@ -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 { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
import { HttpStatusCode } from "@bitwarden/common/enums"; 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 { 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 { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
@@ -72,6 +74,7 @@ export class SsoLoginStrategy extends LoginStrategy {
private deviceTrustService: DeviceTrustServiceAbstraction, private deviceTrustService: DeviceTrustServiceAbstraction,
private authRequestService: AuthRequestServiceAbstraction, private authRequestService: AuthRequestServiceAbstraction,
private i18nService: I18nService, private i18nService: I18nService,
private configService: ConfigService,
...sharedDeps: ConstructorParameters<typeof LoginStrategy> ...sharedDeps: ConstructorParameters<typeof LoginStrategy>
) { ) {
super(...sharedDeps); super(...sharedDeps);
@@ -343,13 +346,38 @@ export class SsoLoginStrategy extends LoginStrategy {
tokenResponse: IdentityTokenResponse, tokenResponse: IdentityTokenResponse,
userId: UserId, userId: UserId,
): Promise<void> { ): Promise<void> {
const newSsoUser = tokenResponse.key == null; const isSetInitialPasswordFlagOn = await this.configService.getFeatureFlag(
FeatureFlag.PM16117_SetInitialPasswordRefactor,
);
if (!newSsoUser) { if (isSetInitialPasswordFlagOn) {
await this.keyService.setPrivateKey( if (tokenResponse.hasMasterKeyEncryptedUserKey()) {
tokenResponse.privateKey ?? (await this.createKeyPairForOldAccount(userId)), // User has masterKeyEncryptedUserKey, so set the userKeyEncryptedPrivateKey
userId, // 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; 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) { if (userDecryptionOptions.trustedDeviceOption?.isTdeOffboarding) {
await this.masterPasswordService.setForceSetPasswordReason( await this.masterPasswordService.setForceSetPasswordReason(
ForceSetPasswordReason.TdeOffboarding, ForceSetPasswordReason.TdeOffboarding,
@@ -398,6 +426,39 @@ export class SsoLoginStrategy extends LoginStrategy {
return true; 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 // Check if user has permission to set password but hasn't yet
if ( if (
!userDecryptionOptions.hasMasterPassword && !userDecryptionOptions.hasMasterPassword &&

View File

@@ -21,6 +21,7 @@ import {
VaultTimeoutSettingsService, VaultTimeoutSettingsService,
} from "@bitwarden/common/key-management/vault-timeout"; } from "@bitwarden/common/key-management/vault-timeout";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; 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 { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -75,6 +76,7 @@ describe("LoginStrategyService", () => {
let vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService>; let vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService>;
let kdfConfigService: MockProxy<KdfConfigService>; let kdfConfigService: MockProxy<KdfConfigService>;
let taskSchedulerService: MockProxy<TaskSchedulerService>; let taskSchedulerService: MockProxy<TaskSchedulerService>;
let configService: MockProxy<ConfigService>;
let stateProvider: FakeGlobalStateProvider; let stateProvider: FakeGlobalStateProvider;
let loginStrategyCacheExpirationState: FakeGlobalState<Date | null>; let loginStrategyCacheExpirationState: FakeGlobalState<Date | null>;
@@ -107,6 +109,7 @@ describe("LoginStrategyService", () => {
vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>(); vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
kdfConfigService = mock<KdfConfigService>(); kdfConfigService = mock<KdfConfigService>();
taskSchedulerService = mock<TaskSchedulerService>(); taskSchedulerService = mock<TaskSchedulerService>();
configService = mock<ConfigService>();
sut = new LoginStrategyService( sut = new LoginStrategyService(
accountService, accountService,
@@ -134,6 +137,7 @@ describe("LoginStrategyService", () => {
vaultTimeoutSettingsService, vaultTimeoutSettingsService,
kdfConfigService, kdfConfigService,
taskSchedulerService, taskSchedulerService,
configService,
); );
loginStrategyCacheExpirationState = stateProvider.getFake(CACHE_EXPIRATION_KEY); loginStrategyCacheExpirationState = stateProvider.getFake(CACHE_EXPIRATION_KEY);

View File

@@ -26,6 +26,7 @@ import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/va
import { PreloginRequest } from "@bitwarden/common/models/request/prelogin.request"; import { PreloginRequest } from "@bitwarden/common/models/request/prelogin.request";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; 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 { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -131,6 +132,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
protected vaultTimeoutSettingsService: VaultTimeoutSettingsService, protected vaultTimeoutSettingsService: VaultTimeoutSettingsService,
protected kdfConfigService: KdfConfigService, protected kdfConfigService: KdfConfigService,
protected taskSchedulerService: TaskSchedulerService, protected taskSchedulerService: TaskSchedulerService,
protected configService: ConfigService,
) { ) {
this.currentAuthnTypeState = this.stateProvider.get(CURRENT_LOGIN_STRATEGY_KEY); this.currentAuthnTypeState = this.stateProvider.get(CURRENT_LOGIN_STRATEGY_KEY);
this.loginStrategyCacheState = this.stateProvider.get(CACHE_KEY); this.loginStrategyCacheState = this.stateProvider.get(CACHE_KEY);
@@ -423,6 +425,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
this.deviceTrustService, this.deviceTrustService,
this.authRequestService, this.authRequestService,
this.i18nService, this.i18nService,
this.configService,
...sharedDeps, ...sharedDeps,
); );
case AuthenticationType.UserApiKey: case AuthenticationType.UserApiKey:

View File

@@ -37,6 +37,15 @@ export enum ForceSetPasswordReason {
*/ */
TdeOffboarding, 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 Change Existing Password
-----------------------------*/ -----------------------------*/

View File

@@ -17,8 +17,8 @@ export class IdentityTokenResponse extends BaseResponse {
tokenType: string; tokenType: string;
resetMasterPassword: boolean; resetMasterPassword: boolean;
privateKey: string; privateKey: string; // userKeyEncryptedPrivateKey
key?: EncString; key?: EncString; // masterKeyEncryptedUserKey
twoFactorToken: string; twoFactorToken: string;
kdf: KdfType; kdf: KdfType;
kdfIterations: number; kdfIterations: number;
@@ -62,4 +62,8 @@ export class IdentityTokenResponse extends BaseResponse {
); );
} }
} }
hasMasterKeyEncryptedUserKey(): boolean {
return Boolean(this.key);
}
} }