implement 2fa setup for u2f
This commit is contained in:
parent
890bf49294
commit
bf5d8cab1c
2
jslib
2
jslib
|
@ -1 +1 @@
|
|||
Subproject commit ec505b8c5508e4c8f6191c946b042a804c73c501
|
||||
Subproject commit ccd10751e3b4228f4117fd1ca2f87f8603eac130
|
|
@ -46,6 +46,7 @@ import { TwoFactorAuthenticatorComponent } from './settings/two-factor-authentic
|
|||
import { TwoFactorDuoComponent } from './settings/two-factor-duo.component';
|
||||
import { TwoFactorEmailComponent } from './settings/two-factor-email.component';
|
||||
import { TwoFactorSetupComponent } from './settings/two-factor-setup.component';
|
||||
import { TwoFactorU2fComponent } from './settings/two-factor-u2f.component';
|
||||
import { TwoFactorYubiKeyComponent } from './settings/two-factor-yubikey.component';
|
||||
|
||||
import { ExportComponent } from './tools/export.component';
|
||||
|
@ -152,6 +153,7 @@ import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe';
|
|||
TwoFactorDuoComponent,
|
||||
TwoFactorEmailComponent,
|
||||
TwoFactorOptionsComponent,
|
||||
TwoFactorU2fComponent,
|
||||
TwoFactorYubiKeyComponent,
|
||||
TwoFactorSetupComponent,
|
||||
UserLayoutComponent,
|
||||
|
@ -175,6 +177,7 @@ import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe';
|
|||
TwoFactorDuoComponent,
|
||||
TwoFactorEmailComponent,
|
||||
TwoFactorOptionsComponent,
|
||||
TwoFactorU2fComponent,
|
||||
TwoFactorYubiKeyComponent,
|
||||
],
|
||||
providers: [],
|
||||
|
|
|
@ -74,7 +74,7 @@
|
|||
<div class="modal-footer">
|
||||
<button appBlurClick type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="fa fa-spinner fa-spin"></i>
|
||||
<span *ngIf="!enabled">{{'save' | i18n}}</span>
|
||||
<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>
|
||||
|
|
|
@ -59,7 +59,7 @@
|
|||
<div class="modal-footer">
|
||||
<button appBlurClick type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="fa fa-spinner fa-spin"></i>
|
||||
<span *ngIf="!enabled">{{'save' | i18n}}</span>
|
||||
<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>
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
<div class="modal-footer">
|
||||
<button appBlurClick type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="fa fa-spinner fa-spin"></i>
|
||||
<span *ngIf="!enabled">{{'save' | i18n}}</span>
|
||||
<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>
|
||||
|
|
|
@ -19,6 +19,7 @@ import { ModalComponent } from '../modal.component';
|
|||
import { TwoFactorAuthenticatorComponent } from './two-factor-authenticator.component';
|
||||
import { TwoFactorDuoComponent } from './two-factor-duo.component';
|
||||
import { TwoFactorEmailComponent } from './two-factor-email.component';
|
||||
import { TwoFactorU2fComponent } from './two-factor-u2f.component';
|
||||
import { TwoFactorYubiKeyComponent } from './two-factor-yubikey.component';
|
||||
|
||||
@Component({
|
||||
|
@ -108,6 +109,12 @@ export class TwoFactorSetupComponent implements OnInit {
|
|||
this.updateStatus(enabled, TwoFactorProviderType.Email);
|
||||
});
|
||||
break;
|
||||
case TwoFactorProviderType.U2f:
|
||||
const u2fComp = this.openModal(this.u2fModalRef, TwoFactorU2fComponent);
|
||||
u2fComp.onUpdated.subscribe((enabled: boolean) => {
|
||||
this.updateStatus(enabled, TwoFactorProviderType.U2f);
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
<div class="modal fade">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">
|
||||
{{'twoStepLogin' | i18n}}
|
||||
<small>FIDO U2F</small>
|
||||
</h2>
|
||||
<button type="button" class="close" data-dismiss="modal" attr.aria-label="{{'close' | i18n}}">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<form #authForm (ngSubmit)="auth()" [appApiAction]="authPromise" ngNativeValidate *ngIf="!authed">
|
||||
<div class="modal-body">
|
||||
<p>{{'twoStepLoginAuthDesc' | i18n}}</p>
|
||||
<label for="masterPassword">{{'masterPass' | i18n}}</label>
|
||||
<input id="masterPassword" type="password" name="MasterPasswordHash" class="form-control" [(ngModel)]="masterPassword" required
|
||||
appAutoFocus>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button appBlurClick type="submit" class="btn btn-primary btn-submit" [disabled]="authForm.loading">
|
||||
<i class="fa fa-spinner fa-spin"></i>
|
||||
<span>{{'continue' | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'close' | i18n}}</button>
|
||||
</div>
|
||||
</form>
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="authed">
|
||||
<div class="modal-body">
|
||||
<app-callout type="success" title="{{'enabled' | i18n}}" icon="fa-check-circle" *ngIf="enabled">
|
||||
{{'twoStepLoginProviderEnabled' | i18n}}
|
||||
</app-callout>
|
||||
<app-callout type="warning">
|
||||
<p>{{'twoFactorU2fWarning' | i18n}}</p>
|
||||
<ul class="mb-0">
|
||||
<li>{{'twoFactorU2fSupportWeb' | i18n}}</li>
|
||||
</ul>
|
||||
</app-callout>
|
||||
<ng-container *ngIf="!enabled">
|
||||
<p>{{'twoFactorU2fAdd' | i18n}}:</p>
|
||||
<ol>
|
||||
<li>{{'twoFactorU2fPlugIn' | i18n}}</li>
|
||||
<li>{{'twoFactorU2fTouchButton' | i18n}}</li>
|
||||
</ol>
|
||||
<hr>
|
||||
<div class="text-center">
|
||||
<ng-container *ngIf="u2fListening">
|
||||
<p>
|
||||
<i class="fa fa-spinner fa-spin fa-2x text-muted"></i>
|
||||
</p>
|
||||
{{'twoFactorU2fWaiting' | i18n}}...
|
||||
</ng-container>
|
||||
<ng-container *ngIf="u2fResponse">
|
||||
<p>
|
||||
<i class="fa fa-check-circle fa-2x text-success"></i>
|
||||
</p>
|
||||
{{'twoFactorU2fClickEnable' | i18n}}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="u2fError">
|
||||
<p>
|
||||
<i class="fa fa-warning fa-2x text-danger"></i>
|
||||
</p>
|
||||
{{'twoFactorU2fProblemReading' | i18n}}
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button appBlurClick type="submit" class="btn btn-primary" [disabled]="form.loading || (!enabled && !u2fResponse)">
|
||||
<i class="fa fa-spinner fa-spin" *ngIf="form.loading"></i>
|
||||
<ng-container *ngIf="!form.loading">
|
||||
<span *ngIf="!enabled">{{'enable' | i18n}}</span>
|
||||
<span *ngIf="enabled">{{'disable' | i18n}}</span>
|
||||
</ng-container>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'close' | i18n}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,155 @@
|
|||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
} from '@angular/core';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
|
||||
import { PasswordVerificationRequest } from 'jslib/models/request/passwordVerificationRequest';
|
||||
import { TwoFactorProviderRequest } from 'jslib/models/request/twoFactorProviderRequest';
|
||||
|
||||
import { TwoFactorProviderType } from 'jslib/enums/twoFactorProviderType';
|
||||
import { UpdateTwoFactorU2fRequest } from 'jslib/models/request/updateTwoFactorU2fRequest';
|
||||
import { TwoFactorU2fResponse } from 'jslib/models/response/twoFactorU2fResponse';
|
||||
|
||||
@Component({
|
||||
selector: 'app-two-factor-u2f',
|
||||
templateUrl: 'two-factor-u2f.component.html',
|
||||
})
|
||||
export class TwoFactorU2fComponent implements OnInit, OnDestroy {
|
||||
@Output() onUpdated = new EventEmitter<boolean>();
|
||||
|
||||
enabled = false;
|
||||
authed = false;
|
||||
u2fChallenge: any;
|
||||
u2fError: boolean;
|
||||
u2fListening: boolean;
|
||||
u2fResponse: string;
|
||||
masterPassword: string;
|
||||
u2fScript: HTMLScriptElement;
|
||||
|
||||
authPromise: Promise<any>;
|
||||
formPromise: Promise<any>;
|
||||
|
||||
private masterPasswordHash: string;
|
||||
|
||||
constructor(private apiService: ApiService, private i18nService: I18nService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
private cryptoService: CryptoService, private platformUtilsService: PlatformUtilsService) {
|
||||
this.u2fScript = window.document.createElement('script');
|
||||
this.u2fScript.src = 'scripts/u2f.js';
|
||||
this.u2fScript.async = true;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
window.document.body.appendChild(this.u2fScript);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
window.document.body.removeChild(this.u2fScript);
|
||||
}
|
||||
|
||||
async auth() {
|
||||
if (this.masterPassword == null || this.masterPassword === '') {
|
||||
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('masterPassRequired'));
|
||||
return;
|
||||
}
|
||||
|
||||
const request = new PasswordVerificationRequest();
|
||||
request.masterPasswordHash = this.masterPasswordHash =
|
||||
await this.cryptoService.hashPassword(this.masterPassword, null);
|
||||
try {
|
||||
this.authPromise = this.apiService.getTwoFactorU2f(request);
|
||||
const response = await this.authPromise;
|
||||
this.authed = true;
|
||||
await this.processResponse(response);
|
||||
this.readDevice();
|
||||
} catch { }
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (this.enabled) {
|
||||
this.disable();
|
||||
} else {
|
||||
this.enable();
|
||||
}
|
||||
}
|
||||
|
||||
private readDevice() {
|
||||
if (this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// tslint:disable-next-line
|
||||
console.log('listening for key...');
|
||||
|
||||
this.u2fResponse = null;
|
||||
this.u2fError = false;
|
||||
this.u2fListening = true;
|
||||
|
||||
(window as any).u2f.register(this.u2fChallenge.AppId, [{
|
||||
version: this.u2fChallenge.Version,
|
||||
challenge: this.u2fChallenge.Challenge,
|
||||
}], [], (data: any) => {
|
||||
this.u2fListening = false;
|
||||
if (data.errorCode === 5) {
|
||||
this.readDevice();
|
||||
return;
|
||||
} else if (data.errorCode) {
|
||||
this.u2fError = true;
|
||||
// tslint:disable-next-line
|
||||
console.log('error: ' + data.errorCode);
|
||||
return;
|
||||
}
|
||||
this.u2fResponse = JSON.stringify(data);
|
||||
}, 10);
|
||||
}
|
||||
|
||||
private async enable() {
|
||||
const request = new UpdateTwoFactorU2fRequest();
|
||||
request.masterPasswordHash = this.masterPasswordHash;
|
||||
request.deviceResponse = this.u2fResponse;
|
||||
try {
|
||||
this.formPromise = this.apiService.putTwoFactorU2f(request);
|
||||
const response = await this.formPromise;
|
||||
await this.processResponse(response);
|
||||
this.analytics.eventTrack.next({ action: 'Enabled Two-step U2F' });
|
||||
this.onUpdated.emit(true);
|
||||
} catch { }
|
||||
}
|
||||
|
||||
private async disable() {
|
||||
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 = new TwoFactorProviderRequest();
|
||||
request.masterPasswordHash = this.masterPasswordHash;
|
||||
request.type = TwoFactorProviderType.U2f;
|
||||
this.formPromise = this.apiService.putTwoFactorDisable(request);
|
||||
await this.formPromise;
|
||||
this.enabled = false;
|
||||
this.analytics.eventTrack.next({ action: 'Disabled Two-step U2F' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('twoStepDisabled'));
|
||||
this.onUpdated.emit(false);
|
||||
} catch { }
|
||||
}
|
||||
|
||||
private async processResponse(response: TwoFactorU2fResponse) {
|
||||
this.u2fChallenge = response.challenge;
|
||||
this.enabled = response.enabled;
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
<div class="modal-header">
|
||||
<h2 class="modal-title">
|
||||
{{'twoStepLogin' | i18n}}
|
||||
<small>{{'yubiKeyTitle' | i18n}}</small>
|
||||
<small>YubiKey</small>
|
||||
</h2>
|
||||
<button type="button" class="close" data-dismiss="modal" attr.aria-label="{{'close' | i18n}}">
|
||||
<span aria-hidden="true">×</span>
|
||||
|
|
|
@ -970,6 +970,9 @@
|
|||
"message": "Providers",
|
||||
"description": "Two-step login providers such as YubiKey, Duo, Authenticator apps, Email, etc."
|
||||
},
|
||||
"enable": {
|
||||
"message": "Enable"
|
||||
},
|
||||
"enabled": {
|
||||
"message": "Enabled"
|
||||
},
|
||||
|
@ -1098,5 +1101,29 @@
|
|||
},
|
||||
"sendEmail": {
|
||||
"message": "Send Email"
|
||||
},
|
||||
"twoFactorU2fAdd": {
|
||||
"message": "Add a FIDO U2F security key to your account"
|
||||
},
|
||||
"twoFactorU2fPlugIn": {
|
||||
"message": "Plug the security key into your computer's USB port."
|
||||
},
|
||||
"twoFactorU2fTouchButton": {
|
||||
"message": "If the security key has a button, touch it."
|
||||
},
|
||||
"twoFactorU2fWarning": {
|
||||
"message": "Due to platform limitations, FIDO U2F cannot be used on all Bitwarden applications. You should enable another two-step login provider so that you can access your account when FIDO U2F cannot be used. Supported platforms:"
|
||||
},
|
||||
"twoFactorU2fSupportWeb": {
|
||||
"message": "Web vault and browser extensions on a desktop/laptop with a U2F enabled browser (Chrome, Opera, Vivaldi, or Firefox with FIDO U2F enabled)."
|
||||
},
|
||||
"twoFactorU2fWaiting": {
|
||||
"message": "Waiting for you to touch the button on your security key"
|
||||
},
|
||||
"twoFactorU2fClickEnable": {
|
||||
"message": "Click the \"Enable\" button below to enable this security key for two-step login."
|
||||
},
|
||||
"twoFactorU2fProblemReading": {
|
||||
"message": "There was a problem reading the security key."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -98,6 +98,7 @@ const plugins = [
|
|||
{ from: './src/app-id.json' },
|
||||
{ from: './src/images', to: 'images' },
|
||||
{ from: './src/locales', to: 'locales' },
|
||||
{ from: './src/scripts', to: 'scripts' },
|
||||
]),
|
||||
extractCss,
|
||||
new webpack.DefinePlugin({
|
||||
|
|
Loading…
Reference in New Issue