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:
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
77
angular/src/components/remove-password.component.ts
Normal file
77
angular/src/components/remove-password.component.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
18
angular/src/components/verify-master-password.component.html
Normal file
18
angular/src/components/verify-master-password.component.html
Normal 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>
|
||||
81
angular/src/components/verify-master-password.component.ts
Normal file
81
angular/src/components/verify-master-password.component.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user