Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking โ€œSign up for GitHubโ€, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-15605] Add new device protection opt out #12880

Merged
Merged
Changes from 1 commit
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
fdf203f
feat(newdeviceVerificaiton) : adding component and request model
ike-kottlowski Jan 8, 2025
707675b
Merge branch 'main' into auth/pm-15605/ike-wip
ike-kottlowski Jan 8, 2025
d900890
feat(newDeviceverification) : adding state structure to track verify โ€ฆ
ike-kottlowski Jan 10, 2025
ed61bfd
feat(newDeviceVerification) : added visual elements for opting out ofโ€ฆ
ike-kottlowski Jan 15, 2025
3b138c8
Merge branch 'main' into auth/pm-15605/add-opt-out-new-device-protection
ike-kottlowski Jan 15, 2025
b5627e4
Fixing tests for account service.
ike-kottlowski Jan 16, 2025
f447825
Fixing strict lint issues
ike-kottlowski Jan 16, 2025
2f9da33
debt(deauthorizeSessionsModal) : changed modal to dialog. fixed stricโ€ฆ
ike-kottlowski Jan 18, 2025
93143b0
fixing tests
ike-kottlowski Jan 20, 2025
23cfc1d
fixing desktop build DI
ike-kottlowski Jan 20, 2025
209c353
Merge branch 'main' into auth/pm-15605/add-opt-out-new-device-protection
ike-kottlowski Jan 20, 2025
10e9ea8
changed dialog to standalone fixed names and comments.
ike-kottlowski Jan 22, 2025
f4fafb6
Adding tests for AccountService
ike-kottlowski Jan 24, 2025
e66b067
Merge branch 'main' into auth/pm-15605/add-opt-out-new-device-protection
ike-kottlowski Jan 24, 2025
b851720
Merge branch 'main' into auth/pm-15605/add-opt-out-new-device-protection
JaredSnider-Bitwarden Jan 24, 2025
95a6b76
fix linting
ike-kottlowski Jan 24, 2025
467fdcb
Merge remote-tracking branch 'origin/main' into auth/pm-15605/add-optโ€ฆ
JaredSnider-Bitwarden Jan 28, 2025
1607957
PM-15605 - AccountComp - fix ngOnDestroy erroring as it was incorrectโ€ฆ
JaredSnider-Bitwarden Jan 28, 2025
1344e73
PM-15605 - SetAccountVerifyDevicesDialogComponent - only show warningโ€ฆ
JaredSnider-Bitwarden Jan 28, 2025
bf824e8
Merge branch 'main' into auth/pm-15605/add-opt-out-new-device-protection
JaredSnider-Bitwarden Jan 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat(newDeviceVerification) : added visual elements for opting out ofโ€ฆ
โ€ฆ new device verification.
ike-kottlowski committed Jan 15, 2025
commit ed61bfd359bdd653caab11bdaedf7e7fac9a616e
18 changes: 18 additions & 0 deletions apps/web/src/app/auth/settings/account/account.component.html
Original file line number Diff line number Diff line change
@@ -9,6 +9,24 @@ <h1 bitTypography="h1">{{ "changeEmail" | i18n }}</h1>
</div>

<app-danger-zone>
<button
*ngIf="verifyNewDeviceLogin && showSetNewDeviceLoginProtection$ | async"
type="button"
bitButton
buttonType="danger"
[bitAction]="setNewDeviceLoginProtection"
>
{{ "turnOffNewDeviceLoginProtection" | i18n }}
</button>
<button
*ngIf="!verifyNewDeviceLogin && showSetNewDeviceLoginProtection$ | async"
type="button"
bitButton
buttonType="secondary"
[bitAction]="setNewDeviceLoginProtection"
>
{{ "turnOnNewDeviceLoginProtection" | i18n }}
</button>
JaredSnider-Bitwarden marked this conversation as resolved.
Show resolved Hide resolved
<button type="button" bitButton buttonType="danger" (click)="deauthorizeSessions()">
{{ "deauthorizeSessions" | i18n }}
</button>
33 changes: 28 additions & 5 deletions apps/web/src/app/auth/settings/account/account.component.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { combineLatest, from, lastValueFrom, map, Observable } from "rxjs";
import { Component, OnInit, ViewChild, ViewContainerRef, OnDestroy } from "@angular/core";
import { combineLatest, from, lastValueFrom, map, Observable, Subject, takeUntil } from "rxjs";

import { ModalService } from "@bitwarden/angular/services/modal.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
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";
@@ -14,28 +13,37 @@ import { PurgeVaultComponent } from "../../../vault/settings/purge-vault.compone

import { DeauthorizeSessionsComponent } from "./deauthorize-sessions.component";
import { DeleteAccountDialogComponent } from "./delete-account-dialog.component";
import { SetAccountVerifyDevicesDialogComponent } from "./set-account-verify-devices-dialog.component";

@Component({
selector: "app-account",
templateUrl: "account.component.html",
})
export class AccountComponent implements OnInit {
export class AccountComponent implements OnInit, OnDestroy {
@ViewChild("deauthorizeSessionsTemplate", { read: ViewContainerRef, static: true })
deauthModalRef: ViewContainerRef;
private destroy$ = new Subject<void>();

showChangeEmail$: Observable<boolean>;
showPurgeVault$: Observable<boolean>;
showDeleteAccount$: Observable<boolean>;
showSetNewDeviceLoginProtection$: Observable<boolean>;
verifyNewDeviceLogin: boolean;

constructor(
private modalService: ModalService,
private accountService: AccountService,
private dialogService: DialogService,
private userVerificationService: UserVerificationService,
private configService: ConfigService,
private organizationService: OrganizationService,
) {}

async ngOnInit() {
this.showSetNewDeviceLoginProtection$ = this.configService.getFeatureFlag$(
FeatureFlag.NewDeviceVerification,
);

const isAccountDeprovisioningEnabled$ = this.configService.getFeatureFlag$(
FeatureFlag.AccountDeprovisioning,
);
@@ -76,6 +84,11 @@ export class AccountComponent implements OnInit {
!isAccountDeprovisioningEnabled || !userIsManagedByOrganization,
),
);
this.accountService.accountVerifyDevices$
.pipe(takeUntil(this.destroy$))
.subscribe((verifyDevices) => {
this.verifyNewDeviceLogin = verifyDevices;
});
}

async deauthorizeSessions() {
@@ -91,4 +104,14 @@ export class AccountComponent implements OnInit {
const dialogRef = DeleteAccountDialogComponent.open(this.dialogService);
await lastValueFrom(dialogRef.closed);
};

setNewDeviceLoginProtection = async () => {
const dialogRef = SetAccountVerifyDevicesDialogComponent.open(this.dialogService);
await lastValueFrom(dialogRef.closed);
};

ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
<h1 bitTypography="h1" class="tw-mt-16 tw-pb-2.5 !tw-text-danger">{{ "dangerZone" | i18n }}</h1>

<div class="tw-rounded tw-border tw-border-solid tw-border-danger-600 tw-p-5">
<p>
{{
(accountDeprovisioningEnabled$ | async) && content.children.length === 1
? ("dangerZoneDescSingular" | i18n)
: ("dangerZoneDesc" | i18n)
ike-kottlowski marked this conversation as resolved.
Show resolved Hide resolved
}}
</p>

<div #content class="tw-flex tw-flex-row tw-gap-2">
<ng-content></ng-content>
</div>
Original file line number Diff line number Diff line change
@@ -8,7 +8,6 @@ import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-a
import { Verification } from "@bitwarden/common/auth/types/verification";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogService, ToastService } from "@bitwarden/components";

@Component({
@@ -22,7 +21,6 @@ export class DeleteAccountDialogComponent {

constructor(
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private formBuilder: FormBuilder,
private accountApiService: AccountApiService,
private dialogRef: DialogRef,
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
<form [formGroup]="setVerifyDevicesForm" [bitSubmit]="submit">
<bit-dialog dialogSize="default" [title]="'newDeviceLoginProtection' | i18n">
<ng-container bitDialogContent>
<p bitTypography="body1">{{ dialogDesc }}</p>
<bit-callout type="warning">{{
<p *ngIf="verifyNewDeviceLogin" bitTypography="body1">
{{ "turnOffNewDeviceLoginProtectionModalDesc" | i18n }}
</p>
<p *ngIf="!verifyNewDeviceLogin" bitTypography="body1">
{{ "turnOnNewDeviceLoginProtectionModalDesc" | i18n }}
</p>
<bit-callout *ngIf="verifyNewDeviceLogin" type="warning">{{
"turnOffNewDeviceLoginProtectionWarning" | i18n
}}</bit-callout>
<app-user-verification-form-input
@@ -12,8 +17,23 @@
></app-user-verification-form-input>
</ng-container>
<ng-container bitDialogFooter>
<button bitButton bitFormButton type="submit" buttonType="danger">
{{ dialogSubmitButtonDesc }}
<button
bitButton
*ngIf="verifyNewDeviceLogin"
bitFormButton
type="submit"
buttonType="danger"
>
{{ "disable" | i18n }}
</button>
<button
bitButton
*ngIf="!verifyNewDeviceLogin"
bitFormButton
type="submit"
buttonType="primary"
>
{{ "enable" | i18n }}
</button>
<button bitButton bitFormButton type="button" buttonType="secondary" bitDialogClose>
{{ "close" | i18n }}
Original file line number Diff line number Diff line change
@@ -1,57 +1,70 @@
import { DialogRef } from "@angular/cdk/dialog";
import { Component } from "@angular/core";
import { Component, OnDestroy } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { Subject, takeUntil } from "rxjs";

import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { SetVerifyDevicesRequest } from "@bitwarden/common/auth/models/request/set-verify-devices.request";
import { Verification } from "@bitwarden/common/auth/types/verification";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { UserId } from "@bitwarden/common/types/guid";
import { DialogService, ToastService } from "@bitwarden/components";

@Component({
templateUrl: "./set-account-verify-devices-dialog.component.html",
})
export class SetAccountVerifyDevicesDialogComponent {
export class SetAccountVerifyDevicesDialogComponent implements OnDestroy {
// use this subject for all subscriptions to ensure all subscripts are completed
private destroy$ = new Subject<void>();
// the default for new device verification is true
verifyNewDeviceLogin: boolean = true;
activeUserId: UserId;

setVerifyDevicesForm = this.formBuilder.group({
verification: undefined as Verification | undefined,
});
invalidSecret: boolean = false;
verifyNewDeviceLogin: boolean = true;
dialogDesc: string = ""; // on or off
dialogSubmitButtonDesc: string = ""; // on or off
dialogBodyDesc: string = ""; // turn on or off

constructor(
private i18nService: I18nService,
private formBuilder: FormBuilder,
private accountApiService: AccountApiService,
private accountService: AccountService,
private userVerificationService: UserVerificationService,
private dialogRef: DialogRef,
private toastService: ToastService,
) {
//todo set dialog text based on account information
this.verifyNewDeviceLogin = getVerifyDevices;
this.accountService.activeAccount$.pipe(
(a) => (this.verifyNewDeviceLogin = a?.verifyDevices ?? true),
);
this.dialogDesc = this.i18nService.t("accountNewDeviceLoginProtection");
this.dialogSubmitButtonDesc = this.i18nService.t("accountNewDeviceLoginProtectionSave");
this.dialogBodyDesc = this.i18nService.t("accountNewDeviceLoginProtectionDesc");
this.accountService.accountVerifyDevices$
.pipe(takeUntil(this.destroy$))
.subscribe((verifyDevices) => {
this.verifyNewDeviceLogin = verifyDevices;
});
this.accountService.activeAccount$
.pipe(takeUntil(this.destroy$))
.subscribe((account) => (this.activeUserId = account.id));
}

submit = async () => {
try {
// const verification = this.setVerifyDevicesForm.get("verification").value;
//todo create request object
const request: SetVerifyDevicesRequest = null;
const verification = this.setVerifyDevicesForm.get("verification").value;
const request: SetVerifyDevicesRequest = await this.userVerificationService.buildRequest(
verification,
SetVerifyDevicesRequest,
);
// set verify device opposite what is currently is.
request.verifyDevices = !this.verifyNewDeviceLogin;

await this.accountApiService.setVerifyDevices(request);
await this.accountService.setAccountVerifyDevices(this.activeUserId, request.verifyDevices);
this.dialogRef.close();
this.toastService.showToast({
variant: "success",
title: this.i18nService.t("accountNewDeviceLoginProtectionSaved"),
message: this.i18nService.t("accountNewDeviceLoginProtectionSavedDesc"),
title: null,
message: this.i18nService.t("accountNewDeviceLoginProtectionSaved"),
});
} catch (e) {
if (e instanceof ErrorResponse && e.statusCode === 400) {
ike-kottlowski marked this conversation as resolved.
Show resolved Hide resolved
@@ -64,4 +77,10 @@ export class SetAccountVerifyDevicesDialogComponent {
static open(dialogService: DialogService) {
return dialogService.open(SetAccountVerifyDevicesDialogComponent);
}

// closes subscription leaks
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
2 changes: 2 additions & 0 deletions apps/web/src/app/shared/loose-components.module.ts
Original file line number Diff line number Diff line change
@@ -30,6 +30,7 @@ import { DangerZoneComponent } from "../auth/settings/account/danger-zone.compon
import { DeauthorizeSessionsComponent } from "../auth/settings/account/deauthorize-sessions.component";
import { DeleteAccountDialogComponent } from "../auth/settings/account/delete-account-dialog.component";
import { ProfileComponent } from "../auth/settings/account/profile.component";
import { SetAccountVerifyDevicesDialogComponent } from "../auth/settings/account/set-account-verify-devices-dialog.component";
import { EmergencyAccessAttachmentsComponent } from "../auth/settings/emergency-access/attachments/emergency-access-attachments.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";
@@ -155,6 +156,7 @@ import { SharedModule } from "./shared.module";
SecurityKeysComponent,
SelectableAvatarComponent,
SendAddEditComponent,
SetAccountVerifyDevicesDialogComponent,
ike-kottlowski marked this conversation as resolved.
Show resolved Hide resolved
SetPasswordComponent,
SponsoredFamiliesComponent,
SponsoringOrgRowComponent,
23 changes: 22 additions & 1 deletion apps/web/src/locales/en/messages.json
Original file line number Diff line number Diff line change
@@ -1726,7 +1726,7 @@
},
"requestPending": {
"message": "Request pending"
},
},
"logBackInOthersToo": {
"message": "Please log back in. If you are using other Bitwarden applications log out and back in to those as well."
},
@@ -1809,6 +1809,27 @@
"deauthorizeSessionsWarning": {
"message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour."
},
"newDeviceLoginProtection": {
"message":"New device login"
},
"turnOffNewDeviceLoginProtection": {
"message":"Turn off new device login protection"
},
"turnOnNewDeviceLoginProtection": {
"message":"Turn on new device login protection"
},
"turnOffNewDeviceLoginProtectionModalDesc": {
"message":"Proceed below to turn off the verification emails bitwarden sends when you login from a new device."
},
"turnOnNewDeviceLoginProtectionModalDesc": {
"message":"Proceed below to have bitwarden send you verification emails when you login from a new device."
},
"turnOffNewDeviceLoginProtectionWarning": {
"message":"With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login."
},
"accountNewDeviceLoginProtectionSaved": {
"message": "New device login protection changes saved"
},
"sessionsDeauthorized": {
"message": "All sessions deauthorized"
},
2 changes: 1 addition & 1 deletion libs/angular/src/services/jslib-services.module.ts
Original file line number Diff line number Diff line change
@@ -551,7 +551,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: InternalAccountService,
useClass: AccountServiceImplementation,
deps: [MessagingServiceAbstraction, LogService, GlobalStateProvider],
deps: [MessagingServiceAbstraction, LogService, GlobalStateProvider, SingleUserStateProvider],
}),
safeProvider({
provide: AccountServiceAbstraction,
Loading