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:
@@ -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."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 = [
|
||||||
|
|||||||
@@ -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") &&
|
||||||
|
|||||||
@@ -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>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -498,6 +498,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
VaultTimeoutSettingsService,
|
VaultTimeoutSettingsService,
|
||||||
KdfConfigService,
|
KdfConfigService,
|
||||||
TaskSchedulerService,
|
TaskSchedulerService,
|
||||||
|
ConfigService,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 &&
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
-----------------------------*/
|
-----------------------------*/
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user