1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-05 18:13:26 +00:00

move web settings to auth (#7022)

This commit is contained in:
Jake Fink
2023-11-30 17:15:06 -05:00
committed by GitHub
parent cf6ed0d8a6
commit 8a0fa574c7
27 changed files with 18 additions and 18 deletions

View File

@@ -0,0 +1,72 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="apiKeyTitle">
<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="apiKeyTitle">{{ apiKeyTitle | 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>{{ apiKeyDescription | i18n }}</p>
<app-user-verification
[(ngModel)]="masterPassword"
ngDefaultControl
name="secret"
*ngIf="!clientSecret"
>
</app-user-verification>
<app-callout type="warning" *ngIf="clientSecret">{{ apiKeyWarning | i18n }}</app-callout>
<app-callout
type="info"
title="{{ 'oauth2ClientCredentials' | i18n }}"
icon="bwi bwi-key"
*ngIf="clientSecret"
>
<p class="mb-1">
<strong>client_id:</strong><br />
<code>{{ clientId }}</code>
</p>
<p class="mb-1">
<strong>client_secret:</strong><br />
<code>{{ clientSecret }}</code>
</p>
<p class="mb-1">
<strong>scope:</strong><br />
<code>{{ scope }}</code>
</p>
<p class="mb-0">
<strong>grant_type:</strong><br />
<code>{{ grantType }}</code>
</p>
</app-callout>
</div>
<div class="modal-footer">
<button
type="submit"
class="btn btn-primary btn-submit"
[disabled]="form.loading"
*ngIf="!clientSecret"
>
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ (isRotation ? "rotateApiKey" : "viewApiKey") | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "close" | i18n }}
</button>
</div>
</form>
</div>
</div>

View File

@@ -0,0 +1,46 @@
import { Component } from "@angular/core";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
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/auth/types/verification";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@Component({
selector: "app-api-key",
templateUrl: "api-key.component.html",
})
export class ApiKeyComponent {
keyType: string;
isRotation: boolean;
postKey: (entityId: string, request: SecretVerificationRequest) => Promise<ApiKeyResponse>;
entityId: string;
scope: string;
grantType: string;
apiKeyTitle: string;
apiKeyWarning: string;
apiKeyDescription: string;
masterPassword: Verification;
formPromise: Promise<ApiKeyResponse>;
clientId: string;
clientSecret: string;
constructor(
private userVerificationService: UserVerificationService,
private logService: LogService,
) {}
async submit() {
try {
this.formPromise = this.userVerificationService
.buildRequest(this.masterPassword)
.then((request) => this.postKey(this.entityId, request));
const response = await this.formPromise;
this.clientSecret = response.apiKey;
this.clientId = `${this.keyType}.${this.entityId}`;
} catch (e) {
this.logService.error(e);
}
}
}

View File

@@ -0,0 +1,50 @@
<bit-dialog>
<span bitDialogTitle>
{{ "changeKdf" | i18n }}
</span>
<span bitDialogContent>
<bit-callout type="warning">{{ "changeKdfLoggedOutWarning" | i18n }}</bit-callout>
<form
id="form"
[formGroup]="form"
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
autocomplete="off"
>
<div class="row">
<div class="col-12">
<bit-form-field class="tw-mb-1"
><bit-label>{{ "masterPass" | i18n }}</bit-label>
<input
bitInput
type="password"
required
formControlName="masterPassword"
appAutofocus
/>
<button
type="button"
bitSuffix
bitIconButton
bitPasswordInputToggle
[(toggled)]="showPassword"
></button
><bit-hint>
{{ "confirmIdentity" | i18n }}
</bit-hint></bit-form-field
>
</div>
</div>
</form>
</span>
<ng-container bitDialogFooter>
<button bitButton buttonType="primary" type="submit" [loading]="loading" form="form">
<span>{{ "changeKdf" | i18n }}</span>
</button>
<button bitButton buttonType="secondary" type="button" bitDialogClose>
{{ "cancel" | i18n }}
</button>
</ng-container>
</bit-dialog>

View File

@@ -0,0 +1,91 @@
import { DIALOG_DATA } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core";
import { FormGroup, FormControl, Validators } from "@angular/forms";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
import { KdfRequest } from "@bitwarden/common/models/request/kdf.request";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { KdfType } from "@bitwarden/common/platform/enums";
@Component({
selector: "app-change-kdf-confirmation",
templateUrl: "change-kdf-confirmation.component.html",
})
export class ChangeKdfConfirmationComponent {
kdf: KdfType;
kdfConfig: KdfConfig;
form = new FormGroup({
masterPassword: new FormControl(null, Validators.required),
});
showPassword = false;
masterPassword: string;
formPromise: Promise<any>;
loading = false;
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private cryptoService: CryptoService,
private messagingService: MessagingService,
private stateService: StateService,
private logService: LogService,
@Inject(DIALOG_DATA) params: { kdf: KdfType; kdfConfig: KdfConfig },
) {
this.kdf = params.kdf;
this.kdfConfig = params.kdfConfig;
this.masterPassword = null;
}
async submit() {
this.loading = true;
try {
this.formPromise = this.makeKeyAndSaveAsync();
await this.formPromise;
this.platformUtilsService.showToast(
"success",
this.i18nService.t("encKeySettingsChanged"),
this.i18nService.t("logBackIn"),
);
this.messagingService.send("logout");
} catch (e) {
this.logService.error(e);
} finally {
this.loading = false;
}
}
private async makeKeyAndSaveAsync() {
const masterPassword = this.form.value.masterPassword;
const request = new KdfRequest();
request.kdf = this.kdf;
request.kdfIterations = this.kdfConfig.iterations;
request.kdfMemory = this.kdfConfig.memory;
request.kdfParallelism = this.kdfConfig.parallelism;
const masterKey = await this.cryptoService.getOrDeriveMasterKey(masterPassword);
request.masterPasswordHash = await this.cryptoService.hashMasterKey(masterPassword, masterKey);
const email = await this.stateService.getEmail();
const newMasterKey = await this.cryptoService.makeMasterKey(
masterPassword,
email,
this.kdf,
this.kdfConfig,
);
request.newMasterPasswordHash = await this.cryptoService.hashMasterKey(
masterPassword,
newMasterKey,
);
const newUserKey = await this.cryptoService.encryptUserKeyWithMasterKey(newMasterKey);
request.key = newUserKey[1].encryptedString;
await this.apiService.postAccountKdf(request);
}
}

View File

@@ -0,0 +1,118 @@
<div class="tabbed-header">
<h1>{{ "encKeySettings" | i18n }}</h1>
</div>
<bit-callout type="warning">{{ "changeKdfLoggedOutWarning" | i18n }}</bit-callout>
<form #form ngNativeValidate autocomplete="off">
<div class="row">
<div class="col-6">
<div class="form-group mb-0">
<label for="kdf">{{ "kdfAlgorithm" | i18n }}</label>
<a
class="ml-auto"
href="https://bitwarden.com/help/kdf-algorithms"
target="_blank"
rel="noopener"
appA11yTitle="{{ 'learnMore' | i18n }}"
>
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
</a>
<select
id="kdf"
name="Kdf"
[(ngModel)]="kdf"
(ngModelChange)="onChangeKdf($event)"
class="form-control mb-3"
required
>
<option *ngFor="let o of kdfOptions" [ngValue]="o.value">{{ o.name }}</option>
</select>
<ng-container *ngIf="kdf == kdfType.Argon2id">
<label for="kdfMemory">{{ "kdfMemory" | i18n }}</label>
<input
id="kdfMemory"
type="number"
min="16"
max="1024"
name="Memory"
class="form-control mb-3"
[(ngModel)]="kdfConfig.memory"
required
/>
</ng-container>
</div>
</div>
<div class="col-6">
<div class="form-group mb-0">
<ng-container *ngIf="kdf == kdfType.PBKDF2_SHA256">
<label for="kdfIterations">{{ "kdfIterations" | i18n }}</label>
<a
class="ml-auto"
href="https://bitwarden.com/help/what-encryption-is-used/#changing-kdf-iterations"
target="_blank"
rel="noopener"
appA11yTitle="{{ 'learnMore' | i18n }}"
>
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
</a>
<input
id="kdfIterations"
type="number"
min="100000"
max="2000000"
name="KdfIterations"
class="form-control"
[(ngModel)]="kdfConfig.iterations"
required
/>
</ng-container>
<ng-container *ngIf="kdf == kdfType.Argon2id">
<label for="kdfIterations">{{ "kdfIterations" | i18n }}</label>
<input
id="iterations"
type="number"
min="2"
max="10"
name="Iterations"
class="form-control mb-3"
[(ngModel)]="kdfConfig.iterations"
required
/>
<label for="kdfParallelism">{{ "kdfParallelism" | i18n }}</label>
<input
id="kdfParallelism"
type="number"
min="1"
max="16"
name="Parallelism"
class="form-control"
[(ngModel)]="kdfConfig.parallelism"
required
/>
</ng-container>
</div>
</div>
<div class="col-12">
<ng-container *ngIf="kdf == kdfType.PBKDF2_SHA256">
<p class="small form-text text-muted">
{{ "kdfIterationsDesc" | i18n: (recommendedPbkdf2Iterations | number) }}
</p>
<bit-callout type="warning">
{{ "kdfIterationsWarning" | i18n: (100000 | number) }}
</bit-callout>
</ng-container>
<ng-container *ngIf="kdf == kdfType.Argon2id">
<p class="small form-text text-muted">{{ "argon2Desc" | i18n }}</p>
<bit-callout type="warning"> {{ "argon2Warning" | i18n }}</bit-callout>
</ng-container>
</div>
</div>
<button
(click)="openConfirmationModal()"
type="button"
buttonType="primary"
bitButton
[loading]="form.loading"
>
{{ "changeKdf" | i18n }}
</button>
</form>

View File

@@ -0,0 +1,65 @@
import { Component, OnInit } from "@angular/core";
import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import {
DEFAULT_KDF_CONFIG,
DEFAULT_PBKDF2_ITERATIONS,
DEFAULT_ARGON2_ITERATIONS,
DEFAULT_ARGON2_MEMORY,
DEFAULT_ARGON2_PARALLELISM,
KdfType,
} from "@bitwarden/common/platform/enums";
import { DialogService } from "@bitwarden/components";
import { ChangeKdfConfirmationComponent } from "./change-kdf-confirmation.component";
@Component({
selector: "app-change-kdf",
templateUrl: "change-kdf.component.html",
})
export class ChangeKdfComponent implements OnInit {
kdf = KdfType.PBKDF2_SHA256;
kdfConfig: KdfConfig = DEFAULT_KDF_CONFIG;
kdfType = KdfType;
kdfOptions: any[] = [];
recommendedPbkdf2Iterations = DEFAULT_PBKDF2_ITERATIONS;
constructor(
private stateService: StateService,
private dialogService: DialogService,
) {
this.kdfOptions = [
{ name: "PBKDF2 SHA-256", value: KdfType.PBKDF2_SHA256 },
{ name: "Argon2id", value: KdfType.Argon2id },
];
}
async ngOnInit() {
this.kdf = await this.stateService.getKdfType();
this.kdfConfig = await this.stateService.getKdfConfig();
}
async onChangeKdf(newValue: KdfType) {
if (newValue === KdfType.PBKDF2_SHA256) {
this.kdfConfig = new KdfConfig(DEFAULT_PBKDF2_ITERATIONS);
} else if (newValue === KdfType.Argon2id) {
this.kdfConfig = new KdfConfig(
DEFAULT_ARGON2_ITERATIONS,
DEFAULT_ARGON2_MEMORY,
DEFAULT_ARGON2_PARALLELISM,
);
} else {
throw new Error("Unknown KDF type.");
}
}
async openConfirmationModal() {
this.dialogService.open(ChangeKdfConfirmationComponent, {
data: {
kdf: this.kdf,
kdfConfig: this.kdfConfig,
},
});
}
}

View File

@@ -0,0 +1,14 @@
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { SharedModule } from "../../../../shared";
import { ChangeKdfConfirmationComponent } from "./change-kdf-confirmation.component";
import { ChangeKdfComponent } from "./change-kdf.component";
@NgModule({
imports: [CommonModule, SharedModule],
declarations: [ChangeKdfComponent, ChangeKdfConfirmationComponent],
exports: [ChangeKdfComponent, ChangeKdfConfirmationComponent],
})
export class ChangeKdfModule {}

View File

@@ -0,0 +1,18 @@
<app-change-kdf *ngIf="showChangeKdf"></app-change-kdf>
<div
[ngClass]="{ 'tabbed-header': !showChangeKdf, 'secondary-header': showChangeKdf }"
class="border-0 mb-0"
>
<h1>{{ "apiKey" | i18n }}</h1>
</div>
<p>
{{ "userApiKeyDesc" | i18n }}
</p>
<button type="button" bitButton buttonType="secondary" (click)="viewUserApiKey()">
{{ "viewApiKey" | i18n }}
</button>
<button type="button" bitButton buttonType="secondary" (click)="rotateUserApiKey()">
{{ "rotateApiKey" | i18n }}
</button>
<ng-template #viewUserApiKeyTemplate></ng-template>
<ng-template #rotateUserApiKeyTemplate></ng-template>

View File

@@ -0,0 +1,61 @@
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 { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { ApiKeyComponent } from "./api-key.component";
@Component({
selector: "app-security-keys",
templateUrl: "security-keys.component.html",
})
export class SecurityKeysComponent implements OnInit {
@ViewChild("viewUserApiKeyTemplate", { read: ViewContainerRef, static: true })
viewUserApiKeyModalRef: ViewContainerRef;
@ViewChild("rotateUserApiKeyTemplate", { read: ViewContainerRef, static: true })
rotateUserApiKeyModalRef: ViewContainerRef;
showChangeKdf = true;
constructor(
private userVerificationService: UserVerificationService,
private stateService: StateService,
private modalService: ModalService,
private apiService: ApiService,
) {}
async ngOnInit() {
this.showChangeKdf = await this.userVerificationService.hasMasterPassword();
}
async viewUserApiKey() {
const entityId = await this.stateService.getUserId();
await this.modalService.openViewRef(ApiKeyComponent, this.viewUserApiKeyModalRef, (comp) => {
comp.keyType = "user";
comp.entityId = entityId;
comp.postKey = this.apiService.postUserApiKey.bind(this.apiService);
comp.scope = "api";
comp.grantType = "client_credentials";
comp.apiKeyTitle = "apiKey";
comp.apiKeyWarning = "userApiKeyWarning";
comp.apiKeyDescription = "userApiKeyDesc";
});
}
async rotateUserApiKey() {
const entityId = await this.stateService.getUserId();
await this.modalService.openViewRef(ApiKeyComponent, this.rotateUserApiKeyModalRef, (comp) => {
comp.keyType = "user";
comp.isRotation = true;
comp.entityId = entityId;
comp.postKey = this.apiService.postUserRotateApiKey.bind(this.apiService);
comp.scope = "api";
comp.grantType = "client_credentials";
comp.apiKeyTitle = "apiKey";
comp.apiKeyWarning = "userApiKeyWarning";
comp.apiKeyDescription = "apiKeyRotateDesc";
});
}
}

View File

@@ -0,0 +1,40 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { ChangePasswordComponent } from "../change-password.component";
import { TwoFactorSetupComponent } from "../two-factor-setup.component";
import { SecurityKeysComponent } from "./security-keys.component";
import { SecurityComponent } from "./security.component";
const routes: Routes = [
{
path: "",
component: SecurityComponent,
data: { titleId: "security" },
children: [
{ path: "", pathMatch: "full", redirectTo: "change-password" },
{
path: "change-password",
component: ChangePasswordComponent,
data: { titleId: "masterPassword" },
},
{
path: "two-factor",
component: TwoFactorSetupComponent,
data: { titleId: "twoStepLogin" },
},
{
path: "security-keys",
component: SecurityKeysComponent,
data: { titleId: "keys" },
},
],
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class SecurityRoutingModule {}

View File

@@ -0,0 +1,22 @@
<div class="tabbed-nav d-flex flex-column">
<ul class="nav nav-tabs">
<ng-container *ngIf="showChangePassword">
<li class="nav-item">
<a class="nav-link" routerLink="change-password" routerLinkActive="active">
{{ "masterPassword" | i18n }}
</a>
</li>
</ng-container>
<li class="nav-item">
<a class="nav-link" routerLink="two-factor" routerLinkActive="active">
{{ "twoStepLogin" | i18n }}
</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="security-keys" routerLinkActive="active">
{{ "keys" | i18n }}
</a>
</li>
</ul>
</div>
<router-outlet></router-outlet>

View File

@@ -0,0 +1,17 @@
import { Component } from "@angular/core";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
@Component({
selector: "app-security",
templateUrl: "security.component.html",
})
export class SecurityComponent {
showChangePassword = true;
constructor(private userVerificationService: UserVerificationService) {}
async ngOnInit() {
this.showChangePassword = await this.userVerificationService.hasMasterPassword();
}
}