1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-22 19:23:52 +00:00

Auth/ps 2298 reorg auth (#4564)

* Move auth service factories to Auth team

* Move authentication componenets to Auth team

* Move auth guard services to Auth team

* Move Duo content script to Auth team

* Move auth CLI commands to Auth team

* Move Desktop Account components to Auth Team

* Move Desktop guards to Auth team

* Move two-factor provider images to Auth team

* Move web Accounts components to Auth Team

* Move web settings components to Auth Team

* Move web two factor images to Auth Team

* Fix missed import changes for Auth Team

* Fix Linting errors

* Fix missed CLI imports

* Fix missed Desktop imports

* Revert images move

* Fix missed imports in Web

* Move angular lib components to Auth Team

* Move angular auth guards to Auth team

* Move strategy specs to Auth team

* Update .eslintignore for new paths

* Move lib common abstractions to Auth team

* Move services to Auth team

* Move common lib enums to Auth team

* Move webauthn iframe to Auth team

* Move lib common domain models to Auth team

* Move common lib requests to Auth team

* Move response models to Auth team

* Clean up whitelist

* Move bit web components to Auth team

* Move SSO and SCIM files to Auth team

* Revert move SCIM to Auth team

SCIM belongs to Admin Console team

* Move captcha to Auth team

* Move key connector to Auth team

* Move emergency access to auth team

* Delete extra file

* linter fixes

* Move kdf config to auth team

* Fix whitelist

* Fix duo autoformat

* Complete two factor provider request move

* Fix whitelist names

* Fix login capitalization

* Revert hint dependency reordering

* Revert hint dependency reordering

* Revert hint component

This components is being picked up as a move between clients

* Move web hint component to Auth team

* Move new files to auth team

* Fix desktop build

* Fix browser build
This commit is contained in:
Matt Gibson
2023-02-06 16:53:37 -05:00
committed by GitHub
parent 084c89107e
commit cf972e784c
377 changed files with 1030 additions and 998 deletions

View File

@@ -2,10 +2,11 @@ import { Component, ViewChild, ViewContainerRef } from "@angular/core";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { KeyConnectorService } from "@bitwarden/common/abstractions/keyConnector.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { DeauthorizeSessionsComponent } from "../../auth/settings/deauthorize-sessions.component";
import { DeauthorizeSessionsComponent } from "./deauthorize-sessions.component";
import { DeleteAccountComponent } from "./delete-account.component";
import { PurgeVaultComponent } from "./purge-vault.component";

View File

@@ -2,8 +2,8 @@ import { Component } from "@angular/core";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
import { SecretVerificationRequest } from "@bitwarden/common/models/request/secret-verification.request";
import { ApiKeyResponse } from "@bitwarden/common/models/response/api-key.response";
import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request";
import { ApiKeyResponse } from "@bitwarden/common/auth/models/response/api-key.response";
import { Verification } from "@bitwarden/common/types/verification";
@Component({

View File

@@ -7,9 +7,9 @@ import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { TwoFactorProviderType } from "@bitwarden/common/enums/twoFactorProviderType";
import { EmailTokenRequest } from "@bitwarden/common/models/request/email-token.request";
import { EmailRequest } from "@bitwarden/common/models/request/email.request";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { EmailTokenRequest } from "@bitwarden/common/auth/models/request/email-token.request";
import { EmailRequest } from "@bitwarden/common/auth/models/request/email.request";
@Component({
selector: "app-change-email",

View File

@@ -7,6 +7,7 @@ import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
import {
DEFAULT_KDF_CONFIG,
DEFAULT_PBKDF2_ITERATIONS,
@@ -15,7 +16,6 @@ import {
DEFAULT_ARGON2_PARALLELISM,
KdfType,
} from "@bitwarden/common/enums/kdfType";
import { KdfConfig } from "@bitwarden/common/models/domain/kdf-config";
import { KdfRequest } from "@bitwarden/common/models/request/kdf.request";
@Component({

View File

@@ -2,11 +2,10 @@ import { Component } from "@angular/core";
import { Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { ChangePasswordComponent as BaseChangePasswordComponent } from "@bitwarden/angular/components/change-password.component";
import { ChangePasswordComponent as BaseChangePasswordComponent } from "@bitwarden/angular/auth/components/change-password.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { KeyConnectorService } from "@bitwarden/common/abstractions/keyConnector.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service";
import { OrganizationUserResetPasswordEnrollmentRequest } from "@bitwarden/common/abstractions/organization-user/requests";
@@ -17,12 +16,13 @@ import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUti
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
import { SendService } from "@bitwarden/common/abstractions/send.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { EmergencyAccessStatusType } from "@bitwarden/common/enums/emergencyAccessStatusType";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { EmergencyAccessStatusType } from "@bitwarden/common/auth/enums/emergency-access-status-type";
import { EmergencyAccessUpdateRequest } from "@bitwarden/common/auth/models/request/emergency-access-update.request";
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
import { Utils } from "@bitwarden/common/misc/utils";
import { EncString } from "@bitwarden/common/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-crypto-key";
import { EmergencyAccessUpdateRequest } from "@bitwarden/common/models/request/emergency-access-update.request";
import { PasswordRequest } from "@bitwarden/common/models/request/password.request";
import { SendWithIdRequest } from "@bitwarden/common/models/request/send-with-id.request";
import { UpdateKeyRequest } from "@bitwarden/common/models/request/update-key.request";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";

View File

@@ -1,38 +0,0 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="deAuthTitle">
<div class="modal-dialog modal-dialog-scrollable" role="document">
<form
class="modal-content"
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
>
<div class="modal-header">
<h1 class="modal-title" id="deAuthTitle">{{ "deauthorizeSessions" | i18n }}</h1>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>{{ "deauthorizeSessionsDesc" | i18n }}</p>
<app-callout type="warning">{{ "deauthorizeSessionsWarning" | i18n }}</app-callout>
<app-user-verification [(ngModel)]="masterPassword" ngDefaultControl name="secret">
</app-user-verification>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-danger btn-submit" [disabled]="form.loading">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "deauthorizeSessions" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "close" | i18n }}
</button>
</div>
</form>
</div>
</div>

View File

@@ -1,44 +0,0 @@
import { Component } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
import { Verification } from "@bitwarden/common/types/verification";
@Component({
selector: "app-deauthorize-sessions",
templateUrl: "deauthorize-sessions.component.html",
})
export class DeauthorizeSessionsComponent {
masterPassword: Verification;
formPromise: Promise<unknown>;
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private userVerificationService: UserVerificationService,
private messagingService: MessagingService,
private logService: LogService
) {}
async submit() {
try {
this.formPromise = this.userVerificationService
.buildRequest(this.masterPassword)
.then((request) => this.apiService.postSecurityStamp(request));
await this.formPromise;
this.platformUtilsService.showToast(
"success",
this.i18nService.t("sessionsDeauthorized"),
this.i18nService.t("logBackIn")
);
this.messagingService.send("logout");
} catch (e) {
this.logService.error(e);
}
}
}

View File

@@ -1,10 +1,10 @@
import { Component } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { AccountApiService } from "@bitwarden/common/abstractions/account/account-api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service";
import { Verification } from "@bitwarden/common/types/verification";
@Component({

View File

@@ -1,142 +0,0 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="userAddEditTitle">
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
<form
class="modal-content"
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
>
<div class="modal-header">
<h1 class="modal-title" id="userAddEditTitle">
<app-premium-badge *ngIf="readOnly"></app-premium-badge>
{{ title }}
<small class="text-muted" *ngIf="name">{{ name }}</small>
</h1>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body" *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</div>
<div class="modal-body" *ngIf="!loading">
<ng-container *ngIf="!editMode">
<p>{{ "inviteEmergencyContactDesc" | i18n }}</p>
<div class="form-group mb-4">
<label for="email">{{ "email" | i18n }}</label>
<input
id="email"
class="form-control"
type="text"
name="Email"
[(ngModel)]="email"
required
/>
</div>
</ng-container>
<h3>
{{ "userAccess" | i18n }}
<a
target="_blank"
rel="noopener"
appA11yTitle="{{ 'learnMore' | i18n }}"
href="https://bitwarden.com/help/emergency-access/#user-access"
>
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
</a>
</h3>
<div class="form-check mt-2 form-check-block">
<input
class="form-check-input"
type="radio"
name="userType"
id="emergencyTypeView"
[value]="emergencyAccessType.View"
[(ngModel)]="type"
/>
<label class="form-check-label" for="emergencyTypeView">
{{ "view" | i18n }}
<small>{{ "viewDesc" | i18n }}</small>
</label>
</div>
<div class="form-check mt-2 form-check-block">
<input
class="form-check-input"
type="radio"
name="userType"
id="emergencyTypeTakeover"
[value]="emergencyAccessType.Takeover"
[(ngModel)]="type"
[disabled]="readOnly"
/>
<label class="form-check-label" for="emergencyTypeTakeover">
{{ "takeover" | i18n }}
<small>{{ "takeoverDesc" | i18n }}</small>
</label>
</div>
<div class="form-group col-6 mt-4">
<label for="waitTime">{{ "waitTime" | i18n }}</label>
<select
id="waitTime"
name="waitTime"
[(ngModel)]="waitTime"
class="form-control"
[disabled]="readOnly"
>
<option *ngFor="let o of waitTimes" [ngValue]="o.value">{{ o.name }}</option>
</select>
<small class="text-muted">{{ "waitTimeDesc" | i18n }}</small>
</div>
</div>
<div class="modal-footer">
<button
type="submit"
buttonType="primary"
bitButton
[loading]="loading || form.loading"
[disabled]="readOnly"
>
{{ "save" | i18n }}
</button>
<button bitButton buttonType="secondary" type="button" data-dismiss="modal">
{{ "cancel" | i18n }}
</button>
<div class="ml-auto">
<button
#deleteBtn
bitButton
buttonType="danger"
type="button"
(click)="delete()"
appA11yTitle="{{ 'delete' | i18n }}"
*ngIf="editMode"
[disabled]="$any(deleteBtn).loading"
>
<i
class="bwi bwi-trash bwi-lg bwi-fw"
[hidden]="$any(deleteBtn).loading"
aria-hidden="true"
></i>
<i
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
[hidden]="!$any(deleteBtn).loading"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
</button>
</div>
</div>
</form>
</div>
</div>

View File

@@ -1,103 +0,0 @@
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { EmergencyAccessType } from "@bitwarden/common/enums/emergencyAccessType";
import { EmergencyAccessInviteRequest } from "@bitwarden/common/models/request/emergency-access-invite.request";
import { EmergencyAccessUpdateRequest } from "@bitwarden/common/models/request/emergency-access-update.request";
@Component({
selector: "emergency-access-add-edit",
templateUrl: "emergency-access-add-edit.component.html",
})
export class EmergencyAccessAddEditComponent implements OnInit {
@Input() name: string;
@Input() emergencyAccessId: string;
@Output() onSaved = new EventEmitter();
@Output() onDeleted = new EventEmitter();
loading = true;
readOnly = false;
editMode = false;
title: string;
email: string;
type: EmergencyAccessType = EmergencyAccessType.View;
formPromise: Promise<any>;
emergencyAccessType = EmergencyAccessType;
waitTimes: { name: string; value: number }[];
waitTime: number;
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private logService: LogService
) {}
async ngOnInit() {
this.editMode = this.loading = this.emergencyAccessId != null;
this.waitTimes = [
{ name: this.i18nService.t("oneDay"), value: 1 },
{ name: this.i18nService.t("days", "2"), value: 2 },
{ name: this.i18nService.t("days", "7"), value: 7 },
{ name: this.i18nService.t("days", "14"), value: 14 },
{ name: this.i18nService.t("days", "30"), value: 30 },
{ name: this.i18nService.t("days", "90"), value: 90 },
];
if (this.editMode) {
this.editMode = true;
this.title = this.i18nService.t("editEmergencyContact");
try {
const emergencyAccess = await this.apiService.getEmergencyAccess(this.emergencyAccessId);
this.type = emergencyAccess.type;
this.waitTime = emergencyAccess.waitTimeDays;
} catch (e) {
this.logService.error(e);
}
} else {
this.title = this.i18nService.t("inviteEmergencyContact");
this.waitTime = this.waitTimes[2].value;
}
this.loading = false;
}
async submit() {
try {
if (this.editMode) {
const request = new EmergencyAccessUpdateRequest();
request.type = this.type;
request.waitTimeDays = this.waitTime;
this.formPromise = this.apiService.putEmergencyAccess(this.emergencyAccessId, request);
} else {
const request = new EmergencyAccessInviteRequest();
request.email = this.email.trim();
request.type = this.type;
request.waitTimeDays = this.waitTime;
this.formPromise = this.apiService.postEmergencyAccessInvite(request);
}
await this.formPromise;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t(this.editMode ? "editedUserId" : "invitedUsers", this.name)
);
this.onSaved.emit();
} catch (e) {
this.logService.error(e);
}
}
async delete() {
this.onDeleted.emit();
}
}

View File

@@ -1,52 +0,0 @@
import { Component } from "@angular/core";
import { AttachmentsComponent as BaseAttachmentsComponent } from "@bitwarden/angular/vault/components/attachments.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
@Component({
selector: "emergency-access-attachments",
templateUrl: "../../vault/app/vault/attachments.component.html",
})
export class EmergencyAccessAttachmentsComponent extends BaseAttachmentsComponent {
viewOnly = true;
canAccessAttachments = true;
constructor(
cipherService: CipherService,
i18nService: I18nService,
cryptoService: CryptoService,
stateService: StateService,
platformUtilsService: PlatformUtilsService,
apiService: ApiService,
logService: LogService,
fileDownloadService: FileDownloadService
) {
super(
cipherService,
i18nService,
cryptoService,
platformUtilsService,
apiService,
window,
logService,
stateService,
fileDownloadService
);
}
protected async init() {
// Do nothing since cipher is already decoded
}
protected showFixOldAttachments(attachment: AttachmentView) {
return false;
}
}

View File

@@ -1,52 +0,0 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="confirmUserTitle">
<div class="modal-dialog modal-dialog-scrollable" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="modal-header">
<h1 class="modal-title" id="confirmUserTitle">
{{ "confirmUser" | i18n }}
<small class="text-muted" *ngIf="name">{{ name }}</small>
</h1>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>
{{ "fingerprintEnsureIntegrityVerify" | i18n }}
<a href="https://bitwarden.com/help/fingerprint-phrase/" target="_blank" rel="noopener">
{{ "learnMore" | i18n }}</a
>
</p>
<p>
<code>{{ fingerprint }}</code>
</p>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="dontAskAgain"
name="DontAskAgain"
[(ngModel)]="dontAskAgain"
/>
<label class="form-check-label" for="dontAskAgain">
{{ "dontAskFingerprintAgain" | i18n }}
</label>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "confirm" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "cancel" | i18n }}
</button>
</div>
</form>
</div>
</div>

View File

@@ -1,62 +0,0 @@
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { Utils } from "@bitwarden/common/misc/utils";
@Component({
selector: "emergency-access-confirm",
templateUrl: "emergency-access-confirm.component.html",
})
export class EmergencyAccessConfirmComponent implements OnInit {
@Input() name: string;
@Input() userId: string;
@Input() emergencyAccessId: string;
@Input() formPromise: Promise<any>;
@Output() onConfirmed = new EventEmitter();
dontAskAgain = false;
loading = true;
fingerprint: string;
constructor(
private apiService: ApiService,
private cryptoService: CryptoService,
private stateService: StateService,
private logService: LogService
) {}
async ngOnInit() {
try {
const publicKeyResponse = await this.apiService.getUserPublicKey(this.userId);
if (publicKeyResponse != null) {
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
const fingerprint = await this.cryptoService.getFingerprint(this.userId, publicKey.buffer);
if (fingerprint != null) {
this.fingerprint = fingerprint.join("-");
}
}
} catch (e) {
this.logService.error(e);
}
this.loading = false;
}
async submit() {
if (this.loading) {
return;
}
if (this.dontAskAgain) {
await this.stateService.setAutoConfirmFingerprints(true);
}
try {
this.onConfirmed.emit();
} catch (e) {
this.logService.error(e);
}
}
}

View File

@@ -1,83 +0,0 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="userAddEditTitle">
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
<form
class="modal-content"
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
>
<div class="modal-header">
<h1 class="modal-title" id="userAddEditTitle">
{{ "takeover" | i18n }}
<small class="text-muted" *ngIf="name">{{ name }}</small>
</h1>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<app-callout type="warning">{{ "loggedOutWarning" | i18n }}</app-callout>
<app-callout
type="info"
[enforcedPolicyOptions]="enforcedPolicyOptions"
*ngIf="enforcedPolicyOptions"
>
</app-callout>
<div class="row">
<div class="col-6">
<div class="form-group">
<label for="masterPassword">{{ "newMasterPass" | i18n }}</label>
<input
id="masterPassword"
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>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "save" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "cancel" | i18n }}
</button>
</div>
</form>
</div>
</div>

View File

@@ -1,128 +0,0 @@
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
import { takeUntil } from "rxjs";
import { ChangePasswordComponent } from "@bitwarden/angular/components/change-password.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { KdfType } from "@bitwarden/common/enums/kdfType";
import { PolicyData } from "@bitwarden/common/models/data/policy.data";
import { KdfConfig } from "@bitwarden/common/models/domain/kdf-config";
import { Policy } from "@bitwarden/common/models/domain/policy";
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-crypto-key";
import { EmergencyAccessPasswordRequest } from "@bitwarden/common/models/request/emergency-access-password.request";
import { PolicyResponse } from "@bitwarden/common/models/response/policy.response";
@Component({
selector: "emergency-access-takeover",
templateUrl: "emergency-access-takeover.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class EmergencyAccessTakeoverComponent
extends ChangePasswordComponent
implements OnInit, OnDestroy
{
@Output() onDone = new EventEmitter();
@Input() emergencyAccessId: string;
@Input() name: string;
@Input() email: string;
@Input() kdf: KdfType;
@Input() kdfIterations: number;
formPromise: Promise<any>;
constructor(
i18nService: I18nService,
cryptoService: CryptoService,
messagingService: MessagingService,
stateService: StateService,
passwordGenerationService: PasswordGenerationService,
platformUtilsService: PlatformUtilsService,
policyService: PolicyService,
private apiService: ApiService,
private logService: LogService
) {
super(
i18nService,
cryptoService,
messagingService,
passwordGenerationService,
platformUtilsService,
policyService,
stateService
);
}
async ngOnInit() {
const response = await this.apiService.getEmergencyGrantorPolicies(this.emergencyAccessId);
if (response.data != null && response.data.length > 0) {
const policies = response.data.map(
(policyResponse: PolicyResponse) => new Policy(new PolicyData(policyResponse))
);
this.policyService
.masterPasswordPolicyOptions$(policies)
.pipe(takeUntil(this.destroy$))
.subscribe((enforcedPolicyOptions) => (this.enforcedPolicyOptions = enforcedPolicyOptions));
}
}
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
ngOnDestroy(): void {
super.ngOnDestroy();
}
async submit() {
if (!(await this.strongPassword())) {
return;
}
const takeoverResponse = await this.apiService.postEmergencyAccessTakeover(
this.emergencyAccessId
);
const oldKeyBuffer = await this.cryptoService.rsaDecrypt(takeoverResponse.keyEncrypted);
const oldEncKey = new SymmetricCryptoKey(oldKeyBuffer);
if (oldEncKey == null) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("unexpectedError")
);
return;
}
const key = await this.cryptoService.makeKey(
this.masterPassword,
this.email,
takeoverResponse.kdf,
new KdfConfig(
takeoverResponse.kdfIterations,
takeoverResponse.kdfMemory,
takeoverResponse.kdfParallelism
)
);
const masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, key);
const encKey = await this.cryptoService.remakeEncKey(key, oldEncKey);
const request = new EmergencyAccessPasswordRequest();
request.newMasterPasswordHash = masterPasswordHash;
request.key = encKey[1].encryptedString;
this.apiService.postEmergencyAccessPassword(this.emergencyAccessId, request);
try {
this.onDone.emit();
} catch (e) {
this.logService.error(e);
}
}
}

View File

@@ -1,72 +0,0 @@
<div class="page-header">
<h1>{{ "vault" | i18n }}</h1>
</div>
<div class="mt-4">
<ng-container *ngIf="ciphers.length">
<table class="table table-hover table-list table-ciphers">
<tbody>
<tr *ngFor="let c of ciphers">
<td class="table-list-icon">
<app-vault-icon [cipher]="c"></app-vault-icon>
</td>
<td class="reduced-lh wrap">
<a href="#" appStopClick (click)="selectCipher(c)" title="{{ 'editItem' | i18n }}">{{
c.name
}}</a>
<ng-container *ngIf="c.organizationId">
<i
class="bwi bwi-collection"
appStopProp
title="{{ 'shared' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "shared" | i18n }}</span>
</ng-container>
<ng-container *ngIf="c.hasAttachments">
<i
class="bwi bwi-paperclip"
appStopProp
title="{{ 'attachments' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "attachments" | i18n }}</span>
</ng-container>
<br />
<small>{{ c.subTitle }}</small>
</td>
<td class="table-list-options">
<div class="dropdown" appListDropdown *ngIf="c.hasAttachments">
<button
class="btn btn-outline-secondary dropdown-toggle"
type="button"
id="dropdownMenuButton"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
appA11yTitle="{{ 'options' | i18n }}"
>
<i class="bwi bwi-cog bwi-lg" aria-hidden="true"></i>
</button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
<a class="dropdown-item" href="#" appStopClick (click)="viewAttachments(c)">
<i class="bwi bwi-fw bwi-paperclip" aria-hidden="true"></i>
{{ "attachments" | i18n }}
</a>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</ng-container>
<ng-container *ngIf="!loaded">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
</div>
<ng-template #cipherAddEdit></ng-template>
<ng-template #attachments></ng-template>

View File

@@ -1,104 +0,0 @@
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-crypto-key";
import { EmergencyAccessViewResponse } from "@bitwarden/common/models/response/emergency-access.response";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { EmergencyAccessAttachmentsComponent } from "./emergency-access-attachments.component";
import { EmergencyAddEditComponent } from "./emergency-add-edit.component";
@Component({
selector: "emergency-access-view",
templateUrl: "emergency-access-view.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class EmergencyAccessViewComponent implements OnInit {
@ViewChild("cipherAddEdit", { read: ViewContainerRef, static: true })
cipherAddEditModalRef: ViewContainerRef;
@ViewChild("attachments", { read: ViewContainerRef, static: true })
attachmentsModalRef: ViewContainerRef;
id: string;
ciphers: CipherView[] = [];
loaded = false;
constructor(
private cipherService: CipherService,
private cryptoService: CryptoService,
private modalService: ModalService,
private router: Router,
private route: ActivatedRoute,
private apiService: ApiService
) {}
ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
this.route.params.subscribe((qParams) => {
if (qParams.id == null) {
return this.router.navigate(["settings/emergency-access"]);
}
this.id = qParams.id;
this.load();
});
}
async selectCipher(cipher: CipherView) {
// eslint-disable-next-line
const [_, childComponent] = await this.modalService.openViewRef(
EmergencyAddEditComponent,
this.cipherAddEditModalRef,
(comp) => {
comp.cipherId = cipher == null ? null : cipher.id;
comp.cipher = cipher;
}
);
return childComponent;
}
async load() {
const response = await this.apiService.postEmergencyAccessView(this.id);
this.ciphers = await this.getAllCiphers(response);
this.loaded = true;
}
async viewAttachments(cipher: CipherView) {
await this.modalService.openViewRef(
EmergencyAccessAttachmentsComponent,
this.attachmentsModalRef,
(comp) => {
comp.cipher = cipher;
comp.emergencyAccessId = this.id;
}
);
}
protected async getAllCiphers(response: EmergencyAccessViewResponse): Promise<CipherView[]> {
const ciphers = response.ciphers;
const decCiphers: CipherView[] = [];
const oldKeyBuffer = await this.cryptoService.rsaDecrypt(response.keyEncrypted);
const oldEncKey = new SymmetricCryptoKey(oldKeyBuffer);
const promises: any[] = [];
ciphers.forEach((cipherResponse) => {
const cipherData = new CipherData(cipherResponse);
const cipher = new Cipher(cipherData);
promises.push(cipher.decrypt(oldEncKey).then((c) => decCiphers.push(c)));
});
await Promise.all(promises);
decCiphers.sort(this.cipherService.getLocaleSortingFunction());
return decCiphers;
}
}

View File

@@ -1,254 +0,0 @@
<!-- Please remove this disable statement when editing this file! -->
<!-- eslint-disable @angular-eslint/template/button-has-type -->
<div class="page-header">
<h1>{{ "emergencyAccess" | i18n }}</h1>
</div>
<p>
{{ "emergencyAccessDesc" | i18n }}
<a href="https://bitwarden.com/help/emergency-access/" target="_blank" rel="noopener">
{{ "learnMore" | i18n }}.
</a>
</p>
<p *ngIf="isOrganizationOwner">
<b>{{ "warning" | i18n }}:</b> {{ "emergencyAccessOwnerWarning" | i18n }}
</p>
<div class="page-header d-flex">
<h2>
{{ "trustedEmergencyContacts" | i18n }}
<app-premium-badge></app-premium-badge>
</h2>
<div class="ml-auto d-flex">
<button
class="btn btn-sm btn-outline-primary ml-3"
type="button"
(click)="invite()"
[disabled]="!canAccessPremium"
>
<i aria-hidden="true" class="bwi bwi-plus bwi-fw"></i>
{{ "addEmergencyContact" | i18n }}
</button>
</div>
</div>
<table class="table table-hover table-list mb-0" *ngIf="trustedContacts && trustedContacts.length">
<tbody>
<tr *ngFor="let c of trustedContacts; let i = index">
<td width="30">
<bit-avatar
[text]="c | userName"
[id]="c.granteeId"
[color]="c.avatarColor"
size="small"
></bit-avatar>
</td>
<td>
<a href="#" appStopClick (click)="edit(c)">{{ c.email }}</a>
<span
bitBadge
badgeType="secondary"
*ngIf="c.status === emergencyAccessStatusType.Invited"
>{{ "invited" | i18n }}</span
>
<span
bitBadge
badgeType="warning"
*ngIf="c.status === emergencyAccessStatusType.Accepted"
>{{ "accepted" | i18n }}</span
>
<span
bitBadge
badgeType="warning"
*ngIf="c.status === emergencyAccessStatusType.RecoveryInitiated"
>{{ "emergencyAccessRecoveryInitiated" | i18n }}</span
>
<span bitBadge *ngIf="c.status === emergencyAccessStatusType.RecoveryApproved">{{
"emergencyAccessRecoveryApproved" | i18n
}}</span>
<span bitBadge *ngIf="c.type === emergencyAccessType.View">{{ "view" | i18n }}</span>
<span bitBadge *ngIf="c.type === emergencyAccessType.Takeover">{{
"takeover" | i18n
}}</span>
<small class="text-muted d-block" *ngIf="c.name">{{ c.name }}</small>
</td>
<td class="table-list-options">
<button
[bitMenuTriggerFor]="trustedContactOptions"
class="tw-border-none tw-bg-transparent tw-text-main"
type="button"
appA11yTitle="{{ 'options' | i18n }}"
>
<i class="bwi bwi-ellipsis-v bwi-lg" aria-hidden="true"></i>
</button>
<bit-menu #trustedContactOptions>
<button
bitMenuItem
*ngIf="c.status === emergencyAccessStatusType.Invited"
(click)="reinvite(c)"
>
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
{{ "resendInvitation" | i18n }}
</button>
<button
bitMenuItem
*ngIf="c.status === emergencyAccessStatusType.Accepted"
(click)="confirm(c)"
>
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
{{ "confirm" | i18n }}
</button>
<button
bitMenuItem
*ngIf="c.status === emergencyAccessStatusType.RecoveryInitiated"
(click)="approve(c)"
>
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
{{ "approve" | i18n }}
</button>
<button
bitMenuItem
*ngIf="
c.status === emergencyAccessStatusType.RecoveryInitiated ||
c.status === emergencyAccessStatusType.RecoveryApproved
"
(click)="reject(c)"
>
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
{{ "reject" | i18n }}
</button>
<button bitMenuItem (click)="remove(c)">
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
{{ "remove" | i18n }}
</button>
</bit-menu>
</td>
</tr>
</tbody>
</table>
<ng-container *ngIf="!trustedContacts || !trustedContacts.length">
<p *ngIf="loaded">{{ "noTrustedContacts" | i18n }}</p>
<ng-container *ngIf="!loaded">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
</ng-container>
<div class="page-header spaced-header">
<h2>{{ "designatedEmergencyContacts" | i18n }}</h2>
</div>
<table class="table table-hover table-list mb-0" *ngIf="grantedContacts && grantedContacts.length">
<tbody>
<tr *ngFor="let c of grantedContacts; let i = index">
<td width="30">
<bit-avatar
[text]="c | userName"
[id]="c.grantorId"
[color]="c.avatarColor"
size="small"
></bit-avatar>
</td>
<td>
<span>{{ c.email }}</span>
<span bitBadge *ngIf="c.status === emergencyAccessStatusType.Invited">{{
"invited" | i18n
}}</span>
<span
bitBadge
badgeType="warning"
*ngIf="c.status === emergencyAccessStatusType.Accepted"
>{{ "accepted" | i18n }}</span
>
<span
bitBadge
badgeType="warning"
*ngIf="c.status === emergencyAccessStatusType.RecoveryInitiated"
>{{ "emergencyAccessRecoveryInitiated" | i18n }}</span
>
<span
bitBadge
badgeType="success"
*ngIf="c.status === emergencyAccessStatusType.RecoveryApproved"
>{{ "emergencyAccessRecoveryApproved" | i18n }}</span
>
<span bitBadge *ngIf="c.type === emergencyAccessType.View">{{ "view" | i18n }}</span>
<span bitBadge *ngIf="c.type === emergencyAccessType.Takeover">{{
"takeover" | i18n
}}</span>
<small class="text-muted d-block" *ngIf="c.name">{{ c.name }}</small>
</td>
<td class="table-list-options">
<button
[bitMenuTriggerFor]="grantedContactOptions"
class="tw-border-none tw-bg-transparent tw-text-main"
type="button"
appA11yTitle="{{ 'options' | i18n }}"
>
<i class="bwi bwi-ellipsis-v bwi-lg" aria-hidden="true"></i>
</button>
<bit-menu #grantedContactOptions>
<button
bitMenuItem
*ngIf="c.status === emergencyAccessStatusType.Confirmed"
(click)="requestAccess(c)"
>
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
{{ "requestAccess" | i18n }}
</button>
<button
bitMenuItem
*ngIf="
c.status === emergencyAccessStatusType.RecoveryApproved &&
c.type === emergencyAccessType.Takeover
"
(click)="takeover(c)"
>
<i class="bwi bwi-fw bwi-key" aria-hidden="true"></i>
{{ "takeover" | i18n }}
</button>
<button
bitMenuItem
*ngIf="
c.status === emergencyAccessStatusType.RecoveryApproved &&
c.type === emergencyAccessType.View
"
[routerLink]="c.id"
>
<i class="bwi bwi-fw bwi-eye" aria-hidden="true"></i>
{{ "view" | i18n }}
</button>
<button bitMenuItem (click)="remove(c)">
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
{{ "remove" | i18n }}
</button>
</bit-menu>
</td>
</tr>
</tbody>
</table>
<ng-container *ngIf="!grantedContacts || !grantedContacts.length">
<p *ngIf="loaded">{{ "noGrantedAccess" | i18n }}</p>
<ng-container *ngIf="!loaded">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
</ng-container>
<ng-template #addEdit></ng-template>
<ng-template #takeoverTemplate></ng-template>
<ng-template #confirmTemplate></ng-template>

View File

@@ -1,318 +0,0 @@
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { EmergencyAccessStatusType } from "@bitwarden/common/enums/emergencyAccessStatusType";
import { EmergencyAccessType } from "@bitwarden/common/enums/emergencyAccessType";
import { Utils } from "@bitwarden/common/misc/utils";
import { EmergencyAccessConfirmRequest } from "@bitwarden/common/models/request/emergency-access-confirm.request";
import {
EmergencyAccessGranteeDetailsResponse,
EmergencyAccessGrantorDetailsResponse,
} from "@bitwarden/common/models/response/emergency-access.response";
import { EmergencyAccessAddEditComponent } from "./emergency-access-add-edit.component";
import { EmergencyAccessConfirmComponent } from "./emergency-access-confirm.component";
import { EmergencyAccessTakeoverComponent } from "./emergency-access-takeover.component";
@Component({
selector: "emergency-access",
templateUrl: "emergency-access.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class EmergencyAccessComponent implements OnInit {
@ViewChild("addEdit", { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
@ViewChild("takeoverTemplate", { read: ViewContainerRef, static: true })
takeoverModalRef: ViewContainerRef;
@ViewChild("confirmTemplate", { read: ViewContainerRef, static: true })
confirmModalRef: ViewContainerRef;
loaded = false;
canAccessPremium: boolean;
trustedContacts: EmergencyAccessGranteeDetailsResponse[];
grantedContacts: EmergencyAccessGrantorDetailsResponse[];
emergencyAccessType = EmergencyAccessType;
emergencyAccessStatusType = EmergencyAccessStatusType;
actionPromise: Promise<any>;
isOrganizationOwner: boolean;
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private modalService: ModalService,
private platformUtilsService: PlatformUtilsService,
private cryptoService: CryptoService,
private messagingService: MessagingService,
private userNamePipe: UserNamePipe,
private logService: LogService,
private stateService: StateService,
private organizationService: OrganizationService
) {}
async ngOnInit() {
this.canAccessPremium = await this.stateService.getCanAccessPremium();
const orgs = await this.organizationService.getAll();
this.isOrganizationOwner = orgs.some((o) => o.isOwner);
this.load();
}
async load() {
this.trustedContacts = (await this.apiService.getEmergencyAccessTrusted()).data;
this.grantedContacts = (await this.apiService.getEmergencyAccessGranted()).data;
this.loaded = true;
}
async premiumRequired() {
if (!this.canAccessPremium) {
this.messagingService.send("premiumRequired");
return;
}
}
async edit(details: EmergencyAccessGranteeDetailsResponse) {
const [modal] = await this.modalService.openViewRef(
EmergencyAccessAddEditComponent,
this.addEditModalRef,
(comp) => {
comp.name = this.userNamePipe.transform(details);
comp.emergencyAccessId = details?.id;
comp.readOnly = !this.canAccessPremium;
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
comp.onSaved.subscribe(() => {
modal.close();
this.load();
});
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
comp.onDeleted.subscribe(() => {
modal.close();
this.remove(details);
});
}
);
}
invite() {
this.edit(null);
}
async reinvite(contact: EmergencyAccessGranteeDetailsResponse) {
if (this.actionPromise != null) {
return;
}
this.actionPromise = this.apiService.postEmergencyAccessReinvite(contact.id);
await this.actionPromise;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("hasBeenReinvited", contact.email)
);
this.actionPromise = null;
}
async confirm(contact: EmergencyAccessGranteeDetailsResponse) {
function updateUser() {
contact.status = EmergencyAccessStatusType.Confirmed;
}
if (this.actionPromise != null) {
return;
}
const autoConfirm = await this.stateService.getAutoConfirmFingerPrints();
if (autoConfirm == null || !autoConfirm) {
const [modal] = await this.modalService.openViewRef(
EmergencyAccessConfirmComponent,
this.confirmModalRef,
(comp) => {
comp.name = this.userNamePipe.transform(contact);
comp.emergencyAccessId = contact.id;
comp.userId = contact?.granteeId;
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
comp.onConfirmed.subscribe(async () => {
modal.close();
comp.formPromise = this.doConfirmation(contact);
await comp.formPromise;
updateUser();
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("hasBeenConfirmed", this.userNamePipe.transform(contact))
);
});
}
);
return;
}
this.actionPromise = this.doConfirmation(contact);
await this.actionPromise;
updateUser();
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("hasBeenConfirmed", this.userNamePipe.transform(contact))
);
this.actionPromise = null;
}
async remove(
details: EmergencyAccessGranteeDetailsResponse | EmergencyAccessGrantorDetailsResponse
) {
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("removeUserConfirmation"),
this.userNamePipe.transform(details),
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
);
if (!confirmed) {
return false;
}
try {
await this.apiService.deleteEmergencyAccess(details.id);
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("removedUserId", this.userNamePipe.transform(details))
);
if (details instanceof EmergencyAccessGranteeDetailsResponse) {
this.removeGrantee(details);
} else {
this.removeGrantor(details);
}
} catch (e) {
this.logService.error(e);
}
}
async requestAccess(details: EmergencyAccessGrantorDetailsResponse) {
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("requestAccessConfirmation", details.waitTimeDays.toString()),
this.userNamePipe.transform(details),
this.i18nService.t("requestAccess"),
this.i18nService.t("no"),
"warning"
);
if (!confirmed) {
return false;
}
await this.apiService.postEmergencyAccessInitiate(details.id);
details.status = EmergencyAccessStatusType.RecoveryInitiated;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("requestSent", this.userNamePipe.transform(details))
);
}
async approve(details: EmergencyAccessGranteeDetailsResponse) {
const type = this.i18nService.t(
details.type === EmergencyAccessType.View ? "view" : "takeover"
);
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("approveAccessConfirmation", this.userNamePipe.transform(details), type),
this.userNamePipe.transform(details),
this.i18nService.t("approve"),
this.i18nService.t("no"),
"warning"
);
if (!confirmed) {
return false;
}
await this.apiService.postEmergencyAccessApprove(details.id);
details.status = EmergencyAccessStatusType.RecoveryApproved;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("emergencyApproved", this.userNamePipe.transform(details))
);
}
async reject(details: EmergencyAccessGranteeDetailsResponse) {
await this.apiService.postEmergencyAccessReject(details.id);
details.status = EmergencyAccessStatusType.Confirmed;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("emergencyRejected", this.userNamePipe.transform(details))
);
}
async takeover(details: EmergencyAccessGrantorDetailsResponse) {
const [modal] = await this.modalService.openViewRef(
EmergencyAccessTakeoverComponent,
this.takeoverModalRef,
(comp) => {
comp.name = this.userNamePipe.transform(details);
comp.email = details.email;
comp.emergencyAccessId = details != null ? details.id : null;
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
comp.onDone.subscribe(() => {
modal.close();
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("passwordResetFor", this.userNamePipe.transform(details))
);
});
}
);
}
private removeGrantee(details: EmergencyAccessGranteeDetailsResponse) {
const index = this.trustedContacts.indexOf(details);
if (index > -1) {
this.trustedContacts.splice(index, 1);
}
}
private removeGrantor(details: EmergencyAccessGrantorDetailsResponse) {
const index = this.grantedContacts.indexOf(details);
if (index > -1) {
this.grantedContacts.splice(index, 1);
}
}
// Encrypt the master password hash using the grantees public key, and send it to bitwarden for escrow.
private async doConfirmation(details: EmergencyAccessGranteeDetailsResponse) {
const encKey = await this.cryptoService.getEncKey();
const publicKeyResponse = await this.apiService.getUserPublicKey(details.granteeId);
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
try {
this.logService.debug(
"User's fingerprint: " +
(await this.cryptoService.getFingerprint(details.granteeId, publicKey.buffer)).join("-")
);
} catch {
// Ignore errors since it's just a debug message
}
const encryptedKey = await this.cryptoService.rsaEncrypt(encKey.key, publicKey.buffer);
const request = new EmergencyAccessConfirmRequest();
request.key = encryptedKey.encryptedString;
await this.apiService.postEmergencyAccessConfirm(details.id, request);
}
}

View File

@@ -1,74 +0,0 @@
import { Component } from "@angular/core";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { TotpService } from "@bitwarden/common/abstractions/totp.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { PasswordRepromptService } from "@bitwarden/common/vault/abstractions/password-reprompt.service";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { AddEditComponent as BaseAddEditComponent } from "../../vault/app/vault/add-edit.component";
@Component({
selector: "app-org-vault-add-edit",
templateUrl: "../../vault/app/vault/add-edit.component.html",
})
export class EmergencyAddEditComponent extends BaseAddEditComponent {
originalCipher: Cipher = null;
viewOnly = true;
protected override componentName = "app-org-vault-add-edit";
constructor(
cipherService: CipherService,
folderService: FolderService,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
auditService: AuditService,
stateService: StateService,
collectionService: CollectionService,
totpService: TotpService,
passwordGenerationService: PasswordGenerationService,
messagingService: MessagingService,
eventCollectionService: EventCollectionService,
policyService: PolicyService,
passwordRepromptService: PasswordRepromptService,
organizationService: OrganizationService,
logService: LogService
) {
super(
cipherService,
folderService,
i18nService,
platformUtilsService,
auditService,
stateService,
collectionService,
totpService,
passwordGenerationService,
messagingService,
eventCollectionService,
policyService,
organizationService,
logService,
passwordRepromptService
);
}
async load() {
this.title = this.i18nService.t("viewItem");
}
protected async loadCipher() {
return Promise.resolve(this.originalCipher);
}
}

View File

@@ -7,7 +7,7 @@ import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { TokenService } from "@bitwarden/common/abstractions/token.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { PaymentComponent } from "./payment.component";

View File

@@ -5,11 +5,11 @@ import { ModalService } from "@bitwarden/angular/services/modal.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { KeyConnectorService } from "@bitwarden/common/abstractions/keyConnector.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { UpdateProfileRequest } from "@bitwarden/common/models/request/update-profile.request";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { UpdateProfileRequest } from "@bitwarden/common/auth/models/request/update-profile.request";
import { ProfileResponse } from "@bitwarden/common/models/response/profile.response";
import { ChangeAvatarComponent } from "./change-avatar.component";

View File

@@ -2,8 +2,8 @@ import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { KeyConnectorService } from "@bitwarden/common/abstractions/keyConnector.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { ApiKeyComponent } from "./api-key.component";

View File

@@ -1,10 +1,11 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { TwoFactorSetupComponent } from "../../auth/settings/two-factor-setup.component";
import { ChangePasswordComponent } from "./change-password.component";
import { SecurityKeysComponent } from "./security-keys.component";
import { SecurityComponent } from "./security.component";
import { TwoFactorSetupComponent } from "./two-factor-setup.component";
const routes: Routes = [
{

View File

@@ -1,6 +1,6 @@
import { Component } from "@angular/core";
import { KeyConnectorService } from "@bitwarden/common/abstractions/keyConnector.service";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
@Component({
selector: "app-security",

View File

@@ -4,7 +4,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { TokenService } from "@bitwarden/common/abstractions/token.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { StateService } from "../core";

View File

@@ -1,116 +0,0 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="2faAuthenticatorTitle">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title" id="2faAuthenticatorTitle">
{{ "twoStepLogin" | i18n }}
<small>{{ "authenticatorAppTitle" | i18n }}</small>
</h1>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<app-two-factor-verify
[organizationId]="organizationId"
[type]="type"
(onAuthed)="auth($any($event))"
*ngIf="!authed"
>
</app-two-factor-verify>
<form
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
*ngIf="authed"
>
<div class="modal-body">
<ng-container *ngIf="!enabled">
<img class="float-right mfaType0" alt="Authenticator app logo" />
<p>{{ "twoStepAuthenticatorDesc" | i18n }}</p>
<p>
<strong>1. {{ "twoStepAuthenticatorDownloadApp" | i18n }}</strong>
</p>
</ng-container>
<ng-container *ngIf="enabled">
<app-callout type="success" title="{{ 'enabled' | i18n }}" icon="bwi bwi-check-circle">
<p>{{ "twoStepLoginProviderEnabled" | i18n }}</p>
{{ "twoStepAuthenticatorReaddDesc" | i18n }}
</app-callout>
<img class="float-right mfaType0" alt="Authenticator app logo" />
<p>{{ "twoStepAuthenticatorNeedApp" | i18n }}</p>
</ng-container>
<ul class="bwi-ul">
<li>
<i class="bwi bwi-li bwi-apple"></i>{{ "iosDevices" | i18n }}:
<a
href="https://itunes.apple.com/us/app/authy/id494168017?mt=8"
target="_blank"
rel="noopener"
>Authy</a
>
</li>
<li>
<i class="bwi bwi-li bwi-android"></i>{{ "androidDevices" | i18n }}:
<a
href="https://play.google.com/store/apps/details?id=com.authy.authy"
target="_blank"
rel="noopener"
>Authy</a
>
</li>
<li>
<i class="bwi bwi-li bwi-windows"></i>{{ "windowsDevices" | i18n }}:
<a
href="https://www.microsoft.com/p/authenticator/9wzdncrfj3rj"
target="_blank"
rel="noopener"
>Microsoft Authenticator</a
>
</li>
</ul>
<p>{{ "twoStepAuthenticatorAppsRecommended" | i18n }}</p>
<p *ngIf="!enabled">
<strong>2. {{ "twoStepAuthenticatorScanCode" | i18n }}</strong>
</p>
<hr *ngIf="enabled" />
<p class="text-center" [ngClass]="{ 'mb-0': enabled }">
<canvas id="qr"></canvas><br />
<code appA11yTitle="{{ 'key' | i18n }}">{{ key }}</code>
</p>
<ng-container *ngIf="!enabled">
<label for="token">3. {{ "twoStepAuthenticatorEnterCode" | i18n }}</label>
<input
id="token"
type="text"
name="Token"
class="form-control"
[(ngModel)]="token"
required
appInputVerbatim
/>
</ng-container>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i
class="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span *ngIf="!enabled">{{ "enable" | i18n }}</span>
<span *ngIf="enabled">{{ "disable" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "close" | i18n }}
</button>
</div>
</form>
</div>
</div>
</div>

View File

@@ -1,111 +0,0 @@
import { Component, OnDestroy, OnInit } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
import { TwoFactorProviderType } from "@bitwarden/common/enums/twoFactorProviderType";
import { Utils } from "@bitwarden/common/misc/utils";
import { UpdateTwoFactorAuthenticatorRequest } from "@bitwarden/common/models/request/update-two-factor-authenticator.request";
import { TwoFactorAuthenticatorResponse } from "@bitwarden/common/models/response/two-factor-authenticator.response";
import { AuthResponse } from "@bitwarden/common/types/authResponse";
import { TwoFactorBaseComponent } from "./two-factor-base.component";
// NOTE: There are additional options available but these are just the ones we are current using.
// See: https://github.com/neocotic/qrious#examples
interface QRiousOptions {
element: HTMLElement;
value: string;
size: number;
}
declare global {
interface Window {
QRious: new (options: QRiousOptions) => unknown;
}
}
@Component({
selector: "app-two-factor-authenticator",
templateUrl: "two-factor-authenticator.component.html",
})
export class TwoFactorAuthenticatorComponent
extends TwoFactorBaseComponent
implements OnInit, OnDestroy
{
type = TwoFactorProviderType.Authenticator;
key: string;
token: string;
formPromise: Promise<TwoFactorAuthenticatorResponse>;
private qrScript: HTMLScriptElement;
constructor(
apiService: ApiService,
i18nService: I18nService,
userVerificationService: UserVerificationService,
platformUtilsService: PlatformUtilsService,
logService: LogService,
private stateService: StateService
) {
super(apiService, i18nService, platformUtilsService, logService, userVerificationService);
this.qrScript = window.document.createElement("script");
this.qrScript.src = "scripts/qrious.min.js";
this.qrScript.async = true;
}
ngOnInit() {
window.document.body.appendChild(this.qrScript);
}
ngOnDestroy() {
window.document.body.removeChild(this.qrScript);
}
auth(authResponse: AuthResponse<TwoFactorAuthenticatorResponse>) {
super.auth(authResponse);
return this.processResponse(authResponse.response);
}
submit() {
if (this.enabled) {
return super.disable(this.formPromise);
} else {
return this.enable();
}
}
protected async enable() {
const request = await this.buildRequestModel(UpdateTwoFactorAuthenticatorRequest);
request.token = this.token;
request.key = this.key;
return super.enable(async () => {
this.formPromise = this.apiService.putTwoFactorAuthenticator(request);
const response = await this.formPromise;
await this.processResponse(response);
});
}
private async processResponse(response: TwoFactorAuthenticatorResponse) {
this.token = null;
this.enabled = response.enabled;
this.key = response.key;
const email = await this.stateService.getEmail();
window.setTimeout(() => {
new window.QRious({
element: document.getElementById("qr"),
value:
"otpauth://totp/Bitwarden:" +
Utils.encodeRFC3986URIComponent(email) +
"?secret=" +
encodeURIComponent(this.key) +
"&issuer=Bitwarden",
size: 160,
});
}, 100);
}
}

View File

@@ -1,91 +0,0 @@
import { Directive, EventEmitter, Output } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
import { TwoFactorProviderType } from "@bitwarden/common/enums/twoFactorProviderType";
import { VerificationType } from "@bitwarden/common/enums/verificationType";
import { SecretVerificationRequest } from "@bitwarden/common/models/request/secret-verification.request";
import { TwoFactorProviderRequest } from "@bitwarden/common/models/request/two-factor-provider.request";
import { AuthResponseBase } from "@bitwarden/common/types/authResponse";
@Directive()
export abstract class TwoFactorBaseComponent {
@Output() onUpdated = new EventEmitter<boolean>();
type: TwoFactorProviderType;
organizationId: string;
twoFactorProviderType = TwoFactorProviderType;
enabled = false;
authed = false;
protected hashedSecret: string;
protected verificationType: VerificationType;
constructor(
protected apiService: ApiService,
protected i18nService: I18nService,
protected platformUtilsService: PlatformUtilsService,
protected logService: LogService,
protected userVerificationService: UserVerificationService
) {}
protected auth(authResponse: AuthResponseBase) {
this.hashedSecret = authResponse.secret;
this.verificationType = authResponse.verificationType;
this.authed = true;
}
protected async enable(enableFunction: () => Promise<void>) {
try {
await enableFunction();
this.onUpdated.emit(true);
} catch (e) {
this.logService.error(e);
}
}
protected async disable(promise: Promise<unknown>) {
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("twoStepDisableDesc"),
this.i18nService.t("disable"),
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
);
if (!confirmed) {
return;
}
try {
const request = await this.buildRequestModel(TwoFactorProviderRequest);
request.type = this.type;
if (this.organizationId != null) {
promise = this.apiService.putTwoFactorOrganizationDisable(this.organizationId, request);
} else {
promise = this.apiService.putTwoFactorDisable(request);
}
await promise;
this.enabled = false;
this.platformUtilsService.showToast("success", null, this.i18nService.t("twoStepDisabled"));
this.onUpdated.emit(false);
} catch (e) {
this.logService.error(e);
}
}
protected async buildRequestModel<T extends SecretVerificationRequest>(
requestClass: new () => T
) {
return this.userVerificationService.buildRequest(
{
secret: this.hashedSecret,
type: this.verificationType,
},
requestClass,
true
);
}
}

View File

@@ -1,105 +0,0 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="2faDuoTitle">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title" id="2faDuoTitle">
{{ "twoStepLogin" | i18n }}
<small>Duo</small>
</h1>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<app-two-factor-verify
[organizationId]="organizationId"
[type]="type"
(onAuthed)="auth($any($event))"
*ngIf="!authed"
>
</app-two-factor-verify>
<form
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
*ngIf="authed"
autocomplete="off"
>
<div class="modal-body">
<ng-container *ngIf="enabled">
<app-callout type="success" title="{{ 'enabled' | i18n }}" icon="bwi bwi-check-circle">
{{ "twoStepLoginProviderEnabled" | i18n }}
</app-callout>
<img class="float-right ml-3 mfaType2" alt="Duo logo" />
<strong>{{ "twoFactorDuoIntegrationKey" | i18n }}:</strong> {{ ikey }}
<br />
<strong>{{ "twoFactorDuoSecretKey" | i18n }}:</strong> {{ skey }}
<br />
<strong>{{ "twoFactorDuoApiHostname" | i18n }}:</strong> {{ host }}
</ng-container>
<ng-container *ngIf="!enabled">
<img class="float-right ml-3 mfaType2" alt="Duo logo" />
<p>{{ "twoFactorDuoDesc" | i18n }}</p>
<div class="form-group">
<label for="ikey">{{ "twoFactorDuoIntegrationKey" | i18n }}</label>
<input
id="ikey"
type="text"
name="IntegrationKey"
class="form-control"
[(ngModel)]="ikey"
required
appInputVerbatim
/>
</div>
<div class="form-group">
<label for="skey">{{ "twoFactorDuoSecretKey" | i18n }}</label>
<input
id="skey"
type="password"
name="SecretKey"
class="form-control"
[(ngModel)]="skey"
required
appInputVerbatim
autocomplete="new-password"
/>
</div>
<div class="form-group">
<label for="host">{{ "twoFactorDuoApiHostname" | i18n }}</label>
<input
id="host"
type="text"
name="Host"
class="form-control"
[(ngModel)]="host"
placeholder="{{ 'ex' | i18n }} api-xxxxxxxx.duosecurity.com"
required
appInputVerbatim
/>
</div>
</ng-container>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i
class="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span *ngIf="!enabled">{{ "enable" | i18n }}</span>
<span *ngIf="enabled">{{ "disable" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "close" | i18n }}
</button>
</div>
</form>
</div>
</div>
</div>

View File

@@ -1,75 +0,0 @@
import { Component } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
import { TwoFactorProviderType } from "@bitwarden/common/enums/twoFactorProviderType";
import { UpdateTwoFactorDuoRequest } from "@bitwarden/common/models/request/update-two-factor-duo.request";
import { TwoFactorDuoResponse } from "@bitwarden/common/models/response/two-factor-duo.response";
import { AuthResponse } from "@bitwarden/common/types/authResponse";
import { TwoFactorBaseComponent } from "./two-factor-base.component";
@Component({
selector: "app-two-factor-duo",
templateUrl: "two-factor-duo.component.html",
})
export class TwoFactorDuoComponent extends TwoFactorBaseComponent {
type = TwoFactorProviderType.Duo;
ikey: string;
skey: string;
host: string;
formPromise: Promise<TwoFactorDuoResponse>;
constructor(
apiService: ApiService,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
logService: LogService,
userVerificationService: UserVerificationService
) {
super(apiService, i18nService, platformUtilsService, logService, userVerificationService);
}
auth(authResponse: AuthResponse<TwoFactorDuoResponse>) {
super.auth(authResponse);
this.processResponse(authResponse.response);
}
submit() {
if (this.enabled) {
return super.disable(this.formPromise);
} else {
return this.enable();
}
}
protected async enable() {
const request = await this.buildRequestModel(UpdateTwoFactorDuoRequest);
request.integrationKey = this.ikey;
request.secretKey = this.skey;
request.host = this.host;
return super.enable(async () => {
if (this.organizationId != null) {
this.formPromise = this.apiService.putTwoFactorOrganizationDuo(
this.organizationId,
request
);
} else {
this.formPromise = this.apiService.putTwoFactorDuo(request);
}
const response = await this.formPromise;
await this.processResponse(response);
});
}
private processResponse(response: TwoFactorDuoResponse) {
this.ikey = response.integrationKey;
this.skey = response.secretKey;
this.host = response.host;
this.enabled = response.enabled;
}
}

View File

@@ -1,108 +0,0 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="2faEmailTitle">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title" id="2faEmailTitle">
{{ "twoStepLogin" | i18n }}
<small>{{ "emailTitle" | i18n }}</small>
</h1>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<app-two-factor-verify
[organizationId]="organizationId"
[type]="type"
(onAuthed)="auth($any($event))"
*ngIf="!authed"
>
</app-two-factor-verify>
<form
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
*ngIf="authed"
>
<div class="modal-body">
<ng-container *ngIf="enabled">
<app-callout type="success" title="{{ 'enabled' | i18n }}" icon="bwi bwi-check-circle">
{{ "twoStepLoginProviderEnabled" | i18n }}
</app-callout>
<strong>{{ "email" | i18n }}:</strong> {{ email }}
</ng-container>
<ng-container *ngIf="!enabled">
<p class="d-flex">
<span class="mr-3">{{ "twoFactorEmailDesc" | i18n }}</span>
<img class="float-right ml-auto mfaType1" alt="Email logo" />
</p>
<div class="form-group">
<label for="email">1. {{ "twoFactorEmailEnterEmail" | i18n }}</label>
<input
id="email"
type="text"
name="Email"
class="form-control"
[(ngModel)]="email"
required
inputmode="email"
appInputVerbatim="false"
/>
</div>
<div class="mb-3 d-flex">
<button
#sendBtn
type="button"
class="btn btn-outline-primary btn-sm btn-submit align-self-start"
(click)="sendEmail()"
[appApiAction]="emailPromise"
[disabled]="$any(sendBtn).loading"
>
<i
class="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span>{{ "sendEmail" | i18n }}</span>
</button>
<span class="text-success ml-3" *ngIf="sentEmail">
{{ "verificationCodeEmailSent" | i18n: sentEmail }}
</span>
</div>
<div class="form-group">
<label for="token">2. {{ "twoFactorEmailEnterCode" | i18n }}</label>
<input
id="token"
type="text"
name="Token"
class="form-control"
[(ngModel)]="token"
required
appInputVerbatim
/>
</div>
</ng-container>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i
class="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span *ngIf="!enabled">{{ "enable" | i18n }}</span>
<span *ngIf="enabled">{{ "disable" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "close" | i18n }}
</button>
</div>
</form>
</div>
</div>
</div>

View File

@@ -1,85 +0,0 @@
import { Component } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
import { TwoFactorProviderType } from "@bitwarden/common/enums/twoFactorProviderType";
import { TwoFactorEmailRequest } from "@bitwarden/common/models/request/two-factor-email.request";
import { UpdateTwoFactorEmailRequest } from "@bitwarden/common/models/request/update-two-factor-email.request";
import { TwoFactorEmailResponse } from "@bitwarden/common/models/response/two-factor-email.response";
import { AuthResponse } from "@bitwarden/common/types/authResponse";
import { TwoFactorBaseComponent } from "./two-factor-base.component";
@Component({
selector: "app-two-factor-email",
templateUrl: "two-factor-email.component.html",
})
export class TwoFactorEmailComponent extends TwoFactorBaseComponent {
type = TwoFactorProviderType.Email;
email: string;
token: string;
sentEmail: string;
formPromise: Promise<TwoFactorEmailResponse>;
emailPromise: Promise<unknown>;
constructor(
apiService: ApiService,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
logService: LogService,
userVerificationService: UserVerificationService,
private stateService: StateService
) {
super(apiService, i18nService, platformUtilsService, logService, userVerificationService);
}
auth(authResponse: AuthResponse<TwoFactorEmailResponse>) {
super.auth(authResponse);
return this.processResponse(authResponse.response);
}
submit() {
if (this.enabled) {
return super.disable(this.formPromise);
} else {
return this.enable();
}
}
async sendEmail() {
try {
const request = await this.buildRequestModel(TwoFactorEmailRequest);
request.email = this.email;
this.emailPromise = this.apiService.postTwoFactorEmailSetup(request);
await this.emailPromise;
this.sentEmail = this.email;
} catch (e) {
this.logService.error(e);
}
}
protected async enable() {
const request = await this.buildRequestModel(UpdateTwoFactorEmailRequest);
request.email = this.email;
request.token = this.token;
return super.enable(async () => {
this.formPromise = this.apiService.putTwoFactorEmail(request);
const response = await this.formPromise;
await this.processResponse(response);
});
}
private async processResponse(response: TwoFactorEmailResponse) {
this.token = null;
this.email = response.email;
this.enabled = response.enabled;
if (!this.enabled && (this.email == null || this.email === "")) {
this.email = await this.stateService.getEmail();
}
}
}

View File

@@ -1,41 +0,0 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="2faRecoveryTitle">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title" id="2faRecoveryTitle">
{{ "twoStepLogin" | i18n }}
<small>{{ "recoveryCodeTitle" | i18n }}</small>
</h1>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<app-two-factor-verify [type]="type" (onAuthed)="auth($event)" *ngIf="!authed">
</app-two-factor-verify>
<ng-container *ngIf="authed">
<div class="modal-body text-center">
<ng-container *ngIf="code">
<p>{{ "twoFactorRecoveryYourCode" | i18n }}:</p>
<code class="text-lg">{{ code }}</code>
</ng-container>
<ng-container *ngIf="!code">
{{ "twoFactorRecoveryNoCode" | i18n }}
</ng-container>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" (click)="print()" *ngIf="code">
{{ "printCode" | i18n }}
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "close" | i18n }}
</button>
</div>
</ng-container>
</div>
</div>
</div>

View File

@@ -1,55 +0,0 @@
import { Component } from "@angular/core";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { TwoFactorProviderType } from "@bitwarden/common/enums/twoFactorProviderType";
import { TwoFactorRecoverResponse } from "@bitwarden/common/models/response/two-factor-recover.response";
@Component({
selector: "app-two-factor-recovery",
templateUrl: "two-factor-recovery.component.html",
})
export class TwoFactorRecoveryComponent {
type = -1;
code: string;
authed: boolean;
twoFactorProviderType = TwoFactorProviderType;
constructor(private i18nService: I18nService) {}
auth(authResponse: any) {
this.authed = true;
this.processResponse(authResponse.response);
}
print() {
const w = window.open();
w.document.write(
'<div style="font-size: 18px; text-align: center;">' +
"<p>" +
this.i18nService.t("twoFactorRecoveryYourCode") +
":</p>" +
"<code style=\"font-family: Menlo, Monaco, Consolas, 'Courier New', monospace;\">" +
this.code +
"</code></div>" +
'<p style="text-align: center;">' +
new Date() +
"</p>"
);
w.onafterprint = () => w.close();
w.print();
}
private formatString(s: string) {
if (s == null) {
return null;
}
return s
.replace(/(.{4})/g, "$1 ")
.trim()
.toUpperCase();
}
private processResponse(response: TwoFactorRecoverResponse) {
this.code = this.formatString(response.code);
}
}

View File

@@ -1,75 +0,0 @@
<!-- Please remove this disable statement when editing this file! -->
<!-- eslint-disable @angular-eslint/template/button-has-type -->
<div class="tabbed-header">
<h1 *ngIf="!organizationId">{{ "twoStepLogin" | i18n }}</h1>
<h1 *ngIf="organizationId">{{ "twoStepLoginEnforcement" | i18n }}</h1>
</div>
<p *ngIf="!organizationId">{{ "twoStepLoginDesc" | i18n }}</p>
<ng-container *ngIf="organizationId">
<p>
{{ "twoStepLoginOrganizationDescStart" | i18n }}
<a routerLink="../policies">{{ "twoStepLoginPolicy" | i18n }}.</a>
<br />
{{ "twoStepLoginOrganizationDuoDesc" | i18n }}
</p>
<p>{{ "twoStepLoginOrganizationSsoDesc" | i18n }}</p>
</ng-container>
<bit-callout type="warning" *ngIf="!organizationId">
<p>{{ "twoStepLoginRecoveryWarning" | i18n }}</p>
<button bitButton buttonType="secondary" (click)="recoveryCode()">
{{ "viewRecoveryCode" | i18n }}
</button>
</bit-callout>
<h2 [ngClass]="{ 'mt-5': !organizationId }">
{{ "providers" | i18n }}
<small *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin bwi-fw text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</small>
</h2>
<bit-callout type="warning" *ngIf="showPolicyWarning">
{{ "twoStepLoginPolicyUserWarning" | i18n }}
</bit-callout>
<ul class="list-group list-group-2fa">
<li *ngFor="let p of providers" class="list-group-item d-flex align-items-center">
<div class="logo-2fa d-flex justify-content-center">
<img [class]="'mfaType' + p.type" [alt]="p.name + ' logo'" />
</div>
<div class="mx-4">
<h3 class="mb-0">
{{ p.name }}
<ng-container *ngIf="p.enabled">
<i
class="bwi bwi-check text-success bwi-fw"
title="{{ 'enabled' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "enabled" | i18n }}</span>
</ng-container>
<app-premium-badge *ngIf="p.premium"></app-premium-badge>
</h3>
{{ p.description }}
</div>
<div class="ml-auto">
<button
bitButton
buttonType="secondary"
[disabled]="!canAccessPremium && p.premium"
(click)="manage(p.type)"
>
{{ "manage" | i18n }}
</button>
</div>
</li>
</ul>
<ng-template #authenticatorTemplate></ng-template>
<ng-template #recoveryTemplate></ng-template>
<ng-template #duoTemplate></ng-template>
<ng-template #emailTemplate></ng-template>
<ng-template #yubikeyTemplate></ng-template>
<ng-template #webAuthnTemplate></ng-template>

View File

@@ -1,210 +0,0 @@
import { Component, OnDestroy, OnInit, Type, ViewChild, ViewContainerRef } from "@angular/core";
import { Subject, takeUntil } from "rxjs";
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { PolicyType } from "@bitwarden/common/enums/policyType";
import { TwoFactorProviderType } from "@bitwarden/common/enums/twoFactorProviderType";
import { TwoFactorProviders } from "@bitwarden/common/services/twoFactor.service";
import { TwoFactorAuthenticatorComponent } from "./two-factor-authenticator.component";
import { TwoFactorDuoComponent } from "./two-factor-duo.component";
import { TwoFactorEmailComponent } from "./two-factor-email.component";
import { TwoFactorRecoveryComponent } from "./two-factor-recovery.component";
import { TwoFactorWebAuthnComponent } from "./two-factor-webauthn.component";
import { TwoFactorYubiKeyComponent } from "./two-factor-yubikey.component";
@Component({
selector: "app-two-factor-setup",
templateUrl: "two-factor-setup.component.html",
})
export class TwoFactorSetupComponent implements OnInit, OnDestroy {
@ViewChild("recoveryTemplate", { read: ViewContainerRef, static: true })
recoveryModalRef: ViewContainerRef;
@ViewChild("authenticatorTemplate", { read: ViewContainerRef, static: true })
authenticatorModalRef: ViewContainerRef;
@ViewChild("yubikeyTemplate", { read: ViewContainerRef, static: true })
yubikeyModalRef: ViewContainerRef;
@ViewChild("duoTemplate", { read: ViewContainerRef, static: true }) duoModalRef: ViewContainerRef;
@ViewChild("emailTemplate", { read: ViewContainerRef, static: true })
emailModalRef: ViewContainerRef;
@ViewChild("webAuthnTemplate", { read: ViewContainerRef, static: true })
webAuthnModalRef: ViewContainerRef;
organizationId: string;
providers: any[] = [];
canAccessPremium: boolean;
showPolicyWarning = false;
loading = true;
modal: ModalRef;
formPromise: Promise<any>;
private destroy$ = new Subject<void>();
private twoFactorAuthPolicyAppliesToActiveUser: boolean;
constructor(
protected apiService: ApiService,
protected modalService: ModalService,
protected messagingService: MessagingService,
protected policyService: PolicyService,
private stateService: StateService
) {}
async ngOnInit() {
this.canAccessPremium = await this.stateService.getCanAccessPremium();
for (const key in TwoFactorProviders) {
// eslint-disable-next-line
if (!TwoFactorProviders.hasOwnProperty(key)) {
continue;
}
const p = (TwoFactorProviders as any)[key];
if (this.filterProvider(p.type)) {
continue;
}
this.providers.push({
type: p.type,
name: p.name,
description: p.description,
enabled: false,
premium: p.premium,
sort: p.sort,
});
}
this.providers.sort((a: any, b: any) => a.sort - b.sort);
this.policyService
.policyAppliesToActiveUser$(PolicyType.TwoFactorAuthentication)
.pipe(takeUntil(this.destroy$))
.subscribe((policyAppliesToActiveUser) => {
this.twoFactorAuthPolicyAppliesToActiveUser = policyAppliesToActiveUser;
});
await this.load();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
async load() {
this.loading = true;
const providerList = await this.getTwoFactorProviders();
providerList.data.forEach((p) => {
this.providers.forEach((p2) => {
if (p.type === p2.type) {
p2.enabled = p.enabled;
}
});
});
this.evaluatePolicies();
this.loading = false;
}
async manage(type: TwoFactorProviderType) {
switch (type) {
case TwoFactorProviderType.Authenticator: {
const authComp = await this.openModal(
this.authenticatorModalRef,
TwoFactorAuthenticatorComponent
);
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
authComp.onUpdated.subscribe((enabled: boolean) => {
this.updateStatus(enabled, TwoFactorProviderType.Authenticator);
});
break;
}
case TwoFactorProviderType.Yubikey: {
const yubiComp = await this.openModal(this.yubikeyModalRef, TwoFactorYubiKeyComponent);
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
yubiComp.onUpdated.subscribe((enabled: boolean) => {
this.updateStatus(enabled, TwoFactorProviderType.Yubikey);
});
break;
}
case TwoFactorProviderType.Duo: {
const duoComp = await this.openModal(this.duoModalRef, TwoFactorDuoComponent);
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
duoComp.onUpdated.subscribe((enabled: boolean) => {
this.updateStatus(enabled, TwoFactorProviderType.Duo);
});
break;
}
case TwoFactorProviderType.Email: {
const emailComp = await this.openModal(this.emailModalRef, TwoFactorEmailComponent);
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
emailComp.onUpdated.subscribe((enabled: boolean) => {
this.updateStatus(enabled, TwoFactorProviderType.Email);
});
break;
}
case TwoFactorProviderType.WebAuthn: {
const webAuthnComp = await this.openModal(
this.webAuthnModalRef,
TwoFactorWebAuthnComponent
);
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
webAuthnComp.onUpdated.subscribe((enabled: boolean) => {
this.updateStatus(enabled, TwoFactorProviderType.WebAuthn);
});
break;
}
default:
break;
}
}
recoveryCode() {
this.openModal(this.recoveryModalRef, TwoFactorRecoveryComponent);
}
async premiumRequired() {
if (!this.canAccessPremium) {
this.messagingService.send("premiumRequired");
return;
}
}
protected getTwoFactorProviders() {
return this.apiService.getTwoFactorProviders();
}
protected filterProvider(type: TwoFactorProviderType) {
return type === TwoFactorProviderType.OrganizationDuo;
}
protected async openModal<T>(ref: ViewContainerRef, type: Type<T>): Promise<T> {
const [modal, childComponent] = await this.modalService.openViewRef(type, ref);
this.modal = modal;
return childComponent;
}
protected updateStatus(enabled: boolean, type: TwoFactorProviderType) {
if (!enabled && this.modal != null) {
this.modal.close();
}
this.providers.forEach((p) => {
if (p.type === type) {
p.enabled = enabled;
}
});
this.evaluatePolicies();
}
private async evaluatePolicies() {
if (this.organizationId == null && this.providers.filter((p) => p.enabled).length === 1) {
this.showPolicyWarning = this.twoFactorAuthPolicyAppliesToActiveUser;
} else {
this.showPolicyWarning = false;
}
}
}

View File

@@ -1,16 +0,0 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="modal-body">
<p>{{ "twoStepLoginAuthDesc" | i18n }}</p>
<app-user-verification [(ngModel)]="secret" ngDefaultControl name="secret">
</app-user-verification>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "continue" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "close" | i18n }}
</button>
</div>
</form>

View File

@@ -1,75 +0,0 @@
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
import { TwoFactorProviderType } from "@bitwarden/common/enums/twoFactorProviderType";
import { VerificationType } from "@bitwarden/common/enums/verificationType";
import { SecretVerificationRequest } from "@bitwarden/common/models/request/secret-verification.request";
import { AuthResponse } from "@bitwarden/common/types/authResponse";
import { TwoFactorResponse } from "@bitwarden/common/types/twoFactorResponse";
import { Verification } from "@bitwarden/common/types/verification";
@Component({
selector: "app-two-factor-verify",
templateUrl: "two-factor-verify.component.html",
})
export class TwoFactorVerifyComponent {
@Input() type: TwoFactorProviderType;
@Input() organizationId: string;
@Output() onAuthed = new EventEmitter<AuthResponse<TwoFactorResponse>>();
secret: Verification;
formPromise: Promise<TwoFactorResponse>;
constructor(
private apiService: ApiService,
private logService: LogService,
private userVerificationService: UserVerificationService
) {}
async submit() {
let hashedSecret: string;
try {
this.formPromise = this.userVerificationService.buildRequest(this.secret).then((request) => {
hashedSecret =
this.secret.type === VerificationType.MasterPassword
? request.masterPasswordHash
: request.otp;
return this.apiCall(request);
});
const response = await this.formPromise;
this.onAuthed.emit({
response: response,
secret: hashedSecret,
verificationType: this.secret.type,
});
} catch (e) {
this.logService.error(e);
}
}
private apiCall(request: SecretVerificationRequest): Promise<TwoFactorResponse> {
switch (this.type) {
case -1 as TwoFactorProviderType:
return this.apiService.getTwoFactorRecover(request);
case TwoFactorProviderType.Duo:
case TwoFactorProviderType.OrganizationDuo:
if (this.organizationId != null) {
return this.apiService.getTwoFactorOrganizationDuo(this.organizationId, request);
} else {
return this.apiService.getTwoFactorDuo(request);
}
case TwoFactorProviderType.Email:
return this.apiService.getTwoFactorEmail(request);
case TwoFactorProviderType.WebAuthn:
return this.apiService.getTwoFactorWebAuthn(request);
case TwoFactorProviderType.Authenticator:
return this.apiService.getTwoFactorAuthenticator(request);
case TwoFactorProviderType.Yubikey:
return this.apiService.getTwoFactorYubiKey(request);
}
}
}

View File

@@ -1,159 +0,0 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="2faU2fTitle">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title" id="2faU2fTitle">
{{ "twoStepLogin" | i18n }}
<small>{{ "webAuthnTitle" | i18n }}</small>
</h1>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<app-two-factor-verify
[organizationId]="organizationId"
[type]="type"
(onAuthed)="auth($any($event))"
*ngIf="!authed"
>
</app-two-factor-verify>
<form
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
*ngIf="authed"
>
<div class="modal-body">
<app-callout
type="success"
title="{{ 'enabled' | i18n }}"
icon="bwi bwi-check-circle"
*ngIf="enabled"
>
{{ "twoStepLoginProviderEnabled" | i18n }}
</app-callout>
<app-callout type="warning">
<p>{{ "twoFactorWebAuthnWarning" | i18n }}</p>
<ul class="mb-0">
<li>{{ "twoFactorWebAuthnSupportWeb" | i18n }}</li>
</ul>
</app-callout>
<img class="float-right ml-5 mfaType7" alt="FIDO2 WebAuthn logo'" />
<ul class="bwi-ul">
<li
*ngFor="let k of keys; let i = index"
#removeKeyBtn
[appApiAction]="k.removePromise"
>
<i class="bwi bwi-li bwi-key"></i>
<strong *ngIf="!k.configured || !k.name">{{ "webAuthnkeyX" | i18n: i + 1 }}</strong>
<strong *ngIf="k.configured && k.name">{{ k.name }}</strong>
<ng-container *ngIf="k.configured && !$any(removeKeyBtn).loading">
<ng-container *ngIf="k.migrated">
<span>{{ "webAuthnMigrated" | i18n }}</span>
</ng-container>
</ng-container>
<ng-container *ngIf="keysConfiguredCount > 1 && k.configured">
<i
class="bwi bwi-spin bwi-spinner text-muted bwi-fw"
title="{{ 'loading' | i18n }}"
*ngIf="$any(removeKeyBtn).loading"
aria-hidden="true"
></i>
-
<a href="#" appStopClick (click)="remove(k)">{{ "remove" | i18n }}</a>
</ng-container>
</li>
</ul>
<hr />
<p>{{ "twoFactorWebAuthnAdd" | i18n }}:</p>
<ol>
<li>{{ "twoFactorU2fGiveName" | i18n }}</li>
<li>{{ "twoFactorU2fPlugInReadKey" | i18n }}</li>
<li>{{ "twoFactorU2fTouchButton" | i18n }}</li>
<li>{{ "twoFactorU2fSaveForm" | i18n }}</li>
</ol>
<div class="row">
<div class="form-group col-6">
<label for="name">{{ "name" | i18n }}</label>
<input
id="name"
type="text"
name="Name"
class="form-control"
[(ngModel)]="name"
[disabled]="!keyIdAvailable"
/>
</div>
</div>
<button
type="button"
(click)="readKey()"
class="btn btn-outline-secondary mr-2"
[disabled]="$any(readKeyBtn).loading || webAuthnListening || !keyIdAvailable"
#readKeyBtn
[appApiAction]="challengePromise"
>
{{ "readKey" | i18n }}
</button>
<ng-container *ngIf="$any(readKeyBtn).loading">
<i class="bwi bwi-spinner bwi-spin text-muted" aria-hidden="true"></i>
</ng-container>
<ng-container *ngIf="!$any(readKeyBtn).loading">
<ng-container *ngIf="webAuthnListening">
<i class="bwi bwi-spinner bwi-spin text-muted" aria-hidden="true"></i>
{{ "twoFactorU2fWaiting" | i18n }}...
</ng-container>
<ng-container *ngIf="webAuthnResponse">
<i class="bwi bwi-check-circle text-success" aria-hidden="true"></i>
{{ "twoFactorU2fClickSave" | i18n }}
</ng-container>
<ng-container *ngIf="webAuthnError">
<i class="bwi bwi-exclamation-triangle text-danger" aria-hidden="true"></i>
{{ "twoFactorU2fProblemReadingTryAgain" | i18n }}
</ng-container>
</ng-container>
</div>
<div class="modal-footer">
<button
type="submit"
class="btn btn-primary"
[disabled]="form.loading || !webAuthnResponse"
>
<i
class="bwi bwi-spinner bwi-spin"
*ngIf="form.loading"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span *ngIf="!form.loading">{{ "save" | i18n }}</span>
</button>
<button
#disableBtn
type="button"
class="btn btn-outline-secondary btn-submit"
[disabled]="$any(disableBtn).loading"
(click)="disable()"
*ngIf="enabled"
>
<i
class="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span>{{ "disableAllKeys" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "close" | i18n }}
</button>
</div>
</form>
</div>
</div>
</div>

View File

@@ -1,180 +0,0 @@
import { Component, NgZone } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
import { TwoFactorProviderType } from "@bitwarden/common/enums/twoFactorProviderType";
import { SecretVerificationRequest } from "@bitwarden/common/models/request/secret-verification.request";
import { UpdateTwoFactorWebAuthnDeleteRequest } from "@bitwarden/common/models/request/update-two-factor-web-authn-delete.request";
import { UpdateTwoFactorWebAuthnRequest } from "@bitwarden/common/models/request/update-two-factor-web-authn.request";
import {
ChallengeResponse,
TwoFactorWebAuthnResponse,
} from "@bitwarden/common/models/response/two-factor-web-authn.response";
import { AuthResponse } from "@bitwarden/common/types/authResponse";
import { TwoFactorBaseComponent } from "./two-factor-base.component";
interface Key {
id: number;
name: string;
configured: boolean;
migrated?: boolean;
removePromise: Promise<TwoFactorWebAuthnResponse> | null;
}
@Component({
selector: "app-two-factor-webauthn",
templateUrl: "two-factor-webauthn.component.html",
})
export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent {
type = TwoFactorProviderType.WebAuthn;
name: string;
keys: Key[];
keyIdAvailable: number = null;
keysConfiguredCount = 0;
webAuthnError: boolean;
webAuthnListening: boolean;
webAuthnResponse: PublicKeyCredential;
challengePromise: Promise<ChallengeResponse>;
formPromise: Promise<TwoFactorWebAuthnResponse>;
constructor(
apiService: ApiService,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
private ngZone: NgZone,
logService: LogService,
userVerificationService: UserVerificationService
) {
super(apiService, i18nService, platformUtilsService, logService, userVerificationService);
}
auth(authResponse: AuthResponse<TwoFactorWebAuthnResponse>) {
super.auth(authResponse);
this.processResponse(authResponse.response);
}
async submit() {
if (this.webAuthnResponse == null || this.keyIdAvailable == null) {
// Should never happen.
return Promise.reject();
}
const request = await this.buildRequestModel(UpdateTwoFactorWebAuthnRequest);
request.deviceResponse = this.webAuthnResponse;
request.id = this.keyIdAvailable;
request.name = this.name;
return super.enable(async () => {
this.formPromise = this.apiService.putTwoFactorWebAuthn(request);
const response = await this.formPromise;
await this.processResponse(response);
});
}
disable() {
return super.disable(this.formPromise);
}
async remove(key: Key) {
if (this.keysConfiguredCount <= 1 || key.removePromise != null) {
return;
}
const name = key.name != null ? key.name : this.i18nService.t("webAuthnkeyX", key.id as any);
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("removeU2fConfirmation"),
name,
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
);
if (!confirmed) {
return;
}
const request = await this.buildRequestModel(UpdateTwoFactorWebAuthnDeleteRequest);
request.id = key.id;
try {
key.removePromise = this.apiService.deleteTwoFactorWebAuthn(request);
const response = await key.removePromise;
key.removePromise = null;
await this.processResponse(response);
} catch (e) {
this.logService.error(e);
}
}
async readKey() {
if (this.keyIdAvailable == null) {
return;
}
const request = await this.buildRequestModel(SecretVerificationRequest);
try {
this.challengePromise = this.apiService.getTwoFactorWebAuthnChallenge(request);
const challenge = await this.challengePromise;
this.readDevice(challenge);
} catch (e) {
this.logService.error(e);
}
}
private readDevice(webAuthnChallenge: ChallengeResponse) {
// eslint-disable-next-line
console.log("listening for key...");
this.resetWebAuthn(true);
navigator.credentials
.create({
publicKey: webAuthnChallenge,
})
.then((data: PublicKeyCredential) => {
this.ngZone.run(() => {
this.webAuthnListening = false;
this.webAuthnResponse = data;
});
})
.catch((err) => {
// eslint-disable-next-line
console.error(err);
this.resetWebAuthn(false);
// TODO: Should we display the actual error?
this.webAuthnError = true;
});
}
private resetWebAuthn(listening = false) {
this.webAuthnResponse = null;
this.webAuthnError = false;
this.webAuthnListening = listening;
}
private processResponse(response: TwoFactorWebAuthnResponse) {
this.resetWebAuthn();
this.keys = [];
this.keyIdAvailable = null;
this.name = null;
this.keysConfiguredCount = 0;
for (let i = 1; i <= 5; i++) {
if (response.keys != null) {
const key = response.keys.filter((k) => k.id === i);
if (key.length > 0) {
this.keysConfiguredCount++;
this.keys.push({
id: i,
name: key[0].name,
configured: true,
migrated: key[0].migrated,
removePromise: null,
});
continue;
}
}
this.keys.push({ id: i, name: null, configured: false, removePromise: null });
if (this.keyIdAvailable == null) {
this.keyIdAvailable = i;
}
}
this.enabled = response.enabled;
}
}

View File

@@ -1,125 +0,0 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="2faYubiKeyTitle">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title" id="2faYubiKeyTitle">
{{ "twoStepLogin" | i18n }}
<small>YubiKey</small>
</h1>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<app-two-factor-verify
[organizationId]="organizationId"
[type]="type"
(onAuthed)="auth($any($event))"
*ngIf="!authed"
>
</app-two-factor-verify>
<form
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
*ngIf="authed"
autocomplete="off"
>
<div class="modal-body">
<app-callout
type="success"
title="{{ 'enabled' | i18n }}"
icon="bwi bwi-check-circle"
*ngIf="enabled"
>
{{ "twoStepLoginProviderEnabled" | i18n }}
</app-callout>
<app-callout type="warning">
<p>{{ "twoFactorYubikeyWarning" | i18n }}</p>
<ul class="mb-0">
<li>{{ "twoFactorYubikeySupportUsb" | i18n }}</li>
<li>{{ "twoFactorYubikeySupportMobile" | i18n }}</li>
</ul>
</app-callout>
<img class="float-right mfaType3" alt="YubiKey OTP security key logo" />
<p>{{ "twoFactorYubikeyAdd" | i18n }}:</p>
<ol>
<li>{{ "twoFactorYubikeyPlugIn" | i18n }}</li>
<li>{{ "twoFactorYubikeySelectKey" | i18n }}</li>
<li>{{ "twoFactorYubikeyTouchButton" | i18n }}</li>
<li>{{ "twoFactorYubikeySaveForm" | i18n }}</li>
</ol>
<hr />
<div class="row">
<div class="form-group col-6" *ngFor="let k of keys; let i = index">
<label for="key{{ i + 1 }}">{{ "yubikeyX" | i18n: i + 1 }}</label>
<input
id="key{{ i + 1 }}"
type="password"
name="Key{{ i + 1 }}"
class="form-control"
[(ngModel)]="k.key"
*ngIf="!k.existingKey"
appInputVerbatim
autocomplete="new-password"
/>
<div class="d-flex" *ngIf="k.existingKey">
<span class="mr-2">{{ k.existingKey }}</span>
<button
type="button"
class="btn btn-link text-danger ml-auto"
(click)="remove(k)"
appA11yTitle="{{ 'remove' | i18n }}"
>
<i class="bwi bwi-minus-circle bwi-lg" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<strong class="d-block mb-2">{{ "nfcSupport" | i18n }}</strong>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="nfc" name="Nfc" [(ngModel)]="nfc" />
<label class="form-check-label" for="nfc">{{
"twoFactorYubikeySupportsNfc" | i18n
}}</label>
</div>
<small class="form-text text-muted">{{ "twoFactorYubikeySupportsNfcDesc" | i18n }}</small>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i
class="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span>{{ "save" | i18n }}</span>
</button>
<button
#disableBtn
type="button"
class="btn btn-outline-secondary btn-submit"
[appApiAction]="disablePromise"
[disabled]="$any(disableBtn).loading"
(click)="disable()"
*ngIf="enabled"
>
<i
class="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span>{{ "disableAllKeys" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "close" | i18n }}
</button>
</div>
</form>
</div>
</div>
</div>

View File

@@ -1,95 +0,0 @@
import { Component } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
import { TwoFactorProviderType } from "@bitwarden/common/enums/twoFactorProviderType";
import { UpdateTwoFactorYubioOtpRequest } from "@bitwarden/common/models/request/update-two-factor-yubio-otp.request";
import { TwoFactorYubiKeyResponse } from "@bitwarden/common/models/response/two-factor-yubi-key.response";
import { AuthResponse } from "@bitwarden/common/types/authResponse";
import { TwoFactorBaseComponent } from "./two-factor-base.component";
interface Key {
key: string;
existingKey: string;
}
@Component({
selector: "app-two-factor-yubikey",
templateUrl: "two-factor-yubikey.component.html",
})
export class TwoFactorYubiKeyComponent extends TwoFactorBaseComponent {
type = TwoFactorProviderType.Yubikey;
keys: Key[];
nfc = false;
formPromise: Promise<TwoFactorYubiKeyResponse>;
disablePromise: Promise<unknown>;
constructor(
apiService: ApiService,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
logService: LogService,
userVerificationService: UserVerificationService
) {
super(apiService, i18nService, platformUtilsService, logService, userVerificationService);
}
auth(authResponse: AuthResponse<TwoFactorYubiKeyResponse>) {
super.auth(authResponse);
this.processResponse(authResponse.response);
}
async submit() {
const request = await this.buildRequestModel(UpdateTwoFactorYubioOtpRequest);
request.key1 = this.keys != null && this.keys.length > 0 ? this.keys[0].key : null;
request.key2 = this.keys != null && this.keys.length > 1 ? this.keys[1].key : null;
request.key3 = this.keys != null && this.keys.length > 2 ? this.keys[2].key : null;
request.key4 = this.keys != null && this.keys.length > 3 ? this.keys[3].key : null;
request.key5 = this.keys != null && this.keys.length > 4 ? this.keys[4].key : null;
request.nfc = this.nfc;
return super.enable(async () => {
this.formPromise = this.apiService.putTwoFactorYubiKey(request);
const response = await this.formPromise;
await this.processResponse(response);
this.platformUtilsService.showToast("success", null, this.i18nService.t("yubikeysUpdated"));
});
}
disable() {
return super.disable(this.disablePromise);
}
remove(key: Key) {
key.existingKey = null;
key.key = null;
}
private processResponse(response: TwoFactorYubiKeyResponse) {
this.enabled = response.enabled;
this.keys = [
{ key: response.key1, existingKey: this.padRight(response.key1) },
{ key: response.key2, existingKey: this.padRight(response.key2) },
{ key: response.key3, existingKey: this.padRight(response.key3) },
{ key: response.key4, existingKey: this.padRight(response.key4) },
{ key: response.key5, existingKey: this.padRight(response.key5) },
];
this.nfc = response.nfc || !response.enabled;
}
private padRight(str: string, character = "•", size = 44) {
if (str == null || character == null || str.length >= size) {
return str;
}
const max = (size - str.length) / character.length;
for (let i = 0; i < max; i++) {
str += character;
}
return str;
}
}

View File

@@ -1,11 +0,0 @@
<div class="tw-rounded tw-border tw-border-solid tw-border-warning-500 tw-bg-background">
<div class="tw-bg-warning-500 tw-px-5 tw-py-2.5 tw-font-bold tw-uppercase tw-text-contrast">
<i class="bwi bwi-envelope bwi-fw" aria-hidden="true"></i> {{ "verifyEmail" | i18n }}
</div>
<div class="tw-p-5">
<p>{{ "verifyEmailDesc" | i18n }}</p>
<button id="sendBtn" bitButton type="button" block [bitAction]="send">
{{ "sendEmail" | i18n }}
</button>
</div>
</div>

View File

@@ -1,45 +0,0 @@
import { Component, EventEmitter, Output } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { TokenService } from "@bitwarden/common/abstractions/token.service";
@Component({
selector: "app-verify-email",
templateUrl: "verify-email.component.html",
})
export class VerifyEmailComponent {
actionPromise: Promise<unknown>;
@Output() onVerified = new EventEmitter<boolean>();
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private logService: LogService,
private tokenService: TokenService
) {}
async verifyEmail(): Promise<void> {
await this.apiService.refreshIdentityToken();
if (await this.tokenService.getEmailVerified()) {
this.onVerified.emit(true);
this.platformUtilsService.showToast("success", null, this.i18nService.t("emailVerified"));
return;
}
await this.apiService.postAccountVerifyEmail();
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("checkInboxForVerification")
);
}
send = async () => {
await this.verifyEmail();
};
}