1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-13 06:43:35 +00:00

chore(feature flags): [PM-19034] Remove feature flags and old components for Set/Change Password

* Removed flag and components.

* More cleanup

* Removed ChangePasswordComponent.

* Removed old EmergencyAccessTakeover

* Removed service initialization.

* Fixed test failures.

* Fixed tests.

* Test changes.

* Updated comments

* Fixed tests.

* Fixed tests.

* Fixed merge conflict.

* Removed style and routing references.

* Better comments.

* Removed ResetPasswordComponent
This commit is contained in:
Todd Martin
2025-07-24 12:46:18 -04:00
committed by GitHub
parent df8e0ed094
commit b3db1b79ce
65 changed files with 247 additions and 4487 deletions

View File

@@ -1,160 +0,0 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<header>
<div class="left">
<button type="button" routerLink="/login">{{ "cancel" | i18n }}</button>
</div>
<h1 class="center">
<span class="title">{{ "setMasterPassword" | i18n }}</span>
</h1>
<div class="right">
<button type="submit" [disabled]="form.loading">
<span [hidden]="form.loading">{{ "submit" | i18n }}</span>
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
</button>
</div>
</header>
<main tabindex="-1">
<div class="full-loading-spinner" *ngIf="syncLoading">
<i class="bwi bwi-spinner bwi-spin bwi-3x" aria-hidden="true"></i>
</div>
<div *ngIf="!syncLoading">
<div class="box">
<p
class="tw-px-4"
*ngIf="
forceSetPasswordReason ==
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission;
else defaultCardDesc
"
>
{{ "orgPermissionsUpdatedMustSetPassword" | i18n }}
</p>
<ng-template #defaultCardDesc>
<p class="tw-px-4">{{ "orgRequiresYouToSetPassword" | i18n }}</p>
</ng-template>
<app-callout
type="warning"
title="{{ 'resetPasswordPolicyAutoEnroll' | i18n }}"
*ngIf="resetPasswordAutoEnroll"
>
{{ "resetPasswordAutoEnrollInviteWarning" | i18n }}
</app-callout>
<app-callout
type="info"
[enforcedPolicyOptions]="enforcedPolicyOptions"
*ngIf="enforcedPolicyOptions"
>
</app-callout>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<div class="box-content-row-flex">
<div class="row-main">
<label for="masterPassword">
{{ "masterPass" | i18n }}
<strong class="sub-label text-{{ color }}" *ngIf="text">
{{ text }}
</strong>
</label>
<input
id="masterPassword"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPassword"
aria-describedby="masterPasswordHelp"
class="monospaced"
[(ngModel)]="masterPassword"
required
appInputVerbatim
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword(false)"
[attr.aria-pressed]="showPassword"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
</div>
</div>
<app-password-strength
[password]="masterPassword"
[email]="email"
(passwordStrengthResult)="getStrengthResult($event)"
(passwordScoreColor)="getPasswordScoreText($event)"
>
</app-password-strength>
</div>
</div>
<div id="masterPasswordHelp" class="box-footer">
{{ "masterPassDesc" | i18n }}
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<div class="box-content-row-flex">
<div class="row-main">
<label for="masterPasswordRetype">{{ "reTypeMasterPass" | i18n }}</label>
<input
id="masterPasswordRetype"
type="password"
name="MasterPasswordRetype"
class="monospaced"
[(ngModel)]="masterPasswordRetype"
required
appInputVerbatim
autocomplete="new-password"
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword(true)"
[attr.aria-pressed]="showPassword"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="box last">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="hint">{{ "masterPassHint" | i18n }}</label>
<input
id="hint"
type="text"
name="Hint"
aria-describedby="hintHelp"
[(ngModel)]="hint"
/>
</div>
</div>
<div id="hintHelp" class="box-footer">
{{ "masterPassHintDesc" | i18n }}
</div>
</div>
</div>
</main>
</form>

View File

@@ -1,10 +0,0 @@
import { Component } from "@angular/core";
import { SetPasswordComponent as BaseSetPasswordComponent } from "@bitwarden/angular/auth/components/set-password.component";
@Component({
selector: "app-set-password",
templateUrl: "set-password.component.html",
standalone: false,
})
export class SetPasswordComponent extends BaseSetPasswordComponent {}

View File

@@ -1,142 +0,0 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<header>
<div class="left">
<button type="button" (click)="logOut()">{{ "logOut" | i18n }}</button>
</div>
<h1 class="center">
<span class="title">{{ "updateMasterPassword" | i18n }}</span>
</h1>
<div class="right">
<button type="submit" [disabled]="form.loading">
<span [hidden]="form.loading">{{ "submit" | i18n }}</span>
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
</button>
</div>
</header>
<main tabindex="-1">
<app-callout type="warning" title="{{ 'updateMasterPassword' | i18n }}">
{{ masterPasswordWarningText }}
</app-callout>
<app-callout
type="info"
[enforcedPolicyOptions]="enforcedPolicyOptions"
*ngIf="enforcedPolicyOptions"
>
</app-callout>
<div class="box" *ngIf="requireCurrentPassword">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<div class="box-content-row-flex">
<div class="row-main">
<label for="currentMasterPassword">
{{ "currentMasterPass" | i18n }}
</label>
<input
id="currentMasterPassword"
type="password"
name="CurrentMasterPassword"
class="monospaced"
[(ngModel)]="verification.secret"
required
appInputVerbatim
/>
</div>
</div>
</div>
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<div class="box-content-row-flex">
<div class="row-main">
<label for="masterPassword">
{{ "newMasterPass" | i18n }}
<strong class="sub-label text-{{ color }}" *ngIf="text">
{{ text }}
</strong>
</label>
<input
id="masterPassword"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPassword"
class="monospaced"
[(ngModel)]="masterPassword"
required
appInputVerbatim
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword(false)"
[attr.aria-pressed]="showPassword"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
</div>
</div>
<app-password-strength
[password]="masterPassword"
[email]="email"
(passwordStrengthResult)="getStrengthResult($event)"
(passwordScoreColor)="getPasswordScoreText($event)"
>
</app-password-strength>
</div>
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-flex" appBoxRow>
<div class="row-main">
<label for="masterPasswordRetype">{{ "confirmNewMasterPass" | i18n }}</label>
<input
id="masterPasswordRetype"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPasswordRetype"
class="monospaced"
[(ngModel)]="masterPasswordRetype"
required
appInputVerbatim
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword(true)"
[attr.aria-pressed]="showPassword"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
</div>
</div>
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="hint">{{ "masterPassHint" | i18n }}</label>
<input id="hint" type="text" name="Hint" aria-describedby="hintHelp" [(ngModel)]="hint" />
</div>
</div>
<div id="hintHelp" class="box-footer">
{{ "masterPassHintDesc" | i18n }}
</div>
</div>
</main>
</form>

View File

@@ -1,30 +0,0 @@
import { Component } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { UpdateTempPasswordComponent as BaseUpdateTempPasswordComponent } from "@bitwarden/angular/auth/components/update-temp-password.component";
import { postLogoutMessageListener$ } from "./utils/post-logout-message-listener";
@Component({
selector: "app-update-temp-password",
templateUrl: "update-temp-password.component.html",
standalone: false,
})
export class UpdateTempPasswordComponent extends BaseUpdateTempPasswordComponent {
onSuccessfulChangePassword: () => Promise<void> = this.doOnSuccessfulChangePassword.bind(this);
private async doOnSuccessfulChangePassword() {
// start listening for "switchAccountFinish" or "doneLoggingOut"
const messagePromise = firstValueFrom(postLogoutMessageListener$);
this.messagingService.send("logout");
// wait for messages
const command = await messagePromise;
// doneLoggingOut already has a message handler that will navigate us
if (command === "switchAccountFinish") {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/"]);
}
}
}

View File

@@ -32,7 +32,6 @@ import {
RegistrationStartSecondaryComponent,
RegistrationStartSecondaryComponentData,
RegistrationUserAddIcon,
SetPasswordJitComponent,
SsoComponent,
TwoFactorTimeoutIcon,
TwoFactorAuthComponent,
@@ -43,15 +42,13 @@ import {
VaultIcon,
} from "@bitwarden/auth/angular";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, Icons } from "@bitwarden/components";
import { AnonLayoutWrapperData, Icons } from "@bitwarden/components";
import { LockComponent } from "@bitwarden/key-management-ui";
import { AccountSwitcherComponent } from "../auth/popup/account-switching/account-switcher.component";
import { fido2AuthGuard } from "../auth/popup/guards/fido2-auth.guard";
import { SetPasswordComponent } from "../auth/popup/set-password.component";
import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
import { ExtensionDeviceManagementComponent } from "../auth/popup/settings/extension-device-management.component";
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
import { Fido2Component } from "../autofill/popup/fido2/fido2.component";
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
import { BlockedDomainsComponent } from "../autofill/popup/settings/blocked-domains.component";
@@ -180,11 +177,6 @@ const routes: Routes = [
elevation: 1,
} satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData,
},
{
path: "set-password",
component: SetPasswordComponent,
data: { elevation: 1 } satisfies RouteDataProperties,
},
{
path: "remove-password",
component: RemovePasswordComponent,
@@ -337,20 +329,6 @@ const routes: Routes = [
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
},
{
path: "update-temp-password",
component: UpdateTempPasswordComponent,
canActivate: [
canAccessFeature(
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
false,
`/change-password`,
false,
),
authGuard,
],
data: { elevation: 1 } satisfies RouteDataProperties,
},
{
path: "",
component: ExtensionAnonLayoutWrapperComponent,
@@ -398,7 +376,7 @@ const routes: Routes = [
},
{
path: "set-initial-password",
canActivate: [canAccessFeature(FeatureFlag.PM16117_SetInitialPasswordRefactor), authGuard],
canActivate: [authGuard],
component: SetInitialPasswordComponent,
data: {
elevation: 1,
@@ -586,29 +564,7 @@ const routes: Routes = [
component: ChangePasswordComponent,
},
],
canActivate: [
canAccessFeature(FeatureFlag.PM16117_ChangeExistingPasswordRefactor),
authGuard,
],
},
],
},
{
path: "",
component: AnonLayoutWrapperComponent,
children: [
{
path: "set-password-jit",
component: SetPasswordJitComponent,
data: {
pageTitle: {
key: "joinOrganization",
},
pageSubtitle: {
key: "finishJoiningThisOrganizationBySettingAMasterPassword",
},
elevation: 1,
} satisfies RouteDataProperties & AnonLayoutWrapperData,
canActivate: [authGuard],
},
],
},

View File

@@ -26,10 +26,8 @@ import {
import { AccountComponent } from "../auth/popup/account-switching/account.component";
import { CurrentAccountComponent } from "../auth/popup/account-switching/current-account.component";
import { SetPasswordComponent } from "../auth/popup/set-password.component";
import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
import { VaultTimeoutInputComponent } from "../auth/popup/settings/vault-timeout-input.component";
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component";
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
@@ -96,9 +94,7 @@ import "../platform/popup/locales";
AppComponent,
ColorPasswordPipe,
ColorPasswordCountPipe,
SetPasswordComponent,
TabsV2Component,
UpdateTempPasswordComponent,
UserVerificationComponent,
VaultTimeoutInputComponent,
RemovePasswordComponent,

View File

@@ -15,8 +15,6 @@ import {
unauthGuardFn,
} from "@bitwarden/angular/auth/guards";
import { ChangePasswordComponent } from "@bitwarden/angular/auth/password-management/change-password";
import { SetInitialPasswordComponent } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.component";
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
import {
LoginComponent,
LoginSecondaryContentComponent,
@@ -28,7 +26,6 @@ import {
RegistrationStartSecondaryComponent,
RegistrationStartSecondaryComponentData,
RegistrationUserAddIcon,
SetPasswordJitComponent,
UserLockIcon,
VaultIcon,
LoginDecryptionOptionsComponent,
@@ -40,13 +37,10 @@ import {
NewDeviceVerificationComponent,
DeviceVerificationIcon,
} from "@bitwarden/auth/angular";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, Icons } from "@bitwarden/components";
import { LockComponent } from "@bitwarden/key-management-ui";
import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard";
import { SetPasswordComponent } from "../auth/set-password.component";
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component";
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
import { VaultV2Component } from "../vault/app/vault/vault-v2.component";
@@ -105,25 +99,11 @@ const routes: Routes = [
component: VaultV2Component,
canActivate: [authGuard],
},
{ path: "set-password", component: SetPasswordComponent },
{
path: "send",
component: SendComponent,
canActivate: [authGuard],
},
{
path: "update-temp-password",
component: UpdateTempPasswordComponent,
canActivate: [
canAccessFeature(
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
false,
`/change-password`,
false,
),
authGuard,
],
},
{
path: "remove-password",
component: RemovePasswordComponent,
@@ -308,26 +288,6 @@ const routes: Routes = [
},
],
},
{
path: "set-password-jit",
component: SetPasswordJitComponent,
data: {
pageTitle: {
key: "joinOrganization",
},
pageSubtitle: {
key: "finishJoiningThisOrganizationBySettingAMasterPassword",
},
} satisfies AnonLayoutWrapperData,
},
{
path: "set-initial-password",
canActivate: [canAccessFeature(FeatureFlag.PM16117_SetInitialPasswordRefactor), authGuard],
component: SetInitialPasswordComponent,
data: {
maxWidth: "lg",
} satisfies AnonLayoutWrapperData,
},
{
path: "2fa",
canActivate: [unauthGuardFn(), TwoFactorAuthGuard],
@@ -346,10 +306,7 @@ const routes: Routes = [
{
path: "change-password",
component: ChangePasswordComponent,
canActivate: [
canAccessFeature(FeatureFlag.PM16117_ChangeExistingPasswordRefactor),
authGuard,
],
canActivate: [authGuard],
},
],
},

View File

@@ -13,8 +13,6 @@ import { AssignCollectionsComponent } from "@bitwarden/vault";
import { DeleteAccountComponent } from "../auth/delete-account.component";
import { LoginModule } from "../auth/login/login.module";
import { SetPasswordComponent } from "../auth/set-password.component";
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component";
import { SshAgentService } from "../autofill/services/ssh-agent.service";
import { PremiumComponent } from "../billing/app/accounts/premium.component";
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
@@ -57,9 +55,7 @@ import { SharedModule } from "./shared/shared.module";
PremiumComponent,
RemovePasswordComponent,
SearchComponent,
SetPasswordComponent,
SettingsComponent,
UpdateTempPasswordComponent,
VaultTimeoutInputComponent,
],
providers: [SshAgentService],

View File

@@ -1,21 +0,0 @@
import { inject } from "@angular/core";
import {
DefaultSetPasswordJitService,
SetPasswordCredentials,
SetPasswordJitService,
} from "@bitwarden/auth/angular";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
export class DesktopSetPasswordJitService
extends DefaultSetPasswordJitService
implements SetPasswordJitService
{
messagingService = inject(MessagingService);
override async setPassword(credentials: SetPasswordCredentials) {
await super.setPassword(credentials);
this.messagingService.send("redrawMenu");
}
}

View File

@@ -25,7 +25,6 @@ import {
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
import {
LoginComponentService,
SetPasswordJitService,
SsoComponentService,
DefaultSsoComponentService,
TwoFactorAuthDuoComponentService,
@@ -139,7 +138,6 @@ import { NativeMessagingService } from "../../services/native-messaging.service"
import { SearchBarService } from "../layout/search/search-bar.service";
import { DesktopFileDownloadService } from "./desktop-file-download.service";
import { DesktopSetPasswordJitService } from "./desktop-set-password-jit.service";
import { InitService } from "./init.service";
import { NativeMessagingManifestService } from "./native-messaging-manifest.service";
import { DesktopSetInitialPasswordService } from "./set-initial-password/desktop-set-initial-password.service";
@@ -379,21 +377,6 @@ const safeProviders: SafeProvider[] = [
provide: CLIENT_TYPE,
useValue: ClientType.Desktop,
}),
safeProvider({
provide: SetPasswordJitService,
useClass: DesktopSetPasswordJitService,
deps: [
EncryptService,
I18nServiceAbstraction,
KdfConfigService,
KeyService,
MasterPasswordApiService,
InternalMasterPasswordServiceAbstraction,
OrganizationApiServiceAbstraction,
OrganizationUserApiService,
InternalUserDecryptionOptionsServiceAbstraction,
],
}),
safeProvider({
provide: SetInitialPasswordService,
useClass: DesktopSetInitialPasswordService,

View File

@@ -1,169 +0,0 @@
<form id="set-password-page" #form>
<div class="content">
<img class="logo-image" alt="Bitwarden" />
<p class="lead">{{ "setMasterPassword" | i18n }}</p>
<div class="box text-center" *ngIf="syncLoading">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
{{ "loading" | i18n }}
</div>
<div *ngIf="!syncLoading">
<div class="box">
<p
*ngIf="
forceSetPasswordReason ==
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission;
else defaultCardDesc
"
>
{{ "orgPermissionsUpdatedMustSetPassword" | i18n }}
</p>
<ng-template #defaultCardDesc>
<p>{{ "orgRequiresYouToSetPassword" | i18n }}</p>
</ng-template>
<app-callout
type="warning"
title="{{ 'resetPasswordPolicyAutoEnroll' | i18n }}"
*ngIf="resetPasswordAutoEnroll"
>
{{ "resetPasswordAutoEnrollInviteWarning" | i18n }}
</app-callout>
<app-callout
type="info"
[enforcedPolicyOptions]="enforcedPolicyOptions"
*ngIf="enforcedPolicyOptions"
>
</app-callout>
</div>
<form
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
autocomplete="off"
>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<div class="box-content-row-flex">
<div class="row-main">
<label for="masterPassword"
>{{ "masterPass" | i18n }}
<strong class="sub-label text-{{ color }}" *ngIf="text">
{{ text }}
</strong>
</label>
<input
id="masterPassword"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPassword"
class="monospaced"
aria-describedby="masterPasswordHelp"
[(ngModel)]="masterPassword"
required
appInputVerbatim
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-pressed]="showPassword"
(click)="togglePassword(false)"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
</div>
</div>
<app-password-strength
[password]="masterPassword"
[email]="email"
(passwordStrengthResult)="getStrengthResult($event)"
(passwordScoreColor)="getPasswordScoreText($event)"
>
</app-password-strength>
</div>
</div>
<div id="masterPasswordHelp" class="box-footer">
{{ "masterPassDesc" | i18n }}
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<div class="box-content-row-flex">
<div class="row-main">
<label for="masterPasswordRetype">{{ "reTypeMasterPass" | i18n }}</label>
<input
id="masterPasswordRetype"
type="password"
name="MasterPasswordRetype"
class="monospaced"
[(ngModel)]="masterPasswordRetype"
required
appInputVerbatim
autocomplete="new-password"
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-pressed]="showPassword"
(click)="togglePassword(true)"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="box last">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="hint">{{ "masterPassHint" | i18n }}</label>
<input
id="hint"
type="text"
name="Hint"
aria-describedby="hintHelp"
[(ngModel)]="hint"
/>
</div>
</div>
<div id="hintHelp" class="box-footer">
{{ "masterPassHintDesc" | i18n }}
</div>
</div>
<div class="buttons">
<button type="submit" class="btn primary block" [disabled]="form.loading">
<i
*ngIf="form.loading"
class="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span>{{ "submit" | i18n }}</span>
</button>
<button type="button" class="btn block" (click)="logOut()">
<span>{{ "logOut" | i18n }}</span>
</button>
</div>
</form>
</div>
</div>
</form>

View File

@@ -1,111 +0,0 @@
import { Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { SetPasswordComponent as BaseSetPasswordComponent } from "@bitwarden/angular/auth/components/set-password.component";
import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { DialogService, ToastService } from "@bitwarden/components";
import { KdfConfigService, KeyService } from "@bitwarden/key-management";
const BroadcasterSubscriptionId = "SetPasswordComponent";
@Component({
selector: "app-set-password",
templateUrl: "set-password.component.html",
standalone: false,
})
export class SetPasswordComponent extends BaseSetPasswordComponent implements OnInit, OnDestroy {
constructor(
protected accountService: AccountService,
protected dialogService: DialogService,
protected encryptService: EncryptService,
protected i18nService: I18nService,
protected kdfConfigService: KdfConfigService,
protected keyService: KeyService,
protected masterPasswordApiService: MasterPasswordApiService,
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
protected messagingService: MessagingService,
protected organizationApiService: OrganizationApiServiceAbstraction,
protected organizationUserApiService: OrganizationUserApiService,
protected platformUtilsService: PlatformUtilsService,
protected policyApiService: PolicyApiServiceAbstraction,
protected policyService: PolicyService,
protected route: ActivatedRoute,
protected router: Router,
protected ssoLoginService: SsoLoginServiceAbstraction,
protected syncService: SyncService,
protected toastService: ToastService,
protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
private broadcasterService: BroadcasterService,
private ngZone: NgZone,
) {
super(
accountService,
dialogService,
encryptService,
i18nService,
kdfConfigService,
keyService,
masterPasswordApiService,
masterPasswordService,
messagingService,
organizationApiService,
organizationUserApiService,
platformUtilsService,
policyApiService,
policyService,
route,
router,
ssoLoginService,
syncService,
toastService,
userDecryptionOptionsService,
);
}
async ngOnInit() {
await super.ngOnInit();
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message) => {
this.ngZone.run(() => {
switch (message.command) {
case "windowHidden":
this.onWindowHidden();
break;
default:
}
});
});
}
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
onWindowHidden() {
this.showPassword = false;
}
protected async onSetPasswordSuccess(
masterKey: MasterKey,
userKey: [UserKey, EncString],
keyPair: [string, EncString],
): Promise<void> {
await super.onSetPasswordSuccess(masterKey, userKey, keyPair);
this.messagingService.send("redrawMenu");
}
}

View File

@@ -1,136 +0,0 @@
<form id="update-temp-password-page" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="content">
<app-callout type="warning" title="{{ 'updateMasterPassword' | i18n }}">
{{ masterPasswordWarningText }}
</app-callout>
<app-callout
type="info"
[enforcedPolicyOptions]="enforcedPolicyOptions"
*ngIf="enforcedPolicyOptions"
>
</app-callout>
<div class="box" *ngIf="requireCurrentPassword">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<div class="box-content-row-flex">
<div class="row-main">
<label for="currentMasterPassword">
{{ "currentMasterPass" | i18n }}
</label>
<input
id="currentMasterPassword"
type="password"
name="currentMasterPassword"
class="monospaced"
[(ngModel)]="verification.secret"
required
appInputVerbatim
/>
</div>
</div>
</div>
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<div class="box-content-row-flex">
<div class="row-main">
<label for="masterPassword">
{{ "newMasterPass" | i18n }}
<strong class="sub-label text-{{ color }}" *ngIf="text">
{{ text }}
</strong>
</label>
<input
id="masterPassword"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPassword"
class="monospaced"
[(ngModel)]="masterPassword"
required
[appAutofocus]="masterPassword === ''"
appInputVerbatim
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-pressed]="showPassword"
(click)="togglePassword(false)"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
</div>
</div>
<app-password-strength
[password]="masterPassword"
[email]="email"
(passwordStrengthResult)="getStrengthResult($event)"
(passwordScoreColor)="getPasswordScoreText($event)"
>
</app-password-strength>
</div>
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-flex" appBoxRow>
<div class="row-main">
<label for="masterPasswordRetype">{{ "confirmNewMasterPass" | i18n }}</label>
<input
id="masterPasswordRetype"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPasswordRetype"
class="monospaced"
[(ngModel)]="masterPasswordRetype"
required
appInputVerbatim
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-pressed]="showPassword"
(click)="togglePassword(true)"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
</div>
</div>
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="hint">{{ "masterPassHint" | i18n }}</label>
<input id="hint" type="text" name="Hint" aria-describedby="hintHelp" [(ngModel)]="hint" />
</div>
</div>
<div id="hintHelp" class="box-footer">
{{ "masterPassHintDesc" | i18n }}
</div>
</div>
<div class="buttons">
<button type="submit" class="btn primary block" [disabled]="form.loading">
<b [hidden]="form.loading">{{ "submit" | i18n }}</b>
<i class="bwi bwi-spinner bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
</button>
<button type="button" (click)="logOut()" class="btn block">{{ "logOut" | i18n }}</button>
</div>
</div>
</form>

View File

@@ -1,10 +0,0 @@
import { Component } from "@angular/core";
import { UpdateTempPasswordComponent as BaseUpdateTempPasswordComponent } from "@bitwarden/angular/auth/components/update-temp-password.component";
@Component({
selector: "app-update-temp-password",
templateUrl: "update-temp-password.component.html",
standalone: false,
})
export class UpdateTempPasswordComponent extends BaseUpdateTempPasswordComponent {}

View File

@@ -1,7 +1,5 @@
@import "variables.scss";
#lock-page,
#set-password-page,
#remove-password-page {
display: flex;
justify-content: center;
@@ -23,9 +21,6 @@
}
}
#register-page,
#hint-page,
#update-temp-password-page,
#remove-password-page {
padding-top: 20px;
@@ -42,68 +37,6 @@
}
}
#register-page,
#hint-page,
#lock-page,
#update-temp-password-page {
.content {
width: 325px;
transition: width 0.25s linear;
p {
text-align: center;
}
p.lead,
h1 {
font-size: $font-size-large;
text-align: center;
margin-bottom: 20px;
font-weight: normal;
}
.box {
margin-bottom: 20px;
}
.buttons {
&:not(.with-rows),
.buttons-row {
display: flex;
margin-bottom: 10px;
}
&:not(.with-rows),
.buttons-row:last-child {
margin-bottom: 20px;
}
button {
margin-right: 10px;
&:last-child {
margin-right: 0;
}
}
}
.sub-options {
text-align: center;
margin-bottom: 20px;
a {
display: block;
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
}
}
}
}
#set-password-page,
#remove-password-page {
.content {
width: 500px;
@@ -155,35 +88,8 @@
}
}
#register-page,
#update-temp-password-page {
.content {
width: 400px;
}
}
#remove-password-page {
.content > p {
margin-bottom: 20px;
}
}
#login-approval-page {
.section-title {
padding: 20px;
}
.content {
padding: 16px;
.section {
margin-bottom: 30px;
code {
@include themify($themes) {
color: themed("codeColor");
}
}
h4.label {
font-weight: bold;
}
}
}
}

View File

@@ -1,67 +0,0 @@
<form [formGroup]="formGroup" [bitSubmit]="submit">
<bit-dialog [title]="'recoverAccount' | i18n" [subtitle]="data.name">
<ng-container bitDialogContent>
<bit-callout type="warning"
>{{ "resetPasswordLoggedOutWarning" | i18n: loggedOutWarningName }}
</bit-callout>
<auth-password-callout
[policy]="enforcedPolicyOptions"
message="resetPasswordMasterPasswordPolicyInEffect"
*ngIf="enforcedPolicyOptions"
>
</auth-password-callout>
<bit-form-field>
<bit-label>
{{ "newPassword" | i18n }}
</bit-label>
<input
id="newPassword"
bitInput
[type]="showPassword ? 'text' : 'password'"
name="NewPassword"
formControlName="newPassword"
required
appInputVerbatim
autocomplete="new-password"
/>
<button
type="button"
bitIconButton="bwi-generate"
bitSuffix
[appA11yTitle]="'generatePassword' | i18n"
(click)="generatePassword()"
></button>
<button
type="button"
bitSuffix
[bitIconButton]="showPassword ? 'bwi-eye-slash' : 'bwi-eye'"
buttonType="secondary"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword()"
></button>
<button
type="button"
bitSuffix
bitIconButton="bwi-clone"
appA11yTitle="{{ 'copyPassword' | i18n }}"
(click)="copy()"
></button>
</bit-form-field>
<tools-password-strength
[password]="formGroup.value.newPassword"
[email]="data.email"
[showText]="true"
(passwordStrengthScore)="getStrengthScore($event)"
>
</tools-password-strength>
</ng-container>
<ng-container bitDialogFooter>
<button bitButton buttonType="primary" bitFormButton type="submit">
{{ "save" | i18n }}
</button>
<button bitButton buttonType="secondary" bitDialogClose type="button">
{{ "cancel" | i18n }}
</button>
</ng-container>
</bit-dialog>
</form>

View File

@@ -1,223 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, Inject, OnDestroy, OnInit, ViewChild } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { Subject, switchMap, takeUntil } from "rxjs";
import { PasswordStrengthV2Component } from "@bitwarden/angular/tools/password-strength/password-strength-v2.component";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { OrganizationId } from "@bitwarden/common/types/guid";
import {
DIALOG_DATA,
DialogConfig,
DialogRef,
DialogService,
ToastService,
} from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { OrganizationUserResetPasswordService } from "../services/organization-user-reset-password/organization-user-reset-password.service";
/**
* Encapsulates a few key data inputs needed to initiate an account recovery
* process for the organization user in question.
*/
export type ResetPasswordDialogData = {
/**
* The organization user's full name
*/
name: string;
/**
* The organization user's email address
*/
email: string;
/**
* The `organizationUserId` for the user
*/
id: string;
/**
* The organization's `organizationId`
*/
organizationId: OrganizationId;
};
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum ResetPasswordDialogResult {
Ok = "ok",
}
/**
* Used in a dialog for initiating the account recovery process against a
* given organization user. An admin will access this form when they want to
* reset a user's password and log them out of sessions.
*
* @deprecated Use the `AccountRecoveryDialogComponent` instead.
*/
@Component({
selector: "app-reset-password",
templateUrl: "reset-password.component.html",
standalone: false,
})
export class ResetPasswordComponent implements OnInit, OnDestroy {
formGroup = this.formBuilder.group({
newPassword: ["", Validators.required],
});
@ViewChild(PasswordStrengthV2Component) passwordStrengthComponent: PasswordStrengthV2Component;
enforcedPolicyOptions: MasterPasswordPolicyOptions;
showPassword = false;
passwordStrengthScore: number;
private destroy$ = new Subject<void>();
constructor(
@Inject(DIALOG_DATA) protected data: ResetPasswordDialogData,
private resetPasswordService: OrganizationUserResetPasswordService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private passwordGenerationService: PasswordGenerationServiceAbstraction,
private policyService: PolicyService,
private logService: LogService,
private dialogService: DialogService,
private toastService: ToastService,
private formBuilder: FormBuilder,
private dialogRef: DialogRef<ResetPasswordDialogResult>,
private accountService: AccountService,
) {}
async ngOnInit() {
this.accountService.activeAccount$
.pipe(
getUserId,
switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId)),
takeUntil(this.destroy$),
)
.subscribe(
(enforcedPasswordPolicyOptions) =>
(this.enforcedPolicyOptions = enforcedPasswordPolicyOptions),
);
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
get loggedOutWarningName() {
return this.data.name != null ? this.data.name : this.i18nService.t("thisUser");
}
async generatePassword() {
const options = (await this.passwordGenerationService.getOptions())?.[0] ?? {};
this.formGroup.patchValue({
newPassword: await this.passwordGenerationService.generatePassword(options),
});
this.passwordStrengthComponent.updatePasswordStrength(this.formGroup.value.newPassword);
}
togglePassword() {
this.showPassword = !this.showPassword;
document.getElementById("newPassword").focus();
}
copy() {
const value = this.formGroup.value.newPassword;
if (value == null) {
return;
}
this.platformUtilsService.copyToClipboard(value, { window: window });
this.toastService.showToast({
variant: "info",
title: null,
message: this.i18nService.t("valueCopied", this.i18nService.t("password")),
});
}
submit = async () => {
// Validation
if (this.formGroup.value.newPassword == null || this.formGroup.value.newPassword === "") {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("masterPasswordRequired"),
});
return false;
}
if (this.formGroup.value.newPassword.length < Utils.minimumPasswordLength) {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("masterPasswordMinlength", Utils.minimumPasswordLength),
});
return false;
}
if (
this.enforcedPolicyOptions != null &&
!this.policyService.evaluateMasterPassword(
this.passwordStrengthScore,
this.formGroup.value.newPassword,
this.enforcedPolicyOptions,
)
) {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("masterPasswordPolicyRequirementsNotMet"),
});
return;
}
if (this.passwordStrengthScore < 3) {
const result = await this.dialogService.openSimpleDialog({
title: { key: "weakMasterPassword" },
content: { key: "weakMasterPasswordDesc" },
type: "warning",
});
if (!result) {
return false;
}
}
try {
await this.resetPasswordService.resetMasterPassword(
this.formGroup.value.newPassword,
this.data.email,
this.data.id,
this.data.organizationId,
);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("resetPasswordSuccess"),
});
} catch (e) {
this.logService.error(e);
}
this.dialogRef.close(ResetPasswordDialogResult.Ok);
};
getStrengthScore(result: number) {
this.passwordStrengthScore = result;
}
static open = (dialogService: DialogService, input: DialogConfig<ResetPasswordDialogData>) => {
return dialogService.open<ResetPasswordDialogResult>(ResetPasswordComponent, input);
};
}

View File

@@ -86,10 +86,6 @@ import {
openUserAddEditDialog,
} from "./components/member-dialog";
import { isFixedSeatPlan } from "./components/member-dialog/validators/org-seat-limit-reached.validator";
import {
ResetPasswordComponent,
ResetPasswordDialogResult,
} from "./components/reset-password.component";
import { DeleteManagedMemberWarningService } from "./services/delete-managed-member/delete-managed-member-warning.service";
import { OrganizationUserService } from "./services/organization-user/organization-user.service";
@@ -767,52 +763,32 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
}
async resetPassword(user: OrganizationUserView) {
const changePasswordRefactorFlag = await this.configService.getFeatureFlag(
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
);
if (changePasswordRefactorFlag) {
if (!user || !user.email || !user.id) {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("orgUserDetailsNotFound"),
});
this.logService.error("Org user details not found when attempting account recovery");
return;
}
const dialogRef = AccountRecoveryDialogComponent.open(this.dialogService, {
data: {
name: this.userNamePipe.transform(user),
email: user.email,
organizationId: this.organization.id as OrganizationId,
organizationUserId: user.id,
},
if (!user || !user.email || !user.id) {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("orgUserDetailsNotFound"),
});
const result = await lastValueFrom(dialogRef.closed);
if (result === AccountRecoveryDialogResultType.Ok) {
await this.load();
}
this.logService.error("Org user details not found when attempting account recovery");
return;
}
const dialogRef = ResetPasswordComponent.open(this.dialogService, {
const dialogRef = AccountRecoveryDialogComponent.open(this.dialogService, {
data: {
name: this.userNamePipe.transform(user),
email: user != null ? user.email : null,
email: user.email,
organizationId: this.organization.id as OrganizationId,
id: user != null ? user.id : null,
organizationUserId: user.id,
},
});
const result = await lastValueFrom(dialogRef.closed);
if (result === ResetPasswordDialogResult.Ok) {
if (result === AccountRecoveryDialogResultType.Ok) {
await this.load();
}
return;
}
protected async removeUserConfirmationDialog(user: OrganizationUserView) {

View File

@@ -16,7 +16,6 @@ import { BulkRemoveDialogComponent } from "./components/bulk/bulk-remove-dialog.
import { BulkRestoreRevokeComponent } from "./components/bulk/bulk-restore-revoke.component";
import { BulkStatusComponent } from "./components/bulk/bulk-status.component";
import { UserDialogModule } from "./components/member-dialog";
import { ResetPasswordComponent } from "./components/reset-password.component";
import { MembersRoutingModule } from "./members-routing.module";
import { MembersComponent } from "./members.component";
@@ -39,7 +38,6 @@ import { MembersComponent } from "./members.component";
BulkRestoreRevokeComponent,
BulkStatusComponent,
MembersComponent,
ResetPasswordComponent,
BulkDeleteDialogComponent,
],
})

View File

@@ -3,7 +3,6 @@ export * from "./login";
export * from "./login-decryption-options";
export * from "./webauthn-login";
export * from "./password-management";
export * from "./set-password-jit";
export * from "./registration";
export * from "./two-factor-auth";
export * from "./link-sso.service";

View File

@@ -1,6 +1,5 @@
import { TestBed } from "@angular/core/testing";
import { MockProxy, mock } from "jest-mock-extended";
import { of } from "rxjs";
import { DefaultLoginComponentService } from "@bitwarden/auth/angular";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
@@ -138,8 +137,8 @@ describe("WebLoginComponentService", () => {
resetPasswordPolicyEnabled,
]);
internalPolicyService.masterPasswordPolicyOptions$.mockReturnValue(
of(masterPasswordPolicyOptions),
internalPolicyService.combinePoliciesIntoMasterPasswordPolicyOptions.mockReturnValue(
masterPasswordPolicyOptions,
);
const result = await service.getOrgPoliciesFromOrgInvite();

View File

@@ -2,7 +2,6 @@
// @ts-strict-ignore
import { Injectable } from "@angular/core";
import { Router } from "@angular/router";
import { firstValueFrom, switchMap } from "rxjs";
import {
DefaultLoginComponentService,
@@ -11,13 +10,10 @@ import {
} from "@bitwarden/auth/angular";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
@@ -99,23 +95,8 @@ export class WebLoginComponentService
const isPolicyAndAutoEnrollEnabled =
resetPasswordPolicy[1] && resetPasswordPolicy[0].autoEnrollEnabled;
let enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions;
if (
await this.configService.getFeatureFlag(FeatureFlag.PM16117_ChangeExistingPasswordRefactor)
) {
enforcedPasswordPolicyOptions =
this.policyService.combinePoliciesIntoMasterPasswordPolicyOptions(policies);
} else {
enforcedPasswordPolicyOptions = await firstValueFrom(
this.accountService.activeAccount$.pipe(
getUserId,
switchMap((userId) =>
this.policyService.masterPasswordPolicyOptions$(userId, policies),
),
),
);
}
const enforcedPasswordPolicyOptions =
this.policyService.combinePoliciesIntoMasterPasswordPolicyOptions(policies);
return {
policies,

View File

@@ -1 +0,0 @@
export * from "./web-set-password-jit.service";

View File

@@ -1,27 +0,0 @@
import { inject } from "@angular/core";
import {
DefaultSetPasswordJitService,
SetPasswordCredentials,
SetPasswordJitService,
} from "@bitwarden/auth/angular";
import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service";
import { RouterService } from "../../../../core/router.service";
export class WebSetPasswordJitService
extends DefaultSetPasswordJitService
implements SetPasswordJitService
{
routerService = inject(RouterService);
organizationInviteService = inject(OrganizationInviteService);
override async setPassword(credentials: SetPasswordCredentials) {
await super.setPassword(credentials);
// SSO JIT accepts org invites when setting their MP, meaning
// we can clear the deep linked url for accepting it.
await this.routerService.getAndClearLoginRedirectUrl();
await this.organizationInviteService.clearOrganizationInvitation();
}
}

View File

@@ -1,130 +0,0 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate autocomplete="off">
<div class="row justify-content-md-center mt-5">
<div class="col-5">
<p class="lead text-center mb-4">{{ "setMasterPassword" | i18n }}</p>
<div class="card d-block">
<div class="card-body text-center" *ngIf="syncLoading">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
{{ "loading" | i18n }}
</div>
<div class="card-body" *ngIf="!syncLoading">
<p
*ngIf="
forceSetPasswordReason ==
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission;
else defaultCardDesc
"
>
{{ "orgPermissionsUpdatedMustSetPassword" | i18n }}
</p>
<ng-template #defaultCardDesc>
<p>{{ "orgRequiresYouToSetPassword" | i18n }}</p>
</ng-template>
<app-callout
type="warning"
title="{{ 'resetPasswordPolicyAutoEnroll' | i18n }}"
*ngIf="resetPasswordAutoEnroll"
>
{{ "resetPasswordAutoEnrollInviteWarning" | i18n }}
</app-callout>
<div class="form-group">
<auth-password-callout [policy]="enforcedPolicyOptions" *ngIf="enforcedPolicyOptions">
</auth-password-callout>
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
<div class="d-flex">
<div class="w-100">
<input
id="masterPassword"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPasswordHash"
class="text-monospace form-control mb-1"
[(ngModel)]="masterPassword"
required
appInputVerbatim
/>
<app-password-strength
[password]="masterPassword"
[email]="email"
[showText]="true"
(passwordStrengthResult)="getStrengthResult($event)"
>
</app-password-strength>
</div>
<div>
<button
type="button"
class="ml-1 btn btn-link"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword(false)"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
<div class="progress-bar invisible"></div>
</div>
</div>
<small class="form-text text-muted">{{ "masterPassDesc" | i18n }}</small>
</div>
<div class="form-group">
<label for="masterPasswordRetype">{{ "reTypeMasterPass" | i18n }}</label>
<div class="d-flex">
<input
id="masterPasswordRetype"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPasswordRetype"
class="text-monospace form-control"
[(ngModel)]="masterPasswordRetype"
required
appInputVerbatim
/>
<button
type="button"
class="ml-1 btn btn-link"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword(true)"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
</div>
</div>
<div class="form-group">
<label for="hint">{{ "masterPassHint" | i18n }}</label>
<input id="hint" class="form-control" type="text" name="Hint" [(ngModel)]="hint" />
<small class="form-text text-muted">{{ "masterPassHintDesc" | i18n }}</small>
</div>
<hr />
<div class="d-flex">
<button
type="submit"
class="btn btn-primary btn-block btn-submit"
[disabled]="form.loading"
>
<i
class="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span>{{ "submit" | i18n }}</span>
</button>
<button
type="button"
class="btn btn-outline-secondary btn-block ml-2 mt-0"
(click)="logOut()"
>
{{ "logOut" | i18n }}
</button>
</div>
</div>
</div>
</div>
</div>
</form>

View File

@@ -1,30 +0,0 @@
import { Component, inject } from "@angular/core";
import { SetPasswordComponent as BaseSetPasswordComponent } from "@bitwarden/angular/auth/components/set-password.component";
import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
import { RouterService } from "../core";
@Component({
selector: "app-set-password",
templateUrl: "set-password.component.html",
standalone: false,
})
export class SetPasswordComponent extends BaseSetPasswordComponent {
routerService = inject(RouterService);
organizationInviteService = inject(OrganizationInviteService);
protected override async onSetPasswordSuccess(
masterKey: MasterKey,
userKey: [UserKey, EncString],
keyPair: [string, EncString],
): Promise<void> {
await super.onSetPasswordSuccess(masterKey, userKey, keyPair);
// SSO JIT accepts org invites when setting their MP, meaning
// we can clear the deep linked url for accepting it.
await this.routerService.getAndClearLoginRedirectUrl();
await this.organizationInviteService.clearOrganizationInvitation();
}
}

View File

@@ -1,129 +0,0 @@
<div class="tabbed-header">
<h1>{{ "changeMasterPassword" | i18n }}</h1>
</div>
<bit-callout type="warning">{{ "loggedOutWarning" | i18n }}</bit-callout>
<auth-password-callout [policy]="enforcedPolicyOptions" *ngIf="enforcedPolicyOptions">
</auth-password-callout>
<form
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
autocomplete="off"
class="tw-mb-14"
>
<div class="row">
<div class="col-6">
<div class="form-group">
<label for="currentMasterPassword">{{ "currentMasterPass" | i18n }}</label>
<input
id="currentMasterPassword"
type="password"
name="MasterPasswordHash"
class="form-control"
[(ngModel)]="currentMasterPassword"
required
appInputVerbatim
/>
</div>
</div>
</div>
<div class="row">
<div class="col-6">
<div class="form-group">
<label for="newMasterPassword">{{ "newMasterPass" | i18n }}</label>
<input
id="newMasterPassword"
type="password"
name="NewMasterPasswordHash"
class="form-control mb-1"
[(ngModel)]="masterPassword"
required
appInputVerbatim
autocomplete="new-password"
/>
<bit-hint>
<span class="tw-font-semibold">{{ "important" | i18n }}</span>
{{ "masterPassImportant" | i18n }} {{ characterMinimumMessage }}
</bit-hint>
<app-password-strength
[password]="masterPassword"
[email]="email"
[showText]="true"
(passwordStrengthResult)="getStrengthResult($event)"
>
</app-password-strength>
</div>
</div>
<div class="col-6">
<div class="form-group">
<label for="masterPasswordRetype">{{ "confirmNewMasterPass" | i18n }}</label>
<input
id="masterPasswordRetype"
type="password"
name="MasterPasswordRetype"
class="form-control"
[(ngModel)]="masterPasswordRetype"
required
appInputVerbatim
autocomplete="new-password"
/>
</div>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="checkForBreaches"
name="checkForBreaches"
[(ngModel)]="checkForBreaches"
/>
<label class="form-check-label" for="checkForBreaches">
{{ "checkForBreaches" | i18n }}
</label>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="rotateUserKey"
name="RotateUserKey"
[(ngModel)]="rotateUserKey"
(change)="rotateUserKeyClicked()"
/>
<label class="form-check-label" for="rotateUserKey">
{{ "rotateAccountEncKey" | i18n }}
</label>
<a
href="https://bitwarden.com/help/account-encryption-key/#rotate-your-encryption-key"
target="_blank"
rel="noreferrer"
appA11yTitle="{{ 'impactOfRotatingYourEncryptionKey' | i18n }}"
>
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
</a>
</div>
</div>
<div class="form-group">
<label for="masterPasswordHint">{{ "newMasterPassHint" | i18n }}</label>
<input
id="masterPasswordHint"
class="form-control"
maxlength="50"
type="text"
name="MasterPasswordHint"
[(ngModel)]="masterPasswordHint"
/>
</div>
<button type="submit" buttonType="primary" bitButton [loading]="loading">
{{ "changeMasterPassword" | i18n }}
</button>
</form>
<app-webauthn-login-settings></app-webauthn-login-settings>

View File

@@ -1,258 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnDestroy, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { firstValueFrom, map } from "rxjs";
import { ChangePasswordComponent as BaseChangePasswordComponent } from "@bitwarden/angular/auth/components/change-password.component";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { DialogService, ToastService } from "@bitwarden/components";
import { KdfConfigService, KeyService } from "@bitwarden/key-management";
import { UserKeyRotationService } from "../../key-management/key-rotation/user-key-rotation.service";
/**
* @deprecated use the auth `PasswordSettingsComponent` instead
*/
@Component({
selector: "app-change-password",
templateUrl: "change-password.component.html",
standalone: false,
})
export class ChangePasswordComponent
extends BaseChangePasswordComponent
implements OnInit, OnDestroy
{
loading = false;
rotateUserKey = false;
currentMasterPassword: string;
masterPasswordHint: string;
checkForBreaches = true;
characterMinimumMessage = "";
constructor(
private auditService: AuditService,
private cipherService: CipherService,
private keyRotationService: UserKeyRotationService,
private masterPasswordApiService: MasterPasswordApiService,
private router: Router,
private syncService: SyncService,
private userVerificationService: UserVerificationService,
protected accountService: AccountService,
protected dialogService: DialogService,
protected i18nService: I18nService,
protected kdfConfigService: KdfConfigService,
protected keyService: KeyService,
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
protected messagingService: MessagingService,
protected platformUtilsService: PlatformUtilsService,
protected policyService: PolicyService,
protected toastService: ToastService,
) {
super(
accountService,
dialogService,
i18nService,
kdfConfigService,
keyService,
masterPasswordService,
messagingService,
platformUtilsService,
policyService,
toastService,
);
}
async ngOnInit() {
if (!(await this.userVerificationService.hasMasterPassword())) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/settings/security/two-factor"]);
}
await super.ngOnInit();
this.characterMinimumMessage = this.i18nService.t("characterMinimum", this.minimumLength);
}
async rotateUserKeyClicked() {
if (this.rotateUserKey) {
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const ciphers = await this.cipherService.getAllDecrypted(activeUserId);
let hasOldAttachments = false;
if (ciphers != null) {
for (let i = 0; i < ciphers.length; i++) {
if (ciphers[i].organizationId == null && ciphers[i].hasOldAttachments) {
hasOldAttachments = true;
break;
}
}
}
if (hasOldAttachments) {
const learnMore = await this.dialogService.openSimpleDialog({
title: { key: "warning" },
content: { key: "oldAttachmentsNeedFixDesc" },
acceptButtonText: { key: "learnMore" },
cancelButtonText: { key: "close" },
type: "warning",
});
if (learnMore) {
this.platformUtilsService.launchUri(
"https://bitwarden.com/help/attachments/#add-storage-space",
);
}
this.rotateUserKey = false;
return;
}
const result = await this.dialogService.openSimpleDialog({
title: { key: "rotateEncKeyTitle" },
content:
this.i18nService.t("updateEncryptionKeyWarning") +
" " +
this.i18nService.t("updateEncryptionKeyAccountExportWarning") +
" " +
this.i18nService.t("rotateEncKeyConfirmation"),
type: "warning",
});
if (!result) {
this.rotateUserKey = false;
}
}
}
async submit() {
this.loading = true;
if (this.currentMasterPassword == null || this.currentMasterPassword === "") {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("masterPasswordRequired"),
});
this.loading = false;
return;
}
if (
this.masterPasswordHint != null &&
this.masterPasswordHint.toLowerCase() === this.masterPassword.toLowerCase()
) {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("hintEqualsPassword"),
});
this.loading = false;
return;
}
this.leakedPassword = false;
if (this.checkForBreaches) {
this.leakedPassword = (await this.auditService.passwordLeaked(this.masterPassword)) > 0;
}
if (!(await this.strongPassword())) {
this.loading = false;
return;
}
try {
if (this.rotateUserKey) {
await this.syncService.fullSync(true);
const user = await firstValueFrom(this.accountService.activeAccount$);
await this.keyRotationService.rotateUserKeyMasterPasswordAndEncryptedData(
this.currentMasterPassword,
this.masterPassword,
user,
this.masterPasswordHint,
);
} else {
await this.updatePassword(this.masterPassword);
}
} catch (e) {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: e.message,
});
} finally {
this.loading = false;
}
}
// todo: move this to a service
// https://bitwarden.atlassian.net/browse/PM-17108
private async updatePassword(newMasterPassword: string) {
const currentMasterPassword = this.currentMasterPassword;
const { userId, email } = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => ({ userId: a?.id, email: a?.email }))),
);
const kdfConfig = await firstValueFrom(this.kdfConfigService.getKdfConfig$(userId));
const currentMasterKey = await this.keyService.makeMasterKey(
currentMasterPassword,
email,
kdfConfig,
);
const decryptedUserKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
currentMasterKey,
userId,
);
if (decryptedUserKey == null) {
this.toastService.showToast({
variant: "error",
title: null,
message: this.i18nService.t("invalidMasterPassword"),
});
return;
}
const newMasterKey = await this.keyService.makeMasterKey(newMasterPassword, email, kdfConfig);
const newMasterKeyEncryptedUserKey = await this.keyService.encryptUserKeyWithMasterKey(
newMasterKey,
decryptedUserKey,
);
const request = new PasswordRequest();
request.masterPasswordHash = await this.keyService.hashMasterKey(
this.currentMasterPassword,
currentMasterKey,
);
request.masterPasswordHint = this.masterPasswordHint;
request.newMasterPasswordHash = await this.keyService.hashMasterKey(
newMasterPassword,
newMasterKey,
);
request.key = newMasterKeyEncryptedUserKey[1].encryptedString;
try {
await this.masterPasswordApiService.postPassword(request);
this.toastService.showToast({
variant: "success",
message: this.i18nService.t("masterPasswordChanged"),
});
this.messagingService.send("logout");
} catch {
this.toastService.showToast({
variant: "error",
title: null,
message: this.i18nService.t("errorOccurred"),
});
}
}
}

View File

@@ -10,8 +10,6 @@ import { OrganizationManagementPreferencesService } from "@bitwarden/common/admi
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
@@ -40,10 +38,6 @@ import {
EmergencyAccessTakeoverDialogComponent,
EmergencyAccessTakeoverDialogResultType,
} from "./takeover/emergency-access-takeover-dialog.component";
import {
EmergencyAccessTakeoverComponent,
EmergencyAccessTakeoverResultType,
} from "./takeover/emergency-access-takeover.component";
@Component({
selector: "emergency-access",
@@ -75,7 +69,6 @@ export class EmergencyAccessComponent implements OnInit {
private toastService: ToastService,
private apiService: ApiService,
private accountService: AccountService,
private configService: ConfigService,
) {
this.canAccessPremium$ = this.accountService.activeAccount$.pipe(
switchMap((account) =>
@@ -292,60 +285,36 @@ export class EmergencyAccessComponent implements OnInit {
}
takeover = async (details: GrantorEmergencyAccess) => {
const changePasswordRefactorFlag = await this.configService.getFeatureFlag(
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
);
if (changePasswordRefactorFlag) {
if (!details || !details.email || !details.id) {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("grantorDetailsNotFound"),
});
this.logService.error(
"Grantor details not found when attempting emergency access takeover",
);
return;
}
const grantorName = this.userNamePipe.transform(details);
const dialogRef = EmergencyAccessTakeoverDialogComponent.open(this.dialogService, {
data: {
grantorName,
grantorEmail: details.email,
emergencyAccessId: details.id,
},
if (!details || !details.email || !details.id) {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("grantorDetailsNotFound"),
});
const result = await lastValueFrom(dialogRef.closed);
if (result === EmergencyAccessTakeoverDialogResultType.Done) {
this.toastService.showToast({
variant: "success",
title: "",
message: this.i18nService.t("passwordResetFor", grantorName),
});
}
this.logService.error("Grantor details not found when attempting emergency access takeover");
return;
}
const dialogRef = EmergencyAccessTakeoverComponent.open(this.dialogService, {
const grantorName = this.userNamePipe.transform(details);
const dialogRef = EmergencyAccessTakeoverDialogComponent.open(this.dialogService, {
data: {
name: this.userNamePipe.transform(details),
email: details.email,
emergencyAccessId: details.id ?? null,
grantorName,
grantorEmail: details.email,
emergencyAccessId: details.id,
},
});
const result = await lastValueFrom(dialogRef.closed);
if (result === EmergencyAccessTakeoverResultType.Done) {
if (result === EmergencyAccessTakeoverDialogResultType.Done) {
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("passwordResetFor", this.userNamePipe.transform(details)),
title: "",
message: this.i18nService.t("passwordResetFor", grantorName),
});
}
return;
};
private removeGrantee(details: GranteeEmergencyAccess) {

View File

@@ -1,54 +0,0 @@
<form [formGroup]="takeoverForm" [bitSubmit]="submit">
<bit-dialog dialogSize="large">
<span bitDialogTitle>
{{ "takeover" | i18n }}
<small class="tw-text-muted" *ngIf="params.name">{{ params.name }}</small>
</span>
<div bitDialogContent>
<bit-callout type="warning">{{ "loggedOutWarning" | i18n }}</bit-callout>
<auth-password-callout [policy]="enforcedPolicyOptions" *ngIf="enforcedPolicyOptions">
</auth-password-callout>
<div class="tw-w-full tw-flex tw-gap-4">
<div class="tw-relative tw-flex-1">
<bit-form-field disableMargin class="tw-mb-2">
<bit-label>{{ "newMasterPass" | i18n }}</bit-label>
<input
bitInput
type="password"
autocomplete="new-password"
formControlName="masterPassword"
/>
<button type="button" bitSuffix bitIconButton bitPasswordInputToggle></button>
</bit-form-field>
<app-password-strength
[password]="takeoverForm.value.masterPassword"
[email]="email"
[showText]="true"
(passwordStrengthResult)="getStrengthResult($event)"
>
</app-password-strength>
</div>
<div class="tw-relative tw-flex-1">
<bit-form-field disableMargin class="tw-mb-2">
<bit-label>{{ "confirmNewMasterPass" | i18n }}</bit-label>
<input
bitInput
type="password"
autocomplete="new-password"
formControlName="masterPasswordRetype"
/>
<button type="button" bitSuffix bitIconButton bitPasswordInputToggle></button>
</bit-form-field>
</div>
</div>
</div>
<ng-container bitDialogFooter>
<button type="submit" bitButton bitFormButton buttonType="primary">
{{ "save" | i18n }}
</button>
<button bitButton bitFormButton type="button" buttonType="secondary" bitDialogClose>
{{ "cancel" | i18n }}
</button>
</ng-container>
</bit-dialog>
</form>

View File

@@ -1,145 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnDestroy, OnInit, Inject, Input } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { switchMap, takeUntil } from "rxjs";
import { ChangePasswordComponent } from "@bitwarden/angular/auth/components/change-password.component";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import {
DialogConfig,
DialogRef,
DIALOG_DATA,
DialogService,
ToastService,
} from "@bitwarden/components";
import { KdfType, KdfConfigService, KeyService } from "@bitwarden/key-management";
import { EmergencyAccessService } from "../../../emergency-access";
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum EmergencyAccessTakeoverResultType {
Done = "done",
}
type EmergencyAccessTakeoverDialogData = {
/** display name of the account requesting emergency access takeover */
name: string;
/** email of the account requesting emergency access takeover */
email: string;
/** traces a unique emergency request */
emergencyAccessId: string;
};
@Component({
selector: "emergency-access-takeover",
templateUrl: "emergency-access-takeover.component.html",
standalone: false,
})
export class EmergencyAccessTakeoverComponent
extends ChangePasswordComponent
implements OnInit, OnDestroy
{
@Input() kdf: KdfType;
@Input() kdfIterations: number;
takeoverForm = this.formBuilder.group({
masterPassword: ["", [Validators.required]],
masterPasswordRetype: ["", [Validators.required]],
});
constructor(
@Inject(DIALOG_DATA) protected params: EmergencyAccessTakeoverDialogData,
private formBuilder: FormBuilder,
i18nService: I18nService,
keyService: KeyService,
messagingService: MessagingService,
platformUtilsService: PlatformUtilsService,
policyService: PolicyService,
private emergencyAccessService: EmergencyAccessService,
private logService: LogService,
dialogService: DialogService,
private dialogRef: DialogRef<EmergencyAccessTakeoverResultType>,
kdfConfigService: KdfConfigService,
masterPasswordService: InternalMasterPasswordServiceAbstraction,
accountService: AccountService,
protected toastService: ToastService,
) {
super(
accountService,
dialogService,
i18nService,
kdfConfigService,
keyService,
masterPasswordService,
messagingService,
platformUtilsService,
policyService,
toastService,
);
}
async ngOnInit() {
const policies = await this.emergencyAccessService.getGrantorPolicies(
this.params.emergencyAccessId,
);
this.accountService.activeAccount$
.pipe(
getUserId,
switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId, policies)),
takeUntil(this.destroy$),
)
.subscribe((enforcedPolicyOptions) => (this.enforcedPolicyOptions = enforcedPolicyOptions));
}
ngOnDestroy(): void {
super.ngOnDestroy();
}
submit = async () => {
if (this.takeoverForm.invalid) {
this.takeoverForm.markAllAsTouched();
return;
}
this.masterPassword = this.takeoverForm.get("masterPassword").value;
this.masterPasswordRetype = this.takeoverForm.get("masterPasswordRetype").value;
if (!(await this.strongPassword())) {
return;
}
try {
await this.emergencyAccessService.takeover(
this.params.emergencyAccessId,
this.masterPassword,
this.params.email,
);
} catch (e) {
this.logService.error(e);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("unexpectedError"),
});
}
this.dialogRef.close(EmergencyAccessTakeoverResultType.Done);
};
/**
* Strongly typed helper to open a EmergencyAccessTakeoverComponent
* @param dialogService Instance of the dialog service that will be used to open the dialog
* @param config Configuration for the dialog
*/
static open = (
dialogService: DialogService,
config: DialogConfig<EmergencyAccessTakeoverDialogData>,
) => {
return dialogService.open<EmergencyAccessTakeoverResultType>(
EmergencyAccessTakeoverComponent,
config,
);
};
}

View File

@@ -2,11 +2,9 @@ import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { DeviceManagementComponent } from "@bitwarden/angular/auth/device-management/device-management.component";
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ChangePasswordComponent } from "../change-password.component";
import { TwoFactorSetupComponent } from "../two-factor/two-factor-setup.component";
import { DeviceManagementOldComponent } from "./device-management-old.component";
@@ -21,30 +19,9 @@ const routes: Routes = [
data: { titleId: "security" },
children: [
{ path: "", pathMatch: "full", redirectTo: "password" },
{
path: "change-password",
component: ChangePasswordComponent,
canActivate: [
canAccessFeature(
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
false,
"/settings/security/password",
false,
),
],
data: { titleId: "masterPassword" },
},
{
path: "password",
component: PasswordSettingsComponent,
canActivate: [
canAccessFeature(
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
true,
"/settings/security/change-password",
false,
),
],
data: { titleId: "masterPassword" },
},
{

View File

@@ -1,8 +1,6 @@
import { Component, OnInit } from "@angular/core";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { HeaderModule } from "../../../layouts/header/header.module";
import { SharedModule } from "../../../shared";
@@ -13,21 +11,11 @@ import { SharedModule } from "../../../shared";
})
export class SecurityComponent implements OnInit {
showChangePassword = true;
changePasswordRoute = "change-password";
changePasswordRoute = "password";
constructor(
private userVerificationService: UserVerificationService,
private configService: ConfigService,
) {}
constructor(private userVerificationService: UserVerificationService) {}
async ngOnInit() {
this.showChangePassword = await this.userVerificationService.hasMasterPassword();
const changePasswordRefreshFlag = await this.configService.getFeatureFlag(
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
);
if (changePasswordRefreshFlag) {
this.changePasswordRoute = "password";
}
}
}

View File

@@ -6,7 +6,6 @@ import { UserKeyRotationModule } from "../../key-management/key-rotation/user-ke
import { SharedModule } from "../../shared";
import { EmergencyAccessModule } from "../emergency-access";
import { ChangePasswordComponent } from "./change-password.component";
import { WebauthnLoginSettingsModule } from "./webauthn-login-settings";
@NgModule({
@@ -17,8 +16,8 @@ import { WebauthnLoginSettingsModule } from "./webauthn-login-settings";
PasswordCalloutComponent,
UserKeyRotationModule,
],
declarations: [ChangePasswordComponent],
declarations: [],
providers: [],
exports: [ChangePasswordComponent],
exports: [],
})
export class AuthSettingsModule {}

View File

@@ -1,90 +0,0 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate autocomplete="off">
<div class="row justify-content-md-center mt-5">
<div class="col-4">
<p class="lead text-center mb-4">{{ "updateMasterPassword" | i18n }}</p>
<div class="card d-block">
<div class="card-body">
<app-callout type="warning">{{ "masterPasswordInvalidWarning" | i18n }} </app-callout>
<auth-password-callout
[policy]="enforcedPolicyOptions"
*ngIf="enforcedPolicyOptions"
></auth-password-callout>
<form
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
autocomplete="off"
>
<div class="row">
<div class="col-6">
<div class="form-group">
<label for="currentMasterPassword">{{ "currentMasterPass" | i18n }}</label>
<input
id="currentMasterPassword"
type="password"
name="MasterPasswordHash"
class="form-control"
[(ngModel)]="currentMasterPassword"
required
appInputVerbatim
/>
</div>
</div>
</div>
<div class="row">
<div class="col-6">
<div class="form-group">
<label for="newMasterPassword">{{ "newMasterPass" | i18n }}</label>
<input
id="newMasterPassword"
type="password"
name="NewMasterPasswordHash"
class="form-control mb-1"
[(ngModel)]="masterPassword"
required
appInputVerbatim
autocomplete="new-password"
/>
<app-password-strength
[password]="masterPassword"
[email]="email"
[showText]="true"
(passwordStrengthResult)="getStrengthResult($event)"
></app-password-strength>
</div>
</div>
<div class="col-6">
<div class="form-group">
<label for="masterPasswordRetype">{{ "confirmNewMasterPass" | i18n }}</label>
<input
id="masterPasswordRetype"
type="password"
name="MasterPasswordRetype"
class="form-control"
[(ngModel)]="masterPasswordRetype"
required
appInputVerbatim
autocomplete="new-password"
/>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i
class="fa fa-spinner fa-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span>{{ "changeMasterPassword" | i18n }}</span>
</button>
<button (click)="cancel()" type="button" class="btn btn-outline-secondary">
<span>{{ "cancel" | i18n }}</span>
</button>
</form>
</div>
</div>
</div>
</div>
</form>

View File

@@ -1,24 +0,0 @@
import { Component, inject } from "@angular/core";
import { UpdatePasswordComponent as BaseUpdatePasswordComponent } from "@bitwarden/angular/auth/components/update-password.component";
import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service";
import { RouterService } from "../core";
@Component({
selector: "app-update-password",
templateUrl: "update-password.component.html",
standalone: false,
})
export class UpdatePasswordComponent extends BaseUpdatePasswordComponent {
private routerService = inject(RouterService);
private organizationInviteService = inject(OrganizationInviteService);
override async cancel() {
// clearing the login redirect url so that the user
// does not join the organization if they cancel
await this.routerService.getAndClearLoginRedirectUrl();
await this.organizationInviteService.clearOrganizationInvitation();
await super.cancel();
}
}

View File

@@ -1,96 +0,0 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate autocomplete="off">
<div class="tw-mt-12 tw-flex tw-justify-center">
<div class="tw-w-1/3">
<h1 bitTypography="h1" class="tw-mb-4 tw-text-center">{{ "updateMasterPassword" | i18n }}</h1>
<div
class="tw-block tw-rounded tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-8"
>
<app-callout type="warning">{{ masterPasswordWarningText }} </app-callout>
<auth-password-callout [policy]="enforcedPolicyOptions" *ngIf="enforcedPolicyOptions">
</auth-password-callout>
<bit-form-field *ngIf="requireCurrentPassword">
<bit-label>{{ "currentMasterPass" | i18n }}</bit-label>
<input
bitInput
type="password"
appInputVerbatim
required
[(ngModel)]="verification.secret"
name="currentMasterPassword"
id="currentMasterPassword"
[appAutofocus]="requireCurrentPassword"
/>
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
</bit-form-field>
<div class="tw-mb-4">
<bit-form-field class="!tw-mb-1">
<bit-label>{{ "newMasterPass" | i18n }}</bit-label>
<input
bitInput
type="password"
appInputVerbatim
required
[(ngModel)]="masterPassword"
name="masterPassword"
id="masterPassword"
/>
<button
type="button"
bitIconButton
bitSuffix
bitPasswordInputToggle
[(toggled)]="showPassword"
></button>
</bit-form-field>
<app-password-strength
[password]="masterPassword"
[email]="email"
[showText]="true"
(passwordStrengthResult)="getStrengthResult($event)"
>
</app-password-strength>
</div>
<bit-form-field>
<bit-label>{{ "confirmNewMasterPass" | i18n }}</bit-label>
<input
bitInput
type="password"
appInputVerbatim
required
[(ngModel)]="masterPasswordRetype"
name="masterPasswordRetype"
id="masterPasswordRetype"
/>
<button
type="button"
bitIconButton
bitSuffix
bitPasswordInputToggle
[(toggled)]="showPassword"
></button>
</bit-form-field>
<bit-form-field>
<bit-label>{{ "masterPassHint" | i18n }}</bit-label>
<input bitInput type="text" [(ngModel)]="hint" name="hint" id="hint" />
<bit-hint>{{ "masterPassHintDesc" | i18n }}</bit-hint>
</bit-form-field>
<hr />
<div class="tw-flex tw-space-x-2">
<button
type="submit"
bitButton
[block]="true"
buttonType="primary"
[loading]="form.loading"
[disabled]="form.loading"
>
{{ "submit" | i18n }}
</button>
<button type="button" bitButton [block]="true" buttonType="secondary" (click)="logOut()">
{{ "logOut" | i18n }}
</button>
</div>
</div>
</div>
</div>
</form>

View File

@@ -1,10 +0,0 @@
import { Component } from "@angular/core";
import { UpdateTempPasswordComponent as BaseUpdateTempPasswordComponent } from "@bitwarden/angular/auth/components/update-temp-password.component";
@Component({
selector: "app-update-temp-password",
templateUrl: "update-temp-password.component.html",
standalone: false,
})
export class UpdateTempPasswordComponent extends BaseUpdateTempPasswordComponent {}

View File

@@ -33,7 +33,6 @@ import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.
import {
RegistrationFinishService as RegistrationFinishServiceAbstraction,
LoginComponentService,
SetPasswordJitService,
SsoComponentService,
LoginDecryptionOptionsService,
TwoFactorAuthDuoComponentService,
@@ -117,7 +116,6 @@ import { flagEnabled } from "../../utils/flags";
import { PolicyListService } from "../admin-console/core/policy-list.service";
import {
WebChangePasswordService,
WebSetPasswordJitService,
WebRegistrationFinishService,
WebLoginComponentService,
WebLoginDecryptionOptionsService,
@@ -277,21 +275,6 @@ const safeProviders: SafeProvider[] = [
useClass: WebLockComponentService,
deps: [],
}),
safeProvider({
provide: SetPasswordJitService,
useClass: WebSetPasswordJitService,
deps: [
EncryptService,
I18nServiceAbstraction,
KdfConfigService,
KeyServiceAbstraction,
MasterPasswordApiService,
InternalMasterPasswordServiceAbstraction,
OrganizationApiServiceAbstraction,
OrganizationUserApiService,
InternalUserDecryptionOptionsServiceAbstraction,
],
}),
safeProvider({
provide: SetInitialPasswordService,
useClass: WebSetInitialPasswordService,

View File

@@ -12,14 +12,12 @@ import {
} from "@bitwarden/angular/auth/guards";
import { ChangePasswordComponent } from "@bitwarden/angular/auth/password-management/change-password";
import { SetInitialPasswordComponent } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.component";
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
import {
PasswordHintComponent,
RegistrationFinishComponent,
RegistrationStartComponent,
RegistrationStartSecondaryComponent,
RegistrationStartSecondaryComponentData,
SetPasswordJitComponent,
RegistrationLinkExpiredComponent,
LoginComponent,
LoginSecondaryContentComponent,
@@ -39,7 +37,6 @@ import {
NewDeviceVerificationComponent,
DeviceVerificationIcon,
} from "@bitwarden/auth/angular";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, Icons } from "@bitwarden/components";
import { LockComponent } from "@bitwarden/key-management-ui";
import { VaultIcons } from "@bitwarden/vault";
@@ -55,13 +52,10 @@ import { LoginViaWebAuthnComponent } from "./auth/login/login-via-webauthn/login
import { AcceptOrganizationComponent } from "./auth/organization-invite/accept-organization.component";
import { RecoverDeleteComponent } from "./auth/recover-delete.component";
import { RecoverTwoFactorComponent } from "./auth/recover-two-factor.component";
import { SetPasswordComponent } from "./auth/set-password.component";
import { AccountComponent } from "./auth/settings/account/account.component";
import { EmergencyAccessComponent } from "./auth/settings/emergency-access/emergency-access.component";
import { EmergencyAccessViewComponent } from "./auth/settings/emergency-access/view/emergency-access-view.component";
import { SecurityRoutingModule } from "./auth/settings/security/security-routing.module";
import { UpdatePasswordComponent } from "./auth/update-password.component";
import { UpdateTempPasswordComponent } from "./auth/update-temp-password.component";
import { VerifyEmailTokenComponent } from "./auth/verify-email-token.component";
import { VerifyRecoverDeleteComponent } from "./auth/verify-recover-delete.component";
import { SponsoredFamiliesComponent } from "./billing/settings/sponsored-families.component";
@@ -115,11 +109,6 @@ const routes: Routes = [
component: LoginViaWebAuthnComponent,
data: { titleId: "logInWithPasskey" } satisfies RouteDataProperties,
},
{
path: "set-password",
component: SetPasswordComponent,
data: { titleId: "setMasterPassword" } satisfies RouteDataProperties,
},
{ path: "verify-email", component: VerifyEmailTokenComponent },
{
path: "accept-organization",
@@ -143,34 +132,6 @@ const routes: Routes = [
canActivate: [unauthGuardFn()],
data: { titleId: "deleteOrganization" },
},
{
path: "update-temp-password",
component: UpdateTempPasswordComponent,
canActivate: [
canAccessFeature(
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
false,
"change-password",
false,
),
authGuard,
],
data: { titleId: "updateTempPassword" } satisfies RouteDataProperties,
},
{
path: "update-password",
component: UpdatePasswordComponent,
canActivate: [
canAccessFeature(
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
false,
"change-password",
false,
),
authGuard,
],
data: { titleId: "updatePassword" } satisfies RouteDataProperties,
},
],
},
{
@@ -329,24 +290,12 @@ const routes: Routes = [
},
{
path: "set-initial-password",
canActivate: [canAccessFeature(FeatureFlag.PM16117_SetInitialPasswordRefactor), authGuard],
canActivate: [authGuard],
component: SetInitialPasswordComponent,
data: {
maxWidth: "lg",
} satisfies AnonLayoutWrapperData,
},
{
path: "set-password-jit",
component: SetPasswordJitComponent,
data: {
pageTitle: {
key: "joinOrganization",
},
pageSubtitle: {
key: "finishJoiningThisOrganizationBySettingAMasterPassword",
},
} satisfies AnonLayoutWrapperData,
},
{
path: "signup-link-expired",
canActivate: [unauthGuardFn()],
@@ -601,10 +550,7 @@ const routes: Routes = [
{
path: "change-password",
component: ChangePasswordComponent,
canActivate: [
canAccessFeature(FeatureFlag.PM16117_ChangeExistingPasswordRefactor),
authGuard,
],
canActivate: [authGuard],
},
{
path: "setup-extension",

View File

@@ -14,16 +14,12 @@ import { VerifyRecoverDeleteOrgComponent } from "../admin-console/organizations/
import { AcceptFamilySponsorshipComponent } from "../admin-console/organizations/sponsorships/accept-family-sponsorship.component";
import { RecoverDeleteComponent } from "../auth/recover-delete.component";
import { RecoverTwoFactorComponent } from "../auth/recover-two-factor.component";
import { SetPasswordComponent } from "../auth/set-password.component";
import { DangerZoneComponent } from "../auth/settings/account/danger-zone.component";
import { EmergencyAccessConfirmComponent } from "../auth/settings/emergency-access/confirm/emergency-access-confirm.component";
import { EmergencyAccessAddEditComponent } from "../auth/settings/emergency-access/emergency-access-add-edit.component";
import { EmergencyAccessComponent } from "../auth/settings/emergency-access/emergency-access.component";
import { EmergencyAccessTakeoverComponent } from "../auth/settings/emergency-access/takeover/emergency-access-takeover.component";
import { EmergencyAccessViewComponent } from "../auth/settings/emergency-access/view/emergency-access-view.component";
import { UserVerificationModule } from "../auth/shared/components/user-verification";
import { UpdatePasswordComponent } from "../auth/update-password.component";
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component";
import { VerifyEmailTokenComponent } from "../auth/verify-email-token.component";
import { VerifyRecoverDeleteComponent } from "../auth/verify-recover-delete.component";
import { FreeBitwardenFamiliesComponent } from "../billing/members/free-bitwarden-families.component";
@@ -73,7 +69,6 @@ import { SharedModule } from "./shared.module";
EmergencyAccessAddEditComponent,
EmergencyAccessComponent,
EmergencyAccessConfirmComponent,
EmergencyAccessTakeoverComponent,
EmergencyAccessViewComponent,
OrgEventsComponent,
OrgExposedPasswordsReportComponent,
@@ -85,12 +80,9 @@ import { SharedModule } from "./shared.module";
RecoverDeleteComponent,
RecoverTwoFactorComponent,
RemovePasswordComponent,
SetPasswordComponent,
SponsoredFamiliesComponent,
FreeBitwardenFamiliesComponent,
SponsoringOrgRowComponent,
UpdatePasswordComponent,
UpdateTempPasswordComponent,
VerifyEmailTokenComponent,
VerifyRecoverDeleteComponent,
],
@@ -100,7 +92,6 @@ import { SharedModule } from "./shared.module";
EmergencyAccessAddEditComponent,
EmergencyAccessComponent,
EmergencyAccessConfirmComponent,
EmergencyAccessTakeoverComponent,
EmergencyAccessViewComponent,
OrganizationLayoutComponent,
OrgEventsComponent,
@@ -114,12 +105,9 @@ import { SharedModule } from "./shared.module";
RecoverDeleteComponent,
RecoverTwoFactorComponent,
RemovePasswordComponent,
SetPasswordComponent,
SponsoredFamiliesComponent,
FreeBitwardenFamiliesComponent,
SponsoringOrgRowComponent,
UpdateTempPasswordComponent,
UpdatePasswordComponent,
VerifyEmailTokenComponent,
VerifyRecoverDeleteComponent,
HeaderModule,

View File

@@ -1,232 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive, OnDestroy, OnInit } from "@angular/core";
import { Subject, firstValueFrom, map, switchMap, takeUntil } from "rxjs";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { UserKey, MasterKey } from "@bitwarden/common/types/key";
import { DialogService, ToastService } from "@bitwarden/components";
import { KdfConfig, KdfConfigService, KeyService } from "@bitwarden/key-management";
import { PasswordColorText } from "../../tools/password-strength/password-strength.component";
@Directive()
export class ChangePasswordComponent implements OnInit, OnDestroy {
masterPassword: string;
masterPasswordRetype: string;
formPromise: Promise<any>;
enforcedPolicyOptions: MasterPasswordPolicyOptions;
passwordStrengthResult: any;
color: string;
text: string;
leakedPassword: boolean;
minimumLength = Utils.minimumPasswordLength;
protected email: string;
protected kdfConfig: KdfConfig;
protected destroy$ = new Subject<void>();
constructor(
protected accountService: AccountService,
protected dialogService: DialogService,
protected i18nService: I18nService,
protected kdfConfigService: KdfConfigService,
protected keyService: KeyService,
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
protected messagingService: MessagingService,
protected platformUtilsService: PlatformUtilsService,
protected policyService: PolicyService,
protected toastService: ToastService,
) {}
async ngOnInit() {
this.email = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.email)),
);
this.accountService.activeAccount$
.pipe(
getUserId,
switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId)),
takeUntil(this.destroy$),
)
.subscribe(
(enforcedPasswordPolicyOptions) =>
(this.enforcedPolicyOptions ??= enforcedPasswordPolicyOptions),
);
if (this.enforcedPolicyOptions?.minLength) {
this.minimumLength = this.enforcedPolicyOptions.minLength;
}
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
async submit() {
if (!(await this.strongPassword())) {
return;
}
if (!(await this.setupSubmitActions())) {
return;
}
const [userId, email] = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => [a?.id, a?.email])),
);
if (this.kdfConfig == null) {
this.kdfConfig = await this.kdfConfigService.getKdfConfig(userId);
}
// Create new master key
const newMasterKey = await this.keyService.makeMasterKey(
this.masterPassword,
email.trim().toLowerCase(),
this.kdfConfig,
);
const newMasterKeyHash = await this.keyService.hashMasterKey(this.masterPassword, newMasterKey);
let newProtectedUserKey: [UserKey, EncString] = null;
const userKey = await this.keyService.getUserKey();
if (userKey == null) {
newProtectedUserKey = await this.keyService.makeUserKey(newMasterKey);
} else {
newProtectedUserKey = await this.keyService.encryptUserKeyWithMasterKey(newMasterKey);
}
await this.performSubmitActions(newMasterKeyHash, newMasterKey, newProtectedUserKey);
}
async setupSubmitActions(): Promise<boolean> {
// Override in sub-class
// Can be used for additional validation and/or other processes the should occur before changing passwords
return true;
}
async performSubmitActions(
newMasterKeyHash: string,
newMasterKey: MasterKey,
newUserKey: [UserKey, EncString],
) {
// Override in sub-class
}
async strongPassword(): Promise<boolean> {
if (this.masterPassword == null || this.masterPassword === "") {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("masterPasswordRequired"),
});
return false;
}
if (this.masterPassword.length < this.minimumLength) {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("masterPasswordMinimumlength", this.minimumLength),
});
return false;
}
if (this.masterPassword !== this.masterPasswordRetype) {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("masterPassDoesntMatch"),
});
return false;
}
const strengthResult = this.passwordStrengthResult;
if (
this.enforcedPolicyOptions != null &&
!this.policyService.evaluateMasterPassword(
strengthResult.score,
this.masterPassword,
this.enforcedPolicyOptions,
)
) {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("masterPasswordPolicyRequirementsNotMet"),
});
return false;
}
const weakPassword = strengthResult != null && strengthResult.score < 3;
if (weakPassword && this.leakedPassword) {
const result = await this.dialogService.openSimpleDialog({
title: { key: "weakAndExposedMasterPassword" },
content: { key: "weakAndBreachedMasterPasswordDesc" },
type: "warning",
});
if (!result) {
return false;
}
} else {
if (weakPassword) {
const result = await this.dialogService.openSimpleDialog({
title: { key: "weakMasterPassword" },
content: { key: "weakMasterPasswordDesc" },
type: "warning",
});
if (!result) {
return false;
}
}
if (this.leakedPassword) {
const result = await this.dialogService.openSimpleDialog({
title: { key: "exposedMasterPassword" },
content: { key: "exposedMasterPasswordDesc" },
type: "warning",
});
if (!result) {
return false;
}
}
}
return true;
}
async logOut() {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "logOut" },
content: { key: "logOutConfirmation" },
acceptButtonText: { key: "logOut" },
type: "warning",
});
if (confirmed) {
this.messagingService.send("logout");
}
}
getStrengthResult(result: any) {
this.passwordStrengthResult = result;
}
getPasswordScoreText(event: PasswordColorText) {
this.color = event.color;
this.text = event.text;
}
}

View File

@@ -1,300 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { firstValueFrom, of } from "rxjs";
import { filter, first, switchMap, tap } from "rxjs/operators";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import {
OrganizationUserApiService,
OrganizationUserResetPasswordEnrollmentRequest,
} from "@bitwarden/admin-console/common";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { OrganizationAutoEnrollStatusResponse } from "@bitwarden/common/admin-console/models/response/organization-auto-enroll-status.response";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { HashPurpose } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { UserId } from "@bitwarden/common/types/guid";
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { DialogService, ToastService } from "@bitwarden/components";
import { DEFAULT_KDF_CONFIG, KdfConfigService, KeyService } from "@bitwarden/key-management";
import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component";
@Directive()
export class SetPasswordComponent extends BaseChangePasswordComponent implements OnInit {
syncLoading = true;
showPassword = false;
hint = "";
orgSsoIdentifier: string = null;
orgId: string;
resetPasswordAutoEnroll = false;
onSuccessfulChangePassword: () => Promise<void>;
successRoute = "vault";
activeUserId: UserId;
forceSetPasswordReason: ForceSetPasswordReason = ForceSetPasswordReason.None;
ForceSetPasswordReason = ForceSetPasswordReason;
constructor(
protected accountService: AccountService,
protected dialogService: DialogService,
protected encryptService: EncryptService,
protected i18nService: I18nService,
protected kdfConfigService: KdfConfigService,
protected keyService: KeyService,
protected masterPasswordApiService: MasterPasswordApiService,
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
protected messagingService: MessagingService,
protected organizationApiService: OrganizationApiServiceAbstraction,
protected organizationUserApiService: OrganizationUserApiService,
protected platformUtilsService: PlatformUtilsService,
protected policyApiService: PolicyApiServiceAbstraction,
protected policyService: PolicyService,
protected route: ActivatedRoute,
protected router: Router,
protected ssoLoginService: SsoLoginServiceAbstraction,
protected syncService: SyncService,
protected toastService: ToastService,
protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
) {
super(
accountService,
dialogService,
i18nService,
kdfConfigService,
keyService,
masterPasswordService,
messagingService,
platformUtilsService,
policyService,
toastService,
);
}
async ngOnInit() {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
super.ngOnInit();
await this.syncService.fullSync(true);
this.syncLoading = false;
this.activeUserId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
this.forceSetPasswordReason = await firstValueFrom(
this.masterPasswordService.forceSetPasswordReason$(this.activeUserId),
);
this.route.queryParams
.pipe(
first(),
switchMap((qParams) => {
if (qParams.identifier != null) {
return of(qParams.identifier);
} else {
// Try to get orgSsoId from state as fallback
// Note: this is primarily for the TDE user w/out MP obtains admin MP reset permission scenario.
return this.ssoLoginService.getActiveUserOrganizationSsoIdentifier(this.activeUserId);
}
}),
filter((orgSsoId) => orgSsoId != null),
tap((orgSsoId: string) => {
this.orgSsoIdentifier = orgSsoId;
}),
switchMap((orgSsoId: string) => this.organizationApiService.getAutoEnrollStatus(orgSsoId)),
tap((orgAutoEnrollStatusResponse: OrganizationAutoEnrollStatusResponse) => {
this.orgId = orgAutoEnrollStatusResponse.id;
this.resetPasswordAutoEnroll = orgAutoEnrollStatusResponse.resetPasswordEnabled;
}),
switchMap((orgAutoEnrollStatusResponse: OrganizationAutoEnrollStatusResponse) =>
// Must get org id from response to get master password policy options
this.policyApiService.getMasterPasswordPolicyOptsForOrgUser(
orgAutoEnrollStatusResponse.id,
),
),
tap((masterPasswordPolicyOptions: MasterPasswordPolicyOptions) => {
this.enforcedPolicyOptions = masterPasswordPolicyOptions;
}),
)
.subscribe({
error: () => {
this.toastService.showToast({
variant: "error",
title: null,
message: this.i18nService.t("errorOccurred"),
});
},
});
}
async setupSubmitActions() {
this.kdfConfig = DEFAULT_KDF_CONFIG;
return true;
}
async performSubmitActions(
masterPasswordHash: string,
masterKey: MasterKey,
userKey: [UserKey, EncString],
) {
let keysRequest: KeysRequest | null = null;
let newKeyPair: [string, EncString] | null = null;
if (
this.forceSetPasswordReason !=
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission
) {
// Existing JIT provisioned user in a MP encryption org setting first password
// Users in this state will not already have a user asymmetric key pair so must create it for them
// We don't want to re-create the user key pair if the user already has one (TDE user case)
// in case we have a local private key, and are not sure whether it has been posted to the server, we post the local private key instead of generating a new one
const existingUserPrivateKey = (await firstValueFrom(
this.keyService.userPrivateKey$(this.activeUserId),
)) as Uint8Array;
const existingUserPublicKey = await firstValueFrom(
this.keyService.userPublicKey$(this.activeUserId),
);
if (existingUserPrivateKey != null && existingUserPublicKey != null) {
const existingUserPublicKeyB64 = Utils.fromBufferToB64(existingUserPublicKey);
newKeyPair = [
existingUserPublicKeyB64,
await this.encryptService.wrapDecapsulationKey(existingUserPrivateKey, userKey[0]),
];
} else {
newKeyPair = await this.keyService.makeKeyPair(userKey[0]);
}
keysRequest = new KeysRequest(newKeyPair[0], newKeyPair[1].encryptedString);
}
const request = new SetPasswordRequest(
masterPasswordHash,
userKey[1].encryptedString,
this.hint,
this.orgSsoIdentifier,
keysRequest,
this.kdfConfig.kdfType, //always PBKDF2 --> see this.setupSubmitActions
this.kdfConfig.iterations,
);
try {
if (this.resetPasswordAutoEnroll) {
this.formPromise = this.masterPasswordApiService
.setPassword(request)
.then(async () => {
await this.onSetPasswordSuccess(masterKey, userKey, newKeyPair);
return this.organizationApiService.getKeys(this.orgId);
})
.then(async (response) => {
if (response == null) {
throw new Error(this.i18nService.t("resetPasswordOrgKeysError"));
}
const publicKey = Utils.fromB64ToArray(response.publicKey);
// RSA Encrypt user key with organization public key
const userKey = await this.keyService.getUserKey();
const encryptedUserKey = await this.encryptService.encapsulateKeyUnsigned(
userKey,
publicKey,
);
const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest();
resetRequest.masterPasswordHash = masterPasswordHash;
resetRequest.resetPasswordKey = encryptedUserKey.encryptedString;
return this.organizationUserApiService.putOrganizationUserResetPasswordEnrollment(
this.orgId,
this.activeUserId,
resetRequest,
);
});
} else {
this.formPromise = this.masterPasswordApiService.setPassword(request).then(async () => {
await this.onSetPasswordSuccess(masterKey, userKey, newKeyPair);
});
}
await this.formPromise;
if (this.onSuccessfulChangePassword != null) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.onSuccessfulChangePassword();
} else {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate([this.successRoute]);
}
} catch {
this.toastService.showToast({
variant: "error",
title: null,
message: this.i18nService.t("errorOccurred"),
});
}
}
togglePassword(confirmField: boolean) {
this.showPassword = !this.showPassword;
document.getElementById(confirmField ? "masterPasswordRetype" : "masterPassword").focus();
}
protected async onSetPasswordSuccess(
masterKey: MasterKey,
userKey: [UserKey, EncString],
keyPair: [string, EncString] | null,
) {
// Clear force set password reason to allow navigation back to vault.
await this.masterPasswordService.setForceSetPasswordReason(
ForceSetPasswordReason.None,
this.activeUserId,
);
// User now has a password so update account decryption options in state
const userDecryptionOpts = await firstValueFrom(
this.userDecryptionOptionsService.userDecryptionOptions$,
);
userDecryptionOpts.hasMasterPassword = true;
await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts);
await this.kdfConfigService.setKdfConfig(this.activeUserId, this.kdfConfig);
await this.masterPasswordService.setMasterKey(masterKey, this.activeUserId);
await this.keyService.setUserKey(userKey[0], this.activeUserId);
// Set private key only for new JIT provisioned users in MP encryption orgs
// Existing TDE users will have private key set on sync or on login
if (
keyPair !== null &&
this.forceSetPasswordReason !=
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission
) {
await this.keyService.setPrivateKey(keyPair[1].encryptedString, this.activeUserId);
}
const localMasterKeyHash = await this.keyService.hashMasterKey(
this.masterPassword,
masterKey,
HashPurpose.LocalAuthorization,
);
await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, this.activeUserId);
}
}

View File

@@ -1,141 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive } from "@angular/core";
import { Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { Verification } from "@bitwarden/common/auth/types/verification";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
import { DialogService, ToastService } from "@bitwarden/components";
import { KdfConfigService, KeyService } from "@bitwarden/key-management";
import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component";
@Directive()
export class UpdatePasswordComponent extends BaseChangePasswordComponent {
hint: string;
key: string;
enforcedPolicyOptions: MasterPasswordPolicyOptions;
showPassword = false;
currentMasterPassword: string;
onSuccessfulChangePassword: () => Promise<void>;
constructor(
protected router: Router,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
policyService: PolicyService,
keyService: KeyService,
messagingService: MessagingService,
private masterPasswordApiService: MasterPasswordApiService,
private userVerificationService: UserVerificationService,
private logService: LogService,
dialogService: DialogService,
kdfConfigService: KdfConfigService,
masterPasswordService: InternalMasterPasswordServiceAbstraction,
accountService: AccountService,
toastService: ToastService,
) {
super(
accountService,
dialogService,
i18nService,
kdfConfigService,
keyService,
masterPasswordService,
messagingService,
platformUtilsService,
policyService,
toastService,
);
}
togglePassword(confirmField: boolean) {
this.showPassword = !this.showPassword;
document.getElementById(confirmField ? "masterPasswordRetype" : "masterPassword").focus();
}
async cancel() {
await this.router.navigate(["/vault"]);
}
async setupSubmitActions(): Promise<boolean> {
if (this.currentMasterPassword == null || this.currentMasterPassword === "") {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("masterPasswordRequired"),
});
return false;
}
const secret: Verification = {
type: VerificationType.MasterPassword,
secret: this.currentMasterPassword,
};
try {
await this.userVerificationService.verifyUser(secret);
} catch (e) {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: e.message,
});
return false;
}
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
this.kdfConfig = await this.kdfConfigService.getKdfConfig(userId);
return true;
}
async performSubmitActions(
newMasterKeyHash: string,
newMasterKey: MasterKey,
newUserKey: [UserKey, EncString],
) {
try {
// Create Request
const request = new PasswordRequest();
request.masterPasswordHash = await this.keyService.hashMasterKey(
this.currentMasterPassword,
await this.keyService.getOrDeriveMasterKey(this.currentMasterPassword),
);
request.newMasterPasswordHash = newMasterKeyHash;
request.key = newUserKey[1].encryptedString;
// Update user's password
await this.masterPasswordApiService.postPassword(request);
this.toastService.showToast({
variant: "success",
title: this.i18nService.t("masterPasswordChanged"),
message: this.i18nService.t("logBackIn"),
});
if (this.onSuccessfulChangePassword != null) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.onSuccessfulChangePassword();
} else {
this.messagingService.send("logout");
}
} catch (e) {
this.logService.error(e);
}
}
}

View File

@@ -1,232 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { firstValueFrom, map } from "rxjs";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
import { UpdateTdeOffboardingPasswordRequest } from "@bitwarden/common/auth/models/request/update-tde-offboarding-password.request";
import { UpdateTempPasswordRequest } from "@bitwarden/common/auth/models/request/update-temp-password.request";
import { MasterPasswordVerification } from "@bitwarden/common/auth/types/verification";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { DialogService, ToastService } from "@bitwarden/components";
import { KdfConfigService, KeyService } from "@bitwarden/key-management";
import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component";
@Directive()
export class UpdateTempPasswordComponent extends BaseChangePasswordComponent implements OnInit {
hint: string;
key: string;
enforcedPolicyOptions: MasterPasswordPolicyOptions;
showPassword = false;
reason: ForceSetPasswordReason = ForceSetPasswordReason.None;
verification: MasterPasswordVerification = {
type: VerificationType.MasterPassword,
secret: "",
};
onSuccessfulChangePassword: () => Promise<any>;
get requireCurrentPassword(): boolean {
return this.reason === ForceSetPasswordReason.WeakMasterPassword;
}
constructor(
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
policyService: PolicyService,
keyService: KeyService,
messagingService: MessagingService,
private masterPasswordApiService: MasterPasswordApiService,
private syncService: SyncService,
private logService: LogService,
private userVerificationService: UserVerificationService,
protected router: Router,
dialogService: DialogService,
kdfConfigService: KdfConfigService,
accountService: AccountService,
masterPasswordService: InternalMasterPasswordServiceAbstraction,
toastService: ToastService,
) {
super(
accountService,
dialogService,
i18nService,
kdfConfigService,
keyService,
masterPasswordService,
messagingService,
platformUtilsService,
policyService,
toastService,
);
}
async ngOnInit() {
await this.syncService.fullSync(true);
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
this.reason = await firstValueFrom(this.masterPasswordService.forceSetPasswordReason$(userId));
// If we somehow end up here without a reason, go back to the home page
if (this.reason == ForceSetPasswordReason.None) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/"]);
return;
}
await super.ngOnInit();
}
get masterPasswordWarningText(): string {
if (this.reason == ForceSetPasswordReason.WeakMasterPassword) {
return this.i18nService.t("updateWeakMasterPasswordWarning");
} else if (this.reason == ForceSetPasswordReason.TdeOffboarding) {
return this.i18nService.t("tdeDisabledMasterPasswordRequired");
} else {
return this.i18nService.t("updateMasterPasswordWarning");
}
}
togglePassword(confirmField: boolean) {
this.showPassword = !this.showPassword;
document.getElementById(confirmField ? "masterPasswordRetype" : "masterPassword").focus();
}
async setupSubmitActions(): Promise<boolean> {
const [userId, email] = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => [a?.id, a?.email])),
);
this.email = email;
this.kdfConfig = await this.kdfConfigService.getKdfConfig(userId);
return true;
}
async submit() {
// Validation
if (!(await this.strongPassword())) {
return;
}
if (!(await this.setupSubmitActions())) {
return;
}
try {
// Create new key and hash new password
const newMasterKey = await this.keyService.makeMasterKey(
this.masterPassword,
this.email.trim().toLowerCase(),
this.kdfConfig,
);
const newPasswordHash = await this.keyService.hashMasterKey(
this.masterPassword,
newMasterKey,
);
// Grab user key
const userKey = await this.keyService.getUserKey();
// Encrypt user key with new master key
const newProtectedUserKey = await this.keyService.encryptUserKeyWithMasterKey(
newMasterKey,
userKey,
);
await this.performSubmitActions(newPasswordHash, newMasterKey, newProtectedUserKey);
} catch (e) {
this.logService.error(e);
}
}
async performSubmitActions(
masterPasswordHash: string,
masterKey: MasterKey,
userKey: [UserKey, EncString],
) {
try {
switch (this.reason) {
case ForceSetPasswordReason.AdminForcePasswordReset:
this.formPromise = this.updateTempPassword(masterPasswordHash, userKey);
break;
case ForceSetPasswordReason.WeakMasterPassword:
this.formPromise = this.updatePassword(masterPasswordHash, userKey);
break;
case ForceSetPasswordReason.TdeOffboarding:
this.formPromise = this.updateTdeOffboardingPassword(masterPasswordHash, userKey);
break;
}
await this.formPromise;
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("updatedMasterPassword"),
});
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
await this.masterPasswordService.setForceSetPasswordReason(
ForceSetPasswordReason.None,
userId,
);
if (this.onSuccessfulChangePassword != null) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.onSuccessfulChangePassword();
} else {
this.messagingService.send("logout");
}
} catch (e) {
this.logService.error(e);
}
}
private async updateTempPassword(masterPasswordHash: string, userKey: [UserKey, EncString]) {
const request = new UpdateTempPasswordRequest();
request.key = userKey[1].encryptedString;
request.newMasterPasswordHash = masterPasswordHash;
request.masterPasswordHint = this.hint;
return this.masterPasswordApiService.putUpdateTempPassword(request);
}
private async updatePassword(newMasterPasswordHash: string, userKey: [UserKey, EncString]) {
const request = await this.userVerificationService.buildRequest(
this.verification,
PasswordRequest,
);
request.masterPasswordHint = this.hint;
request.newMasterPasswordHash = newMasterPasswordHash;
request.key = userKey[1].encryptedString;
return this.masterPasswordApiService.postPassword(request);
}
private async updateTdeOffboardingPassword(
masterPasswordHash: string,
userKey: [UserKey, EncString],
) {
const request = new UpdateTdeOffboardingPasswordRequest();
request.key = userKey[1].encryptedString;
request.newMasterPasswordHash = masterPasswordHash;
request.masterPasswordHint = this.hint;
return this.masterPasswordApiService.putUpdateTdeOffboardingPassword(request);
}
}

View File

@@ -68,10 +68,7 @@ describe("AuthGuard", () => {
{ path: "", component: EmptyComponent },
{ path: "guarded-route", component: EmptyComponent, canActivate: [authGuard] },
{ path: "lock", component: EmptyComponent },
{ path: "set-password", component: EmptyComponent },
{ path: "set-password-jit", component: EmptyComponent },
{ path: "set-initial-password", component: EmptyComponent, canActivate: [authGuard] },
{ path: "update-temp-password", component: EmptyComponent, canActivate: [authGuard] },
{ path: "change-password", component: EmptyComponent },
{ path: "remove-password", component: EmptyComponent, canActivate: [authGuard] },
]),
@@ -125,109 +122,58 @@ describe("AuthGuard", () => {
});
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,
);
it("should redirect to /set-initial-password when the user has ForceSetPasswordReaason.TdeOffboardingUntrustedDevice", async () => {
const { router } = setup(
AuthenticationStatus.Locked,
ForceSetPasswordReason.TdeOffboardingUntrustedDevice,
false,
);
await router.navigate(["guarded-route"]);
expect(router.url).toBe("/set-initial-password");
});
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,
);
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,
);
await router.navigate(["/set-initial-password"]);
expect(router.url).toContain("/set-initial-password");
});
await router.navigate(["/set-initial-password"]);
expect(router.url).toContain("/set-initial-password");
});
});
describe("given user is Unlocked", () => {
describe("given the PM16117_SetInitialPasswordRefactor feature flag is ON", () => {
const tests = [
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission,
ForceSetPasswordReason.TdeOffboarding,
];
describe("given user is Unlocked and ForceSetPasswordReason requires setting an initial password", () => {
const tests = [
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission,
ForceSetPasswordReason.TdeOffboarding,
];
describe("given user attempts to navigate to an auth guarded route", () => {
tests.forEach((reason) => {
it(`should redirect to /set-initial-password when the user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
const { router } = setup(
AuthenticationStatus.Unlocked,
reason,
false,
FeatureFlag.PM16117_SetInitialPasswordRefactor,
);
describe("given user attempts to navigate to an auth guarded route", () => {
tests.forEach((reason) => {
it(`should redirect to /set-initial-password when the user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
const { router } = setup(AuthenticationStatus.Unlocked, reason, false);
await router.navigate(["guarded-route"]);
expect(router.url).toContain("/set-initial-password");
});
});
});
describe("given user attempts to navigate to /set-initial-password", () => {
tests.forEach((reason) => {
it(`should allow navigation to continue to /set-initial-password when the user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
const { router } = setup(
AuthenticationStatus.Unlocked,
reason,
false,
FeatureFlag.PM16117_SetInitialPasswordRefactor,
);
await router.navigate(["/set-initial-password"]);
expect(router.url).toContain("/set-initial-password");
});
await router.navigate(["guarded-route"]);
expect(router.url).toContain("/set-initial-password");
});
});
});
describe("given the PM16117_SetInitialPasswordRefactor feature flag is OFF", () => {
const tests = [
{
reason: ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission,
url: "/set-password",
},
{
reason: ForceSetPasswordReason.TdeOffboarding,
url: "/update-temp-password",
},
];
describe("given user attempts to navigate to /set-initial-password", () => {
tests.forEach((reason) => {
it(`should allow navigation to continue to /set-initial-password when the user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
const { router } = setup(AuthenticationStatus.Unlocked, reason, false);
describe("given user attempts to navigate to an auth guarded route", () => {
tests.forEach(({ reason, url }) => {
it(`should redirect to ${url} when user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
const { router } = setup(AuthenticationStatus.Unlocked, reason);
await router.navigate(["/guarded-route"]);
expect(router.url).toContain(url);
});
});
});
describe("given user attempts to navigate to the set- or update- password route itself", () => {
tests.forEach(({ reason, url }) => {
it(`should allow navigation to continue to ${url} when user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
const { router } = setup(AuthenticationStatus.Unlocked, reason);
await router.navigate([url]);
expect(router.url).toContain(url);
});
await router.navigate(["/set-initial-password"]);
expect(router.url).toContain("/set-initial-password");
});
});
});
describe("given the PM16117_ChangeExistingPasswordRefactor feature flag is ON", () => {
describe("given user is Unlocked and ForceSetPasswordReason requires changing an existing password", () => {
const tests = [
ForceSetPasswordReason.AdminForcePasswordReset,
ForceSetPasswordReason.WeakMasterPassword,
@@ -236,12 +182,7 @@ describe("AuthGuard", () => {
describe("given user attempts to navigate to an auth guarded route", () => {
tests.forEach((reason) => {
it(`should redirect to /change-password when user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
const { router } = setup(
AuthenticationStatus.Unlocked,
reason,
false,
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
);
const { router } = setup(AuthenticationStatus.Unlocked, reason, false);
await router.navigate(["guarded-route"]);
expect(router.url).toContain("/change-password");
@@ -256,7 +197,6 @@ describe("AuthGuard", () => {
AuthenticationStatus.Unlocked,
ForceSetPasswordReason.AdminForcePasswordReset,
false,
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
);
await router.navigate(["/change-password"]);
@@ -265,34 +205,5 @@ describe("AuthGuard", () => {
});
});
});
describe("given the PM16117_ChangeExistingPasswordRefactor feature flag is OFF", () => {
const tests = [
ForceSetPasswordReason.AdminForcePasswordReset,
ForceSetPasswordReason.WeakMasterPassword,
];
describe("given user attempts to navigate to an auth guarded route", () => {
tests.forEach((reason) => {
it(`should redirect to /update-temp-password when user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
const { router } = setup(AuthenticationStatus.Unlocked, reason);
await router.navigate(["guarded-route"]);
expect(router.url).toContain("/update-temp-password");
});
});
});
describe("given user attempts to navigate to /update-temp-password", () => {
tests.forEach((reason) => {
it(`should allow navigation to continue to /update-temp-password when user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
const { router } = setup(AuthenticationStatus.Unlocked, reason);
await router.navigate(["/update-temp-password"]);
expect(router.url).toContain("/update-temp-password");
});
});
});
});
});
});

View File

@@ -14,10 +14,8 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
export const authGuard: CanActivateFn = async (
@@ -30,7 +28,6 @@ export const authGuard: CanActivateFn = async (
const keyConnectorService = inject(KeyConnectorService);
const accountService = inject(AccountService);
const masterPasswordService = inject(MasterPasswordServiceAbstraction);
const configService = inject(ConfigService);
const authStatus = await authService.getAuthStatus();
@@ -44,16 +41,11 @@ export const authGuard: CanActivateFn = async (
masterPasswordService.forceSetPasswordReason$(userId),
);
const isSetInitialPasswordFlagOn = await configService.getFeatureFlag(
FeatureFlag.PM16117_SetInitialPasswordRefactor,
);
// User JIT provisioned into a master-password-encryption org
if (
authStatus === AuthenticationStatus.Locked &&
forceSetPasswordReason === ForceSetPasswordReason.SsoNewJitProvisionedUser &&
!routerState.url.includes("set-initial-password") &&
isSetInitialPasswordFlagOn
!routerState.url.includes("set-initial-password")
) {
return router.createUrlTree(["/set-initial-password"]);
}
@@ -62,8 +54,7 @@ export const authGuard: CanActivateFn = async (
if (
authStatus === AuthenticationStatus.Locked &&
forceSetPasswordReason === ForceSetPasswordReason.TdeOffboardingUntrustedDevice &&
!routerState.url.includes("set-initial-password") &&
isSetInitialPasswordFlagOn
!routerState.url.includes("set-initial-password")
) {
return router.createUrlTree(["/set-initial-password"]);
}
@@ -90,39 +81,28 @@ export const authGuard: CanActivateFn = async (
return router.createUrlTree(["/remove-password"]);
}
// TDE org user has "manage account recovery" permission
// Handle cases where a user needs to set a password when they don't already have one:
// - TDE org user has been given "manage account recovery" permission
// - TDE offboarding on a trusted device, where we have access to their encryption key wrap with their new password
if (
forceSetPasswordReason ===
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission &&
!routerState.url.includes("set-password") &&
(forceSetPasswordReason ===
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission ||
forceSetPasswordReason === ForceSetPasswordReason.TdeOffboarding) &&
!routerState.url.includes("set-initial-password")
) {
const route = isSetInitialPasswordFlagOn ? "/set-initial-password" : "/set-password";
const route = "/set-initial-password";
return router.createUrlTree([route]);
}
// TDE Offboarding on trusted device
if (
forceSetPasswordReason === ForceSetPasswordReason.TdeOffboarding &&
!routerState.url.includes("update-temp-password") &&
!routerState.url.includes("set-initial-password")
) {
const route = isSetInitialPasswordFlagOn ? "/set-initial-password" : "/update-temp-password";
return router.createUrlTree([route]);
}
const isChangePasswordFlagOn = await configService.getFeatureFlag(
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
);
// Post- Account Recovery or Weak Password on login
// Handle cases where a user has a password but needs to set a new one:
// - Account recovery
// - Weak Password on login
if (
(forceSetPasswordReason === ForceSetPasswordReason.AdminForcePasswordReset ||
forceSetPasswordReason === ForceSetPasswordReason.WeakMasterPassword) &&
!routerState.url.includes("update-temp-password") &&
!routerState.url.includes("change-password")
) {
const route = isChangePasswordFlagOn ? "/change-password" : "/update-temp-password";
const route = "/change-password";
return router.createUrlTree([route]);
}

View File

@@ -22,13 +22,11 @@ import {
DefaultLoginComponentService,
DefaultLoginDecryptionOptionsService,
DefaultRegistrationFinishService,
DefaultSetPasswordJitService,
DefaultTwoFactorAuthComponentService,
DefaultTwoFactorAuthWebAuthnComponentService,
LoginComponentService,
LoginDecryptionOptionsService,
RegistrationFinishService as RegistrationFinishServiceAbstraction,
SetPasswordJitService,
TwoFactorAuthComponentService,
TwoFactorAuthWebAuthnComponentService,
} from "@bitwarden/auth/angular";
@@ -1417,21 +1415,6 @@ const safeProviders: SafeProvider[] = [
useClass: DefaultOrganizationInviteService,
deps: [],
}),
safeProvider({
provide: SetPasswordJitService,
useClass: DefaultSetPasswordJitService,
deps: [
EncryptService,
I18nServiceAbstraction,
KdfConfigService,
KeyService,
MasterPasswordApiServiceAbstraction,
InternalMasterPasswordServiceAbstraction,
OrganizationApiServiceAbstraction,
OrganizationUserApiService,
InternalUserDecryptionOptionsServiceAbstraction,
],
}),
safeProvider({
provide: SetInitialPasswordService,
useClass: DefaultSetInitialPasswordService,

View File

@@ -41,11 +41,6 @@ export * from "./registration/registration-env-selector/registration-env-selecto
export * from "./registration/registration-finish/registration-finish.service";
export * from "./registration/registration-finish/default-registration-finish.service";
// set password (JIT user)
export * from "./set-password-jit/set-password-jit.component";
export * from "./set-password-jit/set-password-jit.service.abstraction";
export * from "./set-password-jit/default-set-password-jit.service";
// user verification
export * from "./user-verification/user-verification-dialog.component";
export * from "./user-verification/user-verification-dialog.types";

View File

@@ -18,7 +18,6 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ClientType, HttpStatusCode } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
@@ -230,29 +229,21 @@ export class LoginComponent implements OnInit, OnDestroy {
return;
}
let credentials: PasswordLoginCredentials;
// Try to retrieve any org policies from an org invite now so we can send it to the
// login strategies. Since it is optional and we only want to be doing this on the
// web we will only send in content in the right context.
const orgPoliciesFromInvite = this.loginComponentService.getOrgPoliciesFromOrgInvite
? await this.loginComponentService.getOrgPoliciesFromOrgInvite()
: null;
if (
await this.configService.getFeatureFlag(FeatureFlag.PM16117_ChangeExistingPasswordRefactor)
) {
// Try to retrieve any org policies from an org invite now so we can send it to the
// login strategies. Since it is optional and we only want to be doing this on the
// web we will only send in content in the right context.
const orgPoliciesFromInvite = this.loginComponentService.getOrgPoliciesFromOrgInvite
? await this.loginComponentService.getOrgPoliciesFromOrgInvite()
: null;
const orgMasterPasswordPolicyOptions = orgPoliciesFromInvite?.enforcedPasswordPolicyOptions;
const orgMasterPasswordPolicyOptions = orgPoliciesFromInvite?.enforcedPasswordPolicyOptions;
credentials = new PasswordLoginCredentials(
email,
masterPassword,
undefined,
orgMasterPasswordPolicyOptions,
);
} else {
credentials = new PasswordLoginCredentials(email, masterPassword);
}
const credentials = new PasswordLoginCredentials(
email,
masterPassword,
undefined,
orgMasterPasswordPolicyOptions,
);
try {
const authResult = await this.loginStrategyService.logIn(credentials);
@@ -332,7 +323,7 @@ export class LoginComponent implements OnInit, OnDestroy {
await this.loginSuccessHandlerService.run(authResult.userId);
// Determine where to send the user next
// The AuthGuard will handle routing to update-temp-password based on state
// The AuthGuard will handle routing to change-password based on state
// TODO: PM-18269 - evaluate if we can combine this with the
// password evaluation done in the password login strategy.
@@ -344,7 +335,7 @@ export class LoginComponent implements OnInit, OnDestroy {
if (orgPolicies) {
// Since we have retrieved the policies, we can go ahead and set them into state for future use
// e.g., the update-password page currently only references state for policy data and
// e.g., the change-password page currently only references state for policy data and
// doesn't fallback to pulling them from the server like it should if they are null.
await this.setPoliciesIntoState(authResult.userId, orgPolicies.policies);
@@ -352,13 +343,7 @@ export class LoginComponent implements OnInit, OnDestroy {
orgPolicies.enforcedPasswordPolicyOptions,
);
if (isPasswordChangeRequired) {
const changePasswordFeatureFlagOn = await this.configService.getFeatureFlag(
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
);
await this.router.navigate(
changePasswordFeatureFlagOn ? ["change-password"] : ["update-password"],
);
await this.router.navigate(["change-password"]);
return;
}
}

View File

@@ -10,7 +10,6 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -151,25 +150,17 @@ export class NewDeviceVerificationComponent implements OnInit, OnDestroy {
this.loginSuccessHandlerService.run(authResult.userId);
// TODO: PM-22663 use the new service to handle routing.
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const forceSetPasswordReason = await firstValueFrom(
this.masterPasswordService.forceSetPasswordReason$(activeUserId),
);
if (
await this.configService.getFeatureFlag(FeatureFlag.PM16117_ChangeExistingPasswordRefactor)
forceSetPasswordReason === ForceSetPasswordReason.WeakMasterPassword ||
forceSetPasswordReason === ForceSetPasswordReason.AdminForcePasswordReset
) {
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(getUserId),
);
const forceSetPasswordReason = await firstValueFrom(
this.masterPasswordService.forceSetPasswordReason$(activeUserId),
);
if (
forceSetPasswordReason === ForceSetPasswordReason.WeakMasterPassword ||
forceSetPasswordReason === ForceSetPasswordReason.AdminForcePasswordReset
) {
await this.router.navigate(["/change-password"]);
} else {
await this.router.navigate(["/vault"]);
}
await this.router.navigate(["/change-password"]);
} else {
await this.router.navigate(["/vault"]);
}

View File

@@ -1,241 +0,0 @@
import { MockProxy, mock } from "jest-mock-extended";
import { BehaviorSubject, of } from "rxjs";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import {
FakeUserDecryptionOptions as UserDecryptionOptions,
InternalUserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationKeysResponse } from "@bitwarden/common/admin-console/models/response/organization-keys.response";
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "@bitwarden/common/types/csprng";
import { UserId } from "@bitwarden/common/types/guid";
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
import { DEFAULT_KDF_CONFIG, KdfConfigService, KeyService } from "@bitwarden/key-management";
import { PasswordInputResult } from "../input-password/password-input-result";
import { DefaultSetPasswordJitService } from "./default-set-password-jit.service";
import { SetPasswordCredentials } from "./set-password-jit.service.abstraction";
describe("DefaultSetPasswordJitService", () => {
let sut: DefaultSetPasswordJitService;
let masterPasswordApiService: MockProxy<MasterPasswordApiService>;
let keyService: MockProxy<KeyService>;
let encryptService: MockProxy<EncryptService>;
let i18nService: MockProxy<I18nService>;
let kdfConfigService: MockProxy<KdfConfigService>;
let masterPasswordService: MockProxy<InternalMasterPasswordServiceAbstraction>;
let organizationApiService: MockProxy<OrganizationApiServiceAbstraction>;
let organizationUserApiService: MockProxy<OrganizationUserApiService>;
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
beforeEach(() => {
masterPasswordApiService = mock<MasterPasswordApiService>();
keyService = mock<KeyService>();
encryptService = mock<EncryptService>();
i18nService = mock<I18nService>();
kdfConfigService = mock<KdfConfigService>();
masterPasswordService = mock<InternalMasterPasswordServiceAbstraction>();
organizationApiService = mock<OrganizationApiServiceAbstraction>();
organizationUserApiService = mock<OrganizationUserApiService>();
userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>();
sut = new DefaultSetPasswordJitService(
encryptService,
i18nService,
kdfConfigService,
keyService,
masterPasswordApiService,
masterPasswordService,
organizationApiService,
organizationUserApiService,
userDecryptionOptionsService,
);
});
it("should instantiate the DefaultSetPasswordJitService", () => {
expect(sut).not.toBeFalsy();
});
describe("setPassword", () => {
let masterKey: MasterKey;
let userKey: UserKey;
let userKeyEncString: EncString;
let protectedUserKey: [UserKey, EncString];
let keyPair: [string, EncString];
let keysRequest: KeysRequest;
let organizationKeys: OrganizationKeysResponse;
let orgPublicKey: Uint8Array;
let orgSsoIdentifier: string;
let orgId: string;
let resetPasswordAutoEnroll: boolean;
let userId: UserId;
let passwordInputResult: PasswordInputResult;
let credentials: SetPasswordCredentials;
let userDecryptionOptionsSubject: BehaviorSubject<UserDecryptionOptions>;
let setPasswordRequest: SetPasswordRequest;
beforeEach(() => {
masterKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey;
userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
userKeyEncString = new EncString("userKeyEncrypted");
protectedUserKey = [userKey, userKeyEncString];
keyPair = ["publicKey", new EncString("privateKey")];
keysRequest = new KeysRequest(keyPair[0], keyPair[1].encryptedString);
organizationKeys = {
privateKey: "orgPrivateKey",
publicKey: "orgPublicKey",
} as OrganizationKeysResponse;
orgPublicKey = Utils.fromB64ToArray(organizationKeys.publicKey);
orgSsoIdentifier = "orgSsoIdentifier";
orgId = "orgId";
resetPasswordAutoEnroll = false;
userId = "userId" as UserId;
passwordInputResult = {
newMasterKey: masterKey,
newServerMasterKeyHash: "newServerMasterKeyHash",
newLocalMasterKeyHash: "newLocalMasterKeyHash",
newPasswordHint: "newPasswordHint",
kdfConfig: DEFAULT_KDF_CONFIG,
newPassword: "newPassword",
};
credentials = {
newMasterKey: passwordInputResult.newMasterKey,
newServerMasterKeyHash: passwordInputResult.newServerMasterKeyHash,
newLocalMasterKeyHash: passwordInputResult.newLocalMasterKeyHash,
newPasswordHint: passwordInputResult.newPasswordHint,
kdfConfig: passwordInputResult.kdfConfig,
orgSsoIdentifier,
orgId,
resetPasswordAutoEnroll,
userId,
};
userDecryptionOptionsSubject = new BehaviorSubject(null);
userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject;
setPasswordRequest = new SetPasswordRequest(
passwordInputResult.newServerMasterKeyHash,
protectedUserKey[1].encryptedString,
passwordInputResult.newPasswordHint,
orgSsoIdentifier,
keysRequest,
passwordInputResult.kdfConfig.kdfType,
passwordInputResult.kdfConfig.iterations,
);
});
function setupSetPasswordMocks(hasUserKey = true) {
if (!hasUserKey) {
keyService.userKey$.mockReturnValue(of(null));
keyService.makeUserKey.mockResolvedValue(protectedUserKey);
} else {
keyService.userKey$.mockReturnValue(of(userKey));
keyService.encryptUserKeyWithMasterKey.mockResolvedValue(protectedUserKey);
}
keyService.makeKeyPair.mockResolvedValue(keyPair);
masterPasswordApiService.setPassword.mockResolvedValue(undefined);
masterPasswordService.setForceSetPasswordReason.mockResolvedValue(undefined);
userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: true }));
userDecryptionOptionsService.setUserDecryptionOptions.mockResolvedValue(undefined);
kdfConfigService.setKdfConfig.mockResolvedValue(undefined);
keyService.setUserKey.mockResolvedValue(undefined);
keyService.setPrivateKey.mockResolvedValue(undefined);
masterPasswordService.setMasterKeyHash.mockResolvedValue(undefined);
}
function setupResetPasswordAutoEnrollMocks(organizationKeysExist = true) {
if (organizationKeysExist) {
organizationApiService.getKeys.mockResolvedValue(organizationKeys);
} else {
organizationApiService.getKeys.mockResolvedValue(null);
return;
}
keyService.userKey$.mockReturnValue(of(userKey));
encryptService.encapsulateKeyUnsigned.mockResolvedValue(userKeyEncString);
organizationUserApiService.putOrganizationUserResetPasswordEnrollment.mockResolvedValue(
undefined,
);
}
it("should set password successfully (given a user key)", async () => {
// Arrange
setupSetPasswordMocks();
// Act
await sut.setPassword(credentials);
// Assert
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
});
it("should set password successfully (given no user key)", async () => {
// Arrange
setupSetPasswordMocks(false);
// Act
await sut.setPassword(credentials);
// Assert
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
});
it("should handle reset password auto enroll", async () => {
// Arrange
credentials.resetPasswordAutoEnroll = true;
setupSetPasswordMocks();
setupResetPasswordAutoEnrollMocks();
// Act
await sut.setPassword(credentials);
// Assert
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
expect(organizationApiService.getKeys).toHaveBeenCalledWith(orgId);
expect(encryptService.encapsulateKeyUnsigned).toHaveBeenCalledWith(userKey, orgPublicKey);
expect(
organizationUserApiService.putOrganizationUserResetPasswordEnrollment,
).toHaveBeenCalled();
});
it("when handling reset password auto enroll, it should throw an error if organization keys are not found", async () => {
// Arrange
credentials.resetPasswordAutoEnroll = true;
setupSetPasswordMocks();
setupResetPasswordAutoEnrollMocks(false);
// Act and Assert
await expect(sut.setPassword(credentials)).rejects.toThrow();
expect(
organizationUserApiService.putOrganizationUserResetPasswordEnrollment,
).not.toHaveBeenCalled();
});
});
});

View File

@@ -1,176 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { firstValueFrom } from "rxjs";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import {
OrganizationUserApiService,
OrganizationUserResetPasswordEnrollmentRequest,
} from "@bitwarden/admin-console/common";
import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { UserId } from "@bitwarden/common/types/guid";
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
import { KdfConfigService, KeyService, KdfConfig } from "@bitwarden/key-management";
import {
SetPasswordCredentials,
SetPasswordJitService,
} from "./set-password-jit.service.abstraction";
export class DefaultSetPasswordJitService implements SetPasswordJitService {
constructor(
protected encryptService: EncryptService,
protected i18nService: I18nService,
protected kdfConfigService: KdfConfigService,
protected keyService: KeyService,
protected masterPasswordApiService: MasterPasswordApiService,
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
protected organizationApiService: OrganizationApiServiceAbstraction,
protected organizationUserApiService: OrganizationUserApiService,
protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
) {}
async setPassword(credentials: SetPasswordCredentials): Promise<void> {
const {
newMasterKey,
newServerMasterKeyHash,
newLocalMasterKeyHash,
newPasswordHint,
kdfConfig,
orgSsoIdentifier,
orgId,
resetPasswordAutoEnroll,
userId,
} = credentials;
for (const [key, value] of Object.entries(credentials)) {
if (value == null) {
throw new Error(`${key} not found. Could not set password.`);
}
}
const protectedUserKey = await this.makeProtectedUserKey(newMasterKey, userId);
if (protectedUserKey == null) {
throw new Error("protectedUserKey not found. Could not set password.");
}
// Since this is an existing JIT provisioned user in a MP encryption org setting first password,
// they will not already have a user asymmetric key pair so we must create it for them.
const [keyPair, keysRequest] = await this.makeKeyPairAndRequest(protectedUserKey);
const request = new SetPasswordRequest(
newServerMasterKeyHash,
protectedUserKey[1].encryptedString,
newPasswordHint,
orgSsoIdentifier,
keysRequest,
kdfConfig.kdfType,
kdfConfig.iterations,
);
await this.masterPasswordApiService.setPassword(request);
// Clear force set password reason to allow navigation back to vault.
await this.masterPasswordService.setForceSetPasswordReason(ForceSetPasswordReason.None, userId);
// User now has a password so update account decryption options in state
await this.updateAccountDecryptionProperties(newMasterKey, kdfConfig, protectedUserKey, userId);
await this.keyService.setPrivateKey(keyPair[1].encryptedString, userId);
await this.masterPasswordService.setMasterKeyHash(newLocalMasterKeyHash, userId);
if (resetPasswordAutoEnroll) {
await this.handleResetPasswordAutoEnroll(newServerMasterKeyHash, orgId, userId);
}
}
private async makeProtectedUserKey(
masterKey: MasterKey,
userId: UserId,
): Promise<[UserKey, EncString]> {
let protectedUserKey: [UserKey, EncString] = null;
const userKey = await firstValueFrom(this.keyService.userKey$(userId));
if (userKey == null) {
protectedUserKey = await this.keyService.makeUserKey(masterKey);
} else {
protectedUserKey = await this.keyService.encryptUserKeyWithMasterKey(masterKey);
}
return protectedUserKey;
}
private async makeKeyPairAndRequest(
protectedUserKey: [UserKey, EncString],
): Promise<[[string, EncString], KeysRequest]> {
const keyPair = await this.keyService.makeKeyPair(protectedUserKey[0]);
if (keyPair == null) {
throw new Error("keyPair not found. Could not set password.");
}
const keysRequest = new KeysRequest(keyPair[0], keyPair[1].encryptedString);
return [keyPair, keysRequest];
}
private async updateAccountDecryptionProperties(
masterKey: MasterKey,
kdfConfig: KdfConfig,
protectedUserKey: [UserKey, EncString],
userId: UserId,
) {
const userDecryptionOpts = await firstValueFrom(
this.userDecryptionOptionsService.userDecryptionOptions$,
);
userDecryptionOpts.hasMasterPassword = true;
await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts);
await this.kdfConfigService.setKdfConfig(userId, kdfConfig);
await this.masterPasswordService.setMasterKey(masterKey, userId);
await this.keyService.setUserKey(protectedUserKey[0], userId);
}
private async handleResetPasswordAutoEnroll(
masterKeyHash: string,
orgId: string,
userId: UserId,
) {
const organizationKeys = await this.organizationApiService.getKeys(orgId);
if (organizationKeys == null) {
throw new Error(this.i18nService.t("resetPasswordOrgKeysError"));
}
const publicKey = Utils.fromB64ToArray(organizationKeys.publicKey);
// RSA Encrypt user key with organization public key
const userKey = await firstValueFrom(this.keyService.userKey$(userId));
if (userKey == null) {
throw new Error("userKey not found. Could not handle reset password auto enroll.");
}
const encryptedUserKey = await this.encryptService.encapsulateKeyUnsigned(userKey, publicKey);
const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest();
resetRequest.masterPasswordHash = masterKeyHash;
resetRequest.resetPasswordKey = encryptedUserKey.encryptedString;
await this.organizationUserApiService.putOrganizationUserResetPasswordEnrollment(
orgId,
userId,
resetRequest,
);
}
}

View File

@@ -1,24 +0,0 @@
<ng-container *ngIf="syncLoading">
<i class="bwi bwi-spinner bwi-spin tw-mr-2" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
{{ "loading" | i18n }}
</ng-container>
<ng-container *ngIf="!syncLoading">
<app-callout
type="warning"
title="{{ 'resetPasswordPolicyAutoEnroll' | i18n }}"
*ngIf="resetPasswordAutoEnroll"
>
{{ "resetPasswordAutoEnrollInviteWarning" | i18n }}
</app-callout>
<auth-input-password
[flow]="inputPasswordFlow"
[email]="email"
[userId]="userId"
[loading]="submitting"
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
[primaryButtonText]="{ key: 'createAccount' }"
(onPasswordFormSubmit)="handlePasswordFormSubmit($event)"
></auth-input-password>
</ng-container>

View File

@@ -1,135 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CommonModule } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { UserId } from "@bitwarden/common/types/guid";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { ToastService } from "../../../../components/src/toast";
import {
InputPasswordComponent,
InputPasswordFlow,
} from "../input-password/input-password.component";
import { PasswordInputResult } from "../input-password/password-input-result";
import {
SetPasswordCredentials,
SetPasswordJitService,
} from "./set-password-jit.service.abstraction";
@Component({
selector: "auth-set-password-jit",
templateUrl: "set-password-jit.component.html",
imports: [CommonModule, InputPasswordComponent, JslibModule],
})
export class SetPasswordJitComponent implements OnInit {
protected inputPasswordFlow = InputPasswordFlow.SetInitialPasswordAuthedUser;
protected email: string;
protected masterPasswordPolicyOptions: MasterPasswordPolicyOptions;
protected orgId: string;
protected orgSsoIdentifier: string;
protected resetPasswordAutoEnroll: boolean;
protected submitting = false;
protected syncLoading = true;
protected userId: UserId;
constructor(
private accountService: AccountService,
private activatedRoute: ActivatedRoute,
private i18nService: I18nService,
private organizationApiService: OrganizationApiServiceAbstraction,
private policyApiService: PolicyApiServiceAbstraction,
private router: Router,
private setPasswordJitService: SetPasswordJitService,
private syncService: SyncService,
private toastService: ToastService,
private validationService: ValidationService,
) {}
async ngOnInit() {
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
this.userId = activeAccount?.id;
this.email = activeAccount?.email;
await this.syncService.fullSync(true);
this.syncLoading = false;
await this.handleQueryParams();
}
private async handleQueryParams() {
const qParams = await firstValueFrom(this.activatedRoute.queryParams);
if (qParams.identifier != null) {
try {
this.orgSsoIdentifier = qParams.identifier;
const autoEnrollStatus = await this.organizationApiService.getAutoEnrollStatus(
this.orgSsoIdentifier,
);
this.orgId = autoEnrollStatus.id;
this.resetPasswordAutoEnroll = autoEnrollStatus.resetPasswordEnabled;
this.masterPasswordPolicyOptions =
await this.policyApiService.getMasterPasswordPolicyOptsForOrgUser(autoEnrollStatus.id);
} catch {
this.toastService.showToast({
variant: "error",
title: null,
message: this.i18nService.t("errorOccurred"),
});
}
}
}
protected async handlePasswordFormSubmit(passwordInputResult: PasswordInputResult) {
this.submitting = true;
const credentials: SetPasswordCredentials = {
newMasterKey: passwordInputResult.newMasterKey,
newServerMasterKeyHash: passwordInputResult.newServerMasterKeyHash,
newLocalMasterKeyHash: passwordInputResult.newLocalMasterKeyHash,
newPasswordHint: passwordInputResult.newPasswordHint,
kdfConfig: passwordInputResult.kdfConfig,
orgSsoIdentifier: this.orgSsoIdentifier,
orgId: this.orgId,
resetPasswordAutoEnroll: this.resetPasswordAutoEnroll,
userId: this.userId,
};
try {
await this.setPasswordJitService.setPassword(credentials);
} catch (e) {
this.validationService.showError(e);
this.submitting = false;
return;
}
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("accountSuccessfullyCreated"),
});
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("inviteAccepted"),
});
this.submitting = false;
await this.router.navigate(["vault"]);
}
}

View File

@@ -1,33 +0,0 @@
import { UserId } from "@bitwarden/common/types/guid";
import { MasterKey } from "@bitwarden/common/types/key";
import { KdfConfig } from "@bitwarden/key-management";
export interface SetPasswordCredentials {
newMasterKey: MasterKey;
newServerMasterKeyHash: string;
newLocalMasterKeyHash: string;
newPasswordHint: string;
kdfConfig: KdfConfig;
orgSsoIdentifier: string;
orgId: string;
resetPasswordAutoEnroll: boolean;
userId: UserId;
}
/**
* This service handles setting a password for a "just-in-time" provisioned user.
*
* A "just-in-time" (JIT) provisioned user is a user who does not have a registered account at the
* time they first click "Login with SSO". Once they click "Login with SSO" we register the account on
* the fly ("just-in-time").
*/
export abstract class SetPasswordJitService {
/**
* Sets the password for a JIT provisioned user.
*
* @param credentials An object of the credentials needed to set the password for a JIT provisioned user
* @throws If any property on the `credentials` object is null or undefined, or if a protectedUserKey
* or newKeyPair could not be created.
*/
abstract setPassword(credentials: SetPasswordCredentials): Promise<void>;
}

View File

@@ -23,12 +23,10 @@ import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { SsoPreValidateResponse } from "@bitwarden/common/auth/models/response/sso-pre-validate.response";
import { ClientType, HttpStatusCode } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
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";
@@ -118,7 +116,6 @@ export class SsoComponent implements OnInit {
private toastService: ToastService,
private ssoComponentService: SsoComponentService,
private loginSuccessHandlerService: LoginSuccessHandlerService,
private configService: ConfigService,
) {
environmentService.environment$.pipe(takeUntilDestroyed()).subscribe((env) => {
this.redirectUri = env.getWebVaultUrl() + "/sso-connector.html";
@@ -534,11 +531,7 @@ export class SsoComponent implements OnInit {
}
private async handleChangePasswordRequired(orgIdentifier: string) {
const isSetInitialPasswordRefactorFlagOn = await this.configService.getFeatureFlag(
FeatureFlag.PM16117_SetInitialPasswordRefactor,
);
const route = isSetInitialPasswordRefactorFlagOn ? "set-initial-password" : "set-password-jit";
const route = "set-initial-password";
await this.router.navigate([route], {
queryParams: {
identifier: orgIdentifier,

View File

@@ -2,7 +2,7 @@ import { Component } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { ActivatedRoute, Router, convertToParamMap } from "@angular/router";
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
import { BehaviorSubject, of } from "rxjs";
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import {
@@ -24,8 +24,10 @@ import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service";
import {
InternalMasterPasswordServiceAbstraction,
MasterPasswordServiceAbstraction,
} from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
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";
@@ -66,7 +68,7 @@ describe("TwoFactorAuthComponent", () => {
let mockLoginEmailService: MockProxy<LoginEmailServiceAbstraction>;
let mockUserDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>;
let mockSsoLoginService: MockProxy<SsoLoginServiceAbstraction>;
let mockMasterPasswordService: FakeMasterPasswordService;
let mockMasterPasswordService: MockProxy<InternalMasterPasswordServiceAbstraction>;
let mockAccountService: FakeAccountService;
let mockDialogService: MockProxy<DialogService>;
let mockToastService: MockProxy<ToastService>;
@@ -107,7 +109,7 @@ describe("TwoFactorAuthComponent", () => {
mockUserDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
mockSsoLoginService = mock<SsoLoginServiceAbstraction>();
mockAccountService = mockAccountServiceWith(userId);
mockMasterPasswordService = new FakeMasterPasswordService();
mockMasterPasswordService = mock<InternalMasterPasswordServiceAbstraction>();
mockDialogService = mock<DialogService>();
mockToastService = mock<ToastService>();
mockTwoFactorAuthCompService = mock<TwoFactorAuthComponentService>();
@@ -212,6 +214,7 @@ describe("TwoFactorAuthComponent", () => {
},
{ provide: AuthService, useValue: mockAuthService },
{ provide: ConfigService, useValue: mockConfigService },
{ provide: MasterPasswordServiceAbstraction, useValue: mockMasterPasswordService },
],
});
@@ -267,54 +270,16 @@ describe("TwoFactorAuthComponent", () => {
selectedUserDecryptionOptions.next(mockUserDecryptionOpts.noMasterPassword);
});
describe("Given the PM16117_SetInitialPasswordRefactor feature flag is ON", () => {
it("navigates to the /set-initial-password route when user doesn't have a MP and key connector isn't enabled", async () => {
// Arrange
mockConfigService.getFeatureFlag.mockResolvedValue(true);
// Act
await component.submit("testToken");
// Assert
expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
expect(mockRouter.navigate).toHaveBeenCalledWith(["set-initial-password"], {
queryParams: {
identifier: component.orgSsoIdentifier,
},
});
});
});
describe("Given the PM16117_SetInitialPasswordRefactor feature flag is OFF", () => {
it("navigates to the /set-password route when user doesn't have a MP and key connector isn't enabled", async () => {
// Arrange
mockConfigService.getFeatureFlag.mockResolvedValue(false);
// Act
await component.submit("testToken");
// Assert
expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
expect(mockRouter.navigate).toHaveBeenCalledWith(["set-password"], {
queryParams: {
identifier: component.orgSsoIdentifier,
},
});
});
});
});
describe("Given the PM16117_SetInitialPasswordRefactor feature flag is ON", () => {
it("does not navigate to the /set-initial-password route when the user has key connector even if user has no master password", async () => {
it("navigates to the /set-initial-password route when user doesn't have a MP and key connector isn't enabled", async () => {
// Arrange
mockConfigService.getFeatureFlag.mockResolvedValue(true);
selectedUserDecryptionOptions.next(
mockUserDecryptionOpts.noMasterPasswordWithKeyConnector,
);
// Act
await component.submit("testToken");
await component.submit(token, remember);
expect(mockRouter.navigate).not.toHaveBeenCalledWith(["set-initial-password"], {
// Assert
expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
expect(mockRouter.navigate).toHaveBeenCalledWith(["set-initial-password"], {
queryParams: {
identifier: component.orgSsoIdentifier,
},
@@ -322,21 +287,19 @@ describe("TwoFactorAuthComponent", () => {
});
});
describe("Given the PM16117_SetInitialPasswordRefactor feature flag is OFF", () => {
it("does not navigate to the /set-password route when the user has key connector even if user has no master password", async () => {
mockConfigService.getFeatureFlag.mockResolvedValue(false);
it("does not navigate to the /set-initial-password route when the user has key connector even if user has no master password", async () => {
mockConfigService.getFeatureFlag.mockResolvedValue(true);
selectedUserDecryptionOptions.next(
mockUserDecryptionOpts.noMasterPasswordWithKeyConnector,
);
selectedUserDecryptionOptions.next(
mockUserDecryptionOpts.noMasterPasswordWithKeyConnector,
);
await component.submit(token, remember);
await component.submit(token, remember);
expect(mockRouter.navigate).not.toHaveBeenCalledWith(["set-password"], {
queryParams: {
identifier: component.orgSsoIdentifier,
},
});
expect(mockRouter.navigate).not.toHaveBeenCalledWith(["set-initial-password"], {
queryParams: {
identifier: component.orgSsoIdentifier,
},
});
});
});
@@ -344,6 +307,9 @@ describe("TwoFactorAuthComponent", () => {
it("navigates to the component's defined success route (vault is default) when the login is successful", async () => {
mockLoginStrategyService.logInTwoFactor.mockResolvedValue(new AuthResult());
mockAuthService.activeAccountStatus$ = new BehaviorSubject(AuthenticationStatus.Unlocked);
mockMasterPasswordService.forceSetPasswordReason$.mockReturnValue(
of(ForceSetPasswordReason.None),
);
// Act
await component.submit("testToken");
@@ -409,7 +375,7 @@ describe("TwoFactorAuthComponent", () => {
await component.submit(token, remember);
// Assert
expect(mockMasterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith(
expect(mockMasterPasswordService.setForceSetPasswordReason).toHaveBeenCalledWith(
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission,
userId,
);

View File

@@ -17,7 +17,6 @@ import { JslibModule } from "@bitwarden/angular/jslib.module";
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import {
LoginStrategyServiceAbstraction,
LoginEmailServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
TrustedDeviceUserDecryptionOption,
UserDecryptionOptions,
@@ -32,9 +31,7 @@ import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-p
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
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";
@@ -156,7 +153,6 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
private activatedRoute: ActivatedRoute,
private logService: LogService,
private twoFactorService: TwoFactorService,
private loginEmailService: LoginEmailServiceAbstraction,
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
private ssoLoginService: SsoLoginServiceAbstraction,
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
@@ -171,7 +167,6 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
private loginSuccessHandlerService: LoginSuccessHandlerService,
private twoFactorAuthComponentCacheService: TwoFactorAuthComponentCacheService,
private authService: AuthService,
private configService: ConfigService,
) {}
async ngOnInit() {
@@ -507,19 +502,15 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
}
// TODO: PM-22663 use the new service to handle routing.
if (
await this.configService.getFeatureFlag(FeatureFlag.PM16117_ChangeExistingPasswordRefactor)
) {
const forceSetPasswordReason = await firstValueFrom(
this.masterPasswordService.forceSetPasswordReason$(userId),
);
const forceSetPasswordReason = await firstValueFrom(
this.masterPasswordService.forceSetPasswordReason$(userId),
);
if (
forceSetPasswordReason === ForceSetPasswordReason.WeakMasterPassword ||
forceSetPasswordReason === ForceSetPasswordReason.AdminForcePasswordReset
) {
return "change-password";
}
if (
forceSetPasswordReason === ForceSetPasswordReason.WeakMasterPassword ||
forceSetPasswordReason === ForceSetPasswordReason.AdminForcePasswordReset
) {
return "change-password";
}
return "vault";
@@ -575,11 +566,7 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
}
private async handleChangePasswordRequired(orgIdentifier: string | undefined) {
const isSetInitialPasswordRefactorFlagOn = await this.configService.getFeatureFlag(
FeatureFlag.PM16117_SetInitialPasswordRefactor,
);
const route = isSetInitialPasswordRefactorFlagOn ? "set-initial-password" : "set-password";
const route = "set-initial-password";
await this.router.navigate([route], {
queryParams: {
identifier: orgIdentifier,

View File

@@ -12,7 +12,6 @@ import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/id
import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response";
import { MasterPasswordPolicyResponse } from "@bitwarden/common/auth/models/response/master-password-policy.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 { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service";
import {
@@ -221,7 +220,10 @@ describe("PasswordLoginStrategy", () => {
await passwordLoginStrategy.logIn(credentials);
expect(policyService.evaluateMasterPassword).not.toHaveBeenCalled();
expect(masterPasswordService.mock.setForceSetPasswordReason).not.toHaveBeenCalledWith(
ForceSetPasswordReason.WeakMasterPassword,
userId,
);
});
it("does not force the user to update their master password when it meets requirements", async () => {
@@ -230,7 +232,10 @@ describe("PasswordLoginStrategy", () => {
await passwordLoginStrategy.logIn(credentials);
expect(policyService.evaluateMasterPassword).toHaveBeenCalled();
expect(masterPasswordService.mock.setForceSetPasswordReason).not.toHaveBeenCalledWith(
ForceSetPasswordReason.WeakMasterPassword,
userId,
);
});
it("when given master password policies as part of the login credentials from an org invite, it combines them with the token response policies to evaluate the user's password as weak", async () => {
@@ -242,12 +247,6 @@ describe("PasswordLoginStrategy", () => {
policyService.evaluateMasterPassword.mockReturnValue(false);
tokenService.decodeAccessToken.mockResolvedValue({ sub: userId });
jest
.spyOn(configService, "getFeatureFlag")
.mockImplementation((flag: FeatureFlag) =>
Promise.resolve(flag === FeatureFlag.PM16117_ChangeExistingPasswordRefactor),
);
credentials.masterPasswordPoliciesFromOrgInvite = Object.assign(
new MasterPasswordPolicyOptions(),
{
@@ -296,9 +295,16 @@ describe("PasswordLoginStrategy", () => {
it("forces the user to update their master password on successful login when it does not meet master password policy requirements", async () => {
passwordStrengthService.getPasswordStrength.mockReturnValue({ score: 0 } as any);
policyService.evaluateMasterPassword.mockReturnValue(false);
tokenService.decodeAccessToken.mockResolvedValue({ sub: userId });
const combinedMasterPasswordPolicyOptions = Object.assign(new MasterPasswordPolicyOptions(), {
enforceOnLogin: true,
});
policyService.combineMasterPasswordPolicyOptions.mockReturnValue(
combinedMasterPasswordPolicyOptions,
);
policyService.evaluateMasterPassword.mockReturnValue(false);
await passwordLoginStrategy.logIn(credentials);
expect(policyService.evaluateMasterPassword).toHaveBeenCalled();
@@ -330,9 +336,16 @@ describe("PasswordLoginStrategy", () => {
it("forces the user to update their master password on successful 2FA login when it does not meet master password policy requirements", async () => {
passwordStrengthService.getPasswordStrength.mockReturnValue({ score: 0 } as any);
policyService.evaluateMasterPassword.mockReturnValue(false);
tokenService.decodeAccessToken.mockResolvedValue({ sub: userId });
const combinedMasterPasswordPolicyOptions = Object.assign(new MasterPasswordPolicyOptions(), {
enforceOnLogin: true,
});
policyService.combineMasterPasswordPolicyOptions.mockReturnValue(
combinedMasterPasswordPolicyOptions,
);
policyService.evaluateMasterPassword.mockReturnValue(false);
const token2FAResponse = new IdentityTwoFactorResponse({
TwoFactorProviders: ["0"],
TwoFactorProviders2: { 0: null },

View File

@@ -12,7 +12,6 @@ import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/ide
import { IdentityDeviceVerificationResponse } from "@bitwarden/common/auth/models/response/identity-device-verification.response";
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { HashPurpose } from "@bitwarden/common/platform/enums";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
@@ -171,35 +170,22 @@ export class PasswordLoginStrategy extends LoginStrategy {
return;
}
// The identity result can contain master password policies for the user's organizations
let masterPasswordPolicyOptions: MasterPasswordPolicyOptions | undefined;
// The identity result can contain master password policies for the user's organizations.
// Get the master password policy options from both the org invite and the identity response.
const masterPasswordPolicyOptions = this.policyService.combineMasterPasswordPolicyOptions(
credentials.masterPasswordPoliciesFromOrgInvite,
this.getMasterPasswordPolicyOptionsFromResponse(identityResponse),
);
// We deliberately do not check enforceOnLogin as existing users who are logging
// in after getting an org invite should always be forced to set a password that
// meets the org's policy. Org Invite -> Registration also works this way for
// new BW users as well.
if (
await this.configService.getFeatureFlag(FeatureFlag.PM16117_ChangeExistingPasswordRefactor)
!credentials.masterPasswordPoliciesFromOrgInvite &&
!masterPasswordPolicyOptions?.enforceOnLogin
) {
// Get the master password policy options from both the org invite and the identity response.
masterPasswordPolicyOptions = this.policyService.combineMasterPasswordPolicyOptions(
credentials.masterPasswordPoliciesFromOrgInvite,
this.getMasterPasswordPolicyOptionsFromResponse(identityResponse),
);
// We deliberately do not check enforceOnLogin as existing users who are logging
// in after getting an org invite should always be forced to set a password that
// meets the org's policy. Org Invite -> Registration also works this way for
// new BW users as well.
if (
!credentials.masterPasswordPoliciesFromOrgInvite &&
!masterPasswordPolicyOptions?.enforceOnLogin
) {
return;
}
} else {
masterPasswordPolicyOptions =
this.getMasterPasswordPolicyOptionsFromResponse(identityResponse);
if (!masterPasswordPolicyOptions?.enforceOnLogin) {
return;
}
return;
}
// If there is a policy active, evaluate the supplied password before its no longer in memory

View File

@@ -10,7 +10,6 @@ import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth
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 { EncryptedString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
@@ -83,6 +82,7 @@ describe("SsoLoginStrategy", () => {
const ssoCodeVerifier = "SSO_CODE_VERIFIER";
const ssoRedirectUrl = "SSO_REDIRECT_URL";
const ssoOrgId = "SSO_ORG_ID";
const privateKey = "userKeyEncryptedPrivateKey";
beforeEach(async () => {
accountService = mockAccountServiceWith(userId);
@@ -114,6 +114,9 @@ describe("SsoLoginStrategy", () => {
tokenService.decodeAccessToken.mockResolvedValue({
sub: userId,
});
keyService.userEncryptedPrivateKey$
.calledWith(userId)
.mockReturnValue(of(privateKey as EncryptedString));
const mockVaultTimeoutAction = VaultTimeoutAction.Lock;
const mockVaultTimeoutActionBSub = new BehaviorSubject<VaultTimeoutAction>(
@@ -163,6 +166,7 @@ describe("SsoLoginStrategy", () => {
it("sends SSO information to server", async () => {
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory());
keyService.hasUserKey.mockResolvedValue(true);
await ssoLoginStrategy.logIn(credentials);
@@ -185,6 +189,7 @@ describe("SsoLoginStrategy", () => {
it("does not set keys for new SSO user flow", async () => {
const tokenResponse = identityTokenResponseFactory();
tokenResponse.key = null;
tokenResponse.privateKey = null;
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
await ssoLoginStrategy.logIn(credentials);
@@ -210,42 +215,28 @@ 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);
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.hasUserKey.mockResolvedValue(false);
keyService.userEncryptedPrivateKey$.mockReturnValue(
of("userKeyEncryptedPrivateKey" as EncryptedString),
);
keyService.hasUserKey.mockResolvedValue(false);
// Act
await ssoLoginStrategy.logIn(credentials);
// Act
await ssoLoginStrategy.logIn(credentials);
// Assert
expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledTimes(1);
expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith(
ForceSetPasswordReason.TdeOffboardingUntrustedDevice,
userId,
);
});
// Assert
expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledTimes(1);
expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith(
ForceSetPasswordReason.TdeOffboardingUntrustedDevice,
userId,
);
});
});

View File

@@ -9,7 +9,6 @@ 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";
@@ -344,38 +343,18 @@ export class SsoLoginStrategy extends LoginStrategy {
tokenResponse: IdentityTokenResponse,
userId: UserId,
): Promise<void> {
const isSetInitialPasswordFlagOn = await this.configService.getFeatureFlag(
FeatureFlag.PM16117_SetInitialPasswordRefactor,
);
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,
);
}
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);
}
}
@@ -431,30 +410,25 @@ export class SsoLoginStrategy extends LoginStrategy {
// - 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,
// Why is that the case? Because we set the userKeyEncryptedPrivateKey when we create the userKey, and this is serving as a proxy to tell us that the userKey has been created already (when enrolling in TDE).
const hasUserKeyEncryptedPrivateKey = await firstValueFrom(
this.keyService.userEncryptedPrivateKey$(userId),
);
const hasUserKey = await this.keyService.hasUserKey(userId);
if (isSetInitialPasswordFlagOn) {
const hasUserKeyEncryptedPrivateKey = await firstValueFrom(
this.keyService.userEncryptedPrivateKey$(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,
);
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;
}
return true;
}
// Check if user has permission to set password but hasn't yet

View File

@@ -89,8 +89,7 @@ export class DefaultPolicyService implements PolicyService {
const policies$ = policies ? of(policies) : this.policies$(userId);
return policies$.pipe(
map((obsPolicies) => {
// TODO: replace with this.combinePoliciesIntoMasterPasswordPolicyOptions(obsPolicies)) once
// FeatureFlag.PM16117_ChangeExistingPasswordRefactor is removed.
// TODO ([PM-23777]): replace with this.combinePoliciesIntoMasterPasswordPolicyOptions(obsPolicies))
let enforcedOptions: MasterPasswordPolicyOptions | undefined = undefined;
const filteredPolicies =
obsPolicies.filter((p) => p.type === PolicyType.MasterPassword) ?? [];

View File

@@ -14,8 +14,6 @@ export enum FeatureFlag {
CreateDefaultLocation = "pm-19467-create-default-location",
/* Auth */
PM16117_SetInitialPasswordRefactor = "pm-16117-set-initial-password-refactor",
PM16117_ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor",
PM14938_BrowserExtensionLoginApproval = "pm-14938-browser-extension-login-approvals",
/* Autofill */
@@ -107,8 +105,6 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.PM22136_SdkCipherEncryption]: FALSE,
/* Auth */
[FeatureFlag.PM16117_SetInitialPasswordRefactor]: FALSE,
[FeatureFlag.PM16117_ChangeExistingPasswordRefactor]: FALSE,
[FeatureFlag.PM14938_BrowserExtensionLoginApproval]: FALSE,
/* Billing */