1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-14 07:13:32 +00:00

Add support for requesting and using otp for verifying some requests (#527)

Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
This commit is contained in:
Oscar Hinton
2021-11-09 17:01:22 +01:00
committed by GitHub
parent 99ff3feb53
commit 8f177e2d3a
54 changed files with 746 additions and 223 deletions

View File

@@ -22,6 +22,7 @@ import { FolderService } from 'jslib-common/abstractions/folder.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
import { PasswordRepromptService } from 'jslib-common/abstractions/passwordReprompt.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { PolicyService } from 'jslib-common/abstractions/policy.service';
import { StateService } from 'jslib-common/abstractions/state.service';
@@ -80,6 +81,7 @@ export class AddEditComponent implements OnInit {
currentDate = new Date();
allowPersonal = true;
reprompt: boolean = false;
canUseReprompt: boolean = true;
protected writeableCollections: CollectionView[];
private previousCipherId: string;
@@ -89,7 +91,8 @@ export class AddEditComponent implements OnInit {
protected auditService: AuditService, protected stateService: StateService,
protected userService: UserService, protected collectionService: CollectionService,
protected messagingService: MessagingService, protected eventService: EventService,
protected policyService: PolicyService, private logService: LogService) {
protected policyService: PolicyService, protected passwordRepromptService: PasswordRepromptService,
private logService: LogService) {
this.typeOptions = [
{ name: i18nService.t('typeLogin'), value: CipherType.Login },
{ name: i18nService.t('typeCard'), value: CipherType.Card },
@@ -169,6 +172,8 @@ export class AddEditComponent implements OnInit {
}
this.writeableCollections = await this.loadCollections();
this.canUseReprompt = await this.passwordRepromptService.enabled();
}
async load() {

View File

@@ -4,6 +4,7 @@ import {
OnInit,
Output,
} from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { EventService } from 'jslib-common/abstractions/event.service';
@@ -12,6 +13,7 @@ import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { PolicyService } from 'jslib-common/abstractions/policy.service';
import { UserVerificationService } from 'jslib-common/abstractions/userVerification.service';
import { EventType } from 'jslib-common/enums/eventType';
import { PolicyType } from 'jslib-common/enums/policyType';
@@ -21,15 +23,24 @@ export class ExportComponent implements OnInit {
@Output() onSaved = new EventEmitter();
formPromise: Promise<string>;
masterPassword: string;
format: 'json' | 'encrypted_json' | 'csv' = 'json';
showPassword = false;
disabledByPolicy: boolean = false;
exportForm = this.fb.group({
format: ['json'],
secret: [''],
});
formatOptions = [
{ name: '.json', value: 'json' },
{ name: '.csv', value: 'csv' },
{ name: '.json (Encrypted)', value: 'encrypted_json' },
];
constructor(protected cryptoService: CryptoService, protected i18nService: I18nService,
protected platformUtilsService: PlatformUtilsService, protected exportService: ExportService,
protected eventService: EventService, private policyService: PolicyService, protected win: Window,
private logService: LogService) { }
private logService: LogService, private userVerificationService: UserVerificationService,
private fb: FormBuilder) { }
async ngOnInit() {
await this.checkExportDisabled();
@@ -37,6 +48,9 @@ export class ExportComponent implements OnInit {
async checkExportDisabled() {
this.disabledByPolicy = await this.policyService.policyAppliesToUser(PolicyType.DisablePersonalVaultExport);
if (this.disabledByPolicy) {
this.exportForm.disable();
}
}
get encryptedFormat() {
@@ -49,31 +63,25 @@ export class ExportComponent implements OnInit {
return;
}
if (this.masterPassword == null || this.masterPassword === '') {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('invalidMasterPassword'));
return;
}
const acceptedWarning = await this.warningDialog();
if (!acceptedWarning) {
return;
}
const passwordValid = await this.cryptoService.compareAndUpdateKeyHash(this.masterPassword, null);
if (passwordValid) {
try {
this.formPromise = this.getExportData();
const data = await this.formPromise;
this.downloadFile(data);
this.saved();
await this.collectEvent();
} catch (e) {
this.logService.error(e);
}
} else {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('invalidMasterPassword'));
const secret = this.exportForm.get('secret').value;
if (!await this.userVerificationService.verifyUser(secret)) {
return;
}
try {
this.formPromise = this.getExportData();
const data = await this.formPromise;
this.downloadFile(data);
this.saved();
await this.collectEvent();
this.exportForm.get('secret').setValue('');
} catch (e) {
this.logService.error(e);
}
}
@@ -93,11 +101,6 @@ export class ExportComponent implements OnInit {
}
}
togglePassword() {
this.showPassword = !this.showPassword;
document.getElementById('masterPassword').focus();
}
protected saved() {
this.onSaved.emit();
}
@@ -123,6 +126,10 @@ export class ExportComponent implements OnInit {
await this.eventService.collect(EventType.User_ClientExportedVault);
}
get format() {
return this.exportForm.get('format').value;
}
private downloadFile(csv: string): void {
const fileName = this.getFileName();
this.platformUtilsService.saveFile(this.win, csv, { type: 'text/plain' }, fileName);

View File

@@ -5,6 +5,7 @@ import { ApiService } from 'jslib-common/abstractions/api.service';
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { EnvironmentService } from 'jslib-common/abstractions/environment.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { KeyConnectorService } from 'jslib-common/abstractions/keyConnector.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
@@ -18,7 +19,7 @@ import { ConstantsService } from 'jslib-common/services/constants.service';
import { EncString } from 'jslib-common/models/domain/encString';
import { SymmetricCryptoKey } from 'jslib-common/models/domain/symmetricCryptoKey';
import { PasswordVerificationRequest } from 'jslib-common/models/request/passwordVerificationRequest';
import { SecretVerificationRequest } from 'jslib-common/models/request/secretVerificationRequest';
import { Utils } from 'jslib-common/misc/utils';
@@ -48,7 +49,8 @@ export class LockComponent implements OnInit {
protected userService: UserService, protected cryptoService: CryptoService,
protected storageService: StorageService, protected vaultTimeoutService: VaultTimeoutService,
protected environmentService: EnvironmentService, protected stateService: StateService,
protected apiService: ApiService, private logService: LogService) { }
protected apiService: ApiService, private logService: LogService,
private keyConnectorService: KeyConnectorService) { }
async ngOnInit() {
this.pinSet = await this.vaultTimeoutService.isPinLockSet();
@@ -59,6 +61,11 @@ export class LockComponent implements OnInit {
this.biometricText = await this.storageService.get(ConstantsService.biometricText);
this.email = await this.userService.getEmail();
// Users with key connector and without biometric or pin has no MP to unlock using
if (await this.keyConnectorService.getUsesKeyConnector() && !(this.biometricLock || this.pinLock)) {
await this.vaultTimeoutService.logOut();
}
const webVaultUrl = this.environmentService.getWebVaultUrl();
const vaultUrl = webVaultUrl === 'https://vault.bitwarden.com' ? 'https://bitwarden.com' : webVaultUrl;
this.webVaultHostname = Utils.getHostname(vaultUrl);
@@ -119,7 +126,7 @@ export class LockComponent implements OnInit {
if (storedKeyHash != null) {
passwordValid = await this.cryptoService.compareAndUpdateKeyHash(this.masterPassword, key);
} else {
const request = new PasswordVerificationRequest();
const request = new SecretVerificationRequest();
const serverKeyHash = await this.cryptoService.hashPassword(this.masterPassword, key,
HashPurpose.ServerAuthorization);
request.masterPasswordHash = serverKeyHash;

View File

@@ -0,0 +1,77 @@
import {
Directive,
OnInit,
} from '@angular/core';
import { Router } from '@angular/router';
import { ApiService } from 'jslib-common/abstractions/api.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { KeyConnectorService } from 'jslib-common/abstractions/keyConnector.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { StorageService } from 'jslib-common/abstractions/storage.service';
import { SyncService } from 'jslib-common/abstractions/sync.service';
import { UserService } from 'jslib-common/abstractions/user.service';
import { ConstantsService } from 'jslib-common/services/constants.service';
import { Organization } from 'jslib-common/models/domain/organization';
@Directive()
export class RemovePasswordComponent implements OnInit {
actionPromise: Promise<any>;
continuing: boolean = false;
leaving: boolean = false;
loading: boolean = true;
organization: Organization;
email: string;
constructor(private router: Router, private userService: UserService,
private apiService: ApiService, private syncService: SyncService,
private platformUtilsService: PlatformUtilsService, private i18nService: I18nService,
private keyConnectorService: KeyConnectorService, private storageService: StorageService) { }
async ngOnInit() {
this.organization = await this.keyConnectorService.getManagingOrganization();
this.email = await this.userService.getEmail();
await this.syncService.fullSync(false);
this.loading = false;
}
async convert() {
this.continuing = true;
this.actionPromise = this.keyConnectorService.migrateUser();
try {
await this.actionPromise;
this.platformUtilsService.showToast('success', null, this.i18nService.t('removedMasterPassword'));
await this.keyConnectorService.removeConvertAccountRequired();
this.router.navigate(['']);
} catch (e) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), e.message);
}
}
async leave() {
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('leaveOrganizationConfirmation'), this.organization.name,
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return false;
}
try {
this.leaving = true;
this.actionPromise = this.apiService.postLeaveOrganization(this.organization.id).then(() => {
return this.syncService.fullSync(true);
});
await this.actionPromise;
this.platformUtilsService.showToast('success', null, this.i18nService.t('leftOrganization'));
await this.keyConnectorService.removeConvertAccountRequired();
this.router.navigate(['']);
} catch (e) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), e);
}
}
}

View File

@@ -60,7 +60,7 @@ export class SsoComponent {
await this.storageService.remove(ConstantsService.ssoCodeVerifierKey);
await this.storageService.remove(ConstantsService.ssoStateKey);
if (qParams.code != null && codeVerifier != null && state != null && this.checkState(state, qParams.state)) {
await this.logIn(qParams.code, codeVerifier, this.getOrgIdentiferFromState(qParams.state));
await this.logIn(qParams.code, codeVerifier, this.getOrgIdentifierFromState(qParams.state));
}
} else if (qParams.clientId != null && qParams.redirectUri != null && qParams.state != null &&
qParams.codeChallenge != null) {
@@ -183,14 +183,14 @@ export class SsoComponent {
}
} catch (e) {
this.logService.error(e);
if (e.message === 'Unable to reach crypto agent') {
this.platformUtilsService.showToast('error', null, this.i18nService.t('ssoCryptoAgentUnavailable'));
if (e.message === 'Unable to reach key connector') {
this.platformUtilsService.showToast('error', null, this.i18nService.t('ssoKeyConnectorUnavailable'));
}
}
this.loggingIn = false;
}
private getOrgIdentiferFromState(state: string): string {
private getOrgIdentifierFromState(state: string): string {
if (state === null || state === undefined) {
return null;
}

View File

@@ -211,7 +211,9 @@ export class TwoFactorComponent implements OnInit, OnDestroy {
}
try {
const request = new TwoFactorEmailRequest(this.authService.email, this.authService.masterPasswordHash);
const request = new TwoFactorEmailRequest();
request.email = this.authService.email;
request.masterPasswordHash = this.authService.masterPasswordHash;
this.emailPromise = this.apiService.postTwoFactorEmail(request);
await this.emailPromise;
if (doToast) {

View File

@@ -0,0 +1,18 @@
<ng-container *ngIf="!usesKeyConnector">
<label for="masterPassword">{{'masterPass' | i18n}}</label>
<input id="masterPassword" type="password" name="MasterPasswordHash" class="form-control"
[formControl]="secret" required appAutofocus appInputVerbatim>
</ng-container>
<ng-container *ngIf="usesKeyConnector">
<div class="form-group">
<button type="button" class="btn btn-primary" (click)="requestOTP()" [disabled]="disableRequestOTP">
{{'requestVerificationCode' | i18n}}
</button>
</div>
<div class="form-group">
<label for="verificationCode">{{'verificationCode' | i18n}}</label>
<input id="verificationCode" type="input" name="verificationCode" class="form-control"
[formControl]="secret" required appAutofocus appInputVerbatim>
</div>
</ng-container>

View File

@@ -0,0 +1,81 @@
import {
Component,
OnInit,
} from '@angular/core';
import {
ControlValueAccessor,
FormControl,
NG_VALUE_ACCESSOR,
} from '@angular/forms';
import { ApiService } from 'jslib-common/abstractions/api.service';
import { KeyConnectorService } from 'jslib-common/abstractions/keyConnector.service';
import { VerificationType } from 'jslib-common/enums/verificationType';
import { Verification } from 'jslib-common/types/verification';
@Component({
selector: 'app-verify-master-password',
templateUrl: 'verify-master-password.component.html',
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: VerifyMasterPasswordComponent,
},
],
})
export class VerifyMasterPasswordComponent implements ControlValueAccessor, OnInit {
usesKeyConnector: boolean = false;
disableRequestOTP: boolean = false;
secret = new FormControl('');
private onChange: (value: Verification) => void;
constructor(private keyConnectorService: KeyConnectorService, private apiService: ApiService) { }
async ngOnInit() {
this.usesKeyConnector = await this.keyConnectorService.getUsesKeyConnector();
this.secret.valueChanges.subscribe(secret => {
if (this.onChange == null) {
return;
}
this.onChange({
type: this.usesKeyConnector ? VerificationType.OTP : VerificationType.MasterPassword,
secret: secret,
});
});
}
async requestOTP() {
if (this.usesKeyConnector) {
this.disableRequestOTP = true;
await this.apiService.postAccountRequestOTP();
}
}
writeValue(obj: any): void {
this.secret.setValue(obj);
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
// Not implemented
}
setDisabledState?(isDisabled: boolean): void {
this.disableRequestOTP = isDisabled;
if (isDisabled) {
this.secret.disable();
} else {
this.secret.enable();
}
}
}