mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-27 12:36:14 +01:00
yubikey 2fa config
This commit is contained in:
parent
246d605e5c
commit
4b4bedaef3
@ -44,6 +44,7 @@ import { PurgeVaultComponent } from './settings/purge-vault.component';
|
|||||||
import { SettingsComponent } from './settings/settings.component';
|
import { SettingsComponent } from './settings/settings.component';
|
||||||
import { TwoFactorAuthenticatorComponent } from './settings/two-factor-authenticator.component';
|
import { TwoFactorAuthenticatorComponent } from './settings/two-factor-authenticator.component';
|
||||||
import { TwoFactorSetupComponent } from './settings/two-factor-setup.component';
|
import { TwoFactorSetupComponent } from './settings/two-factor-setup.component';
|
||||||
|
import { TwoFactorYubiKeyComponent } from './settings/two-factor-yubikey.component';
|
||||||
|
|
||||||
import { ExportComponent } from './tools/export.component';
|
import { ExportComponent } from './tools/export.component';
|
||||||
import { ImportComponent } from './tools/import.component';
|
import { ImportComponent } from './tools/import.component';
|
||||||
@ -147,6 +148,7 @@ import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe';
|
|||||||
TwoFactorAuthenticatorComponent,
|
TwoFactorAuthenticatorComponent,
|
||||||
TwoFactorComponent,
|
TwoFactorComponent,
|
||||||
TwoFactorOptionsComponent,
|
TwoFactorOptionsComponent,
|
||||||
|
TwoFactorYubiKeyComponent,
|
||||||
TwoFactorSetupComponent,
|
TwoFactorSetupComponent,
|
||||||
UserLayoutComponent,
|
UserLayoutComponent,
|
||||||
VaultComponent,
|
VaultComponent,
|
||||||
@ -167,6 +169,7 @@ import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe';
|
|||||||
ShareComponent,
|
ShareComponent,
|
||||||
TwoFactorAuthenticatorComponent,
|
TwoFactorAuthenticatorComponent,
|
||||||
TwoFactorOptionsComponent,
|
TwoFactorOptionsComponent,
|
||||||
|
TwoFactorYubiKeyComponent,
|
||||||
],
|
],
|
||||||
providers: [],
|
providers: [],
|
||||||
bootstrap: [AppComponent],
|
bootstrap: [AppComponent],
|
||||||
|
@ -28,6 +28,7 @@
|
|||||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="authed">
|
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="authed">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<ng-container *ngIf="!enabled">
|
<ng-container *ngIf="!enabled">
|
||||||
|
<img src="../../images/two-factor/0.png" class="float-right" alt="">
|
||||||
<p>{{'twoStepAuthenticatorDesc' | i18n}}</p>
|
<p>{{'twoStepAuthenticatorDesc' | i18n}}</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>1. {{'twoStepAuthenticatorDownloadApp' | i18n}}</strong>
|
<strong>1. {{'twoStepAuthenticatorDownloadApp' | i18n}}</strong>
|
||||||
@ -38,6 +39,7 @@
|
|||||||
<p>{{'twoStepLoginProviderEnabled' | i18n}}</p>
|
<p>{{'twoStepLoginProviderEnabled' | i18n}}</p>
|
||||||
{{'twoStepAuthenticatorReaddDesc' | i18n}}
|
{{'twoStepAuthenticatorReaddDesc' | i18n}}
|
||||||
</app-callout>
|
</app-callout>
|
||||||
|
<img src="../../images/two-factor/0.png" class="float-right" alt="">
|
||||||
<p>{{'twoStepAuthenticatorNeedApp' | i18n}}</p>
|
<p>{{'twoStepAuthenticatorNeedApp' | i18n}}</p>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ul class="fa-ul">
|
<ul class="fa-ul">
|
||||||
@ -66,7 +68,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<ng-container *ngIf="!enabled">
|
<ng-container *ngIf="!enabled">
|
||||||
<label for="token">3. {{'twoStepAuthenticatorEnterCode' | i18n}}</label>
|
<label for="token">3. {{'twoStepAuthenticatorEnterCode' | i18n}}</label>
|
||||||
<input id="token" type="text" name="Token" class="form-control" [(ngModel)]="token" required>
|
<input id="token" type="text" name="Token" class="form-control" [(ngModel)]="token" required appInputVerbatim>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
|
@ -17,6 +17,7 @@ import { TwoFactorProviderType } from 'jslib/enums/twoFactorProviderType';
|
|||||||
import { ModalComponent } from '../modal.component';
|
import { ModalComponent } from '../modal.component';
|
||||||
|
|
||||||
import { TwoFactorAuthenticatorComponent } from './two-factor-authenticator.component';
|
import { TwoFactorAuthenticatorComponent } from './two-factor-authenticator.component';
|
||||||
|
import { TwoFactorYubiKeyComponent } from './two-factor-yubikey.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-two-factor-setup',
|
selector: 'app-two-factor-setup',
|
||||||
@ -81,11 +82,17 @@ export class TwoFactorSetupComponent implements OnInit {
|
|||||||
manage(type: TwoFactorProviderType) {
|
manage(type: TwoFactorProviderType) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case TwoFactorProviderType.Authenticator:
|
case TwoFactorProviderType.Authenticator:
|
||||||
const component = this.openModal(this.authenticatorModalRef, TwoFactorAuthenticatorComponent);
|
const authComp = this.openModal(this.authenticatorModalRef, TwoFactorAuthenticatorComponent);
|
||||||
component.onUpdated.subscribe((enabled: boolean) => {
|
authComp.onUpdated.subscribe((enabled: boolean) => {
|
||||||
this.updateStatus(enabled, TwoFactorProviderType.Authenticator)
|
this.updateStatus(enabled, TwoFactorProviderType.Authenticator)
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case TwoFactorProviderType.Yubikey:
|
||||||
|
const yubiComp = this.openModal(this.yubikeyModalRef, TwoFactorYubiKeyComponent);
|
||||||
|
yubiComp.onUpdated.subscribe((enabled: boolean) => {
|
||||||
|
this.updateStatus(enabled, TwoFactorProviderType.Yubikey)
|
||||||
|
});
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
83
src/app/settings/two-factor-yubikey.component.html
Normal file
83
src/app/settings/two-factor-yubikey.component.html
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<div class="modal fade">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 class="modal-title">
|
||||||
|
{{'twoStepLogin' | i18n}}
|
||||||
|
<small>{{'yubiKeyTitle' | i18n}}</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>{{'twoFactorYubikeyWarning' | i18n}}</p>
|
||||||
|
<ul class="mb-0">
|
||||||
|
<li>{{'twoFactorYubikeySupportUsb' | i18n}}</li>
|
||||||
|
<li>{{'twoFactorYubikeySupportMobile' | i18n}}</li>
|
||||||
|
</ul>
|
||||||
|
</app-callout>
|
||||||
|
<img src="../../images/two-factor/3.png" class="float-right" alt="">
|
||||||
|
<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="text" name="Key{{i + 1}}" class="form-control" [(ngModel)]="k.key" *ngIf="!k.existingKey" appInputVerbatim>
|
||||||
|
<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" appBlurClick (click)="remove(k)" title="{{'remove' | i18n}}">
|
||||||
|
<i class="fa fa-minus-circle fa-lg"></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="exampleCheck1">
|
||||||
|
<label class="form-check-label" for="exampleCheck1">{{'twoFactorYubikeySupportsNfc' | i18n}}</label>
|
||||||
|
</div>
|
||||||
|
<small class="form-text text-muted">{{'twoFactorYubikeySupportsNfcDesc' | i18n}}</small>
|
||||||
|
</div>
|
||||||
|
<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>{{'save' | i18n}}</span>
|
||||||
|
</button>
|
||||||
|
<button #disableBtn appBlurClick type="button" class="btn btn-outline-secondary btn-submit" [appApiAction]="disablePromise"
|
||||||
|
[disabled]="disableBtn.loading" (click)="disable()">
|
||||||
|
<i class="fa fa-spinner fa-spin"></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>
|
130
src/app/settings/two-factor-yubikey.component.ts
Normal file
130
src/app/settings/two-factor-yubikey.component.ts
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import {
|
||||||
|
Component,
|
||||||
|
EventEmitter,
|
||||||
|
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 { UpdateTwoFactorYubioOtpRequest } from 'jslib/models/request/updateTwoFactorYubioOtpRequest';
|
||||||
|
import { TwoFactorYubiKeyResponse } from 'jslib/models/response/twoFactorYubiKeyResponse';
|
||||||
|
|
||||||
|
import { TwoFactorProviderType } from 'jslib/enums/twoFactorProviderType';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-two-factor-yubikey',
|
||||||
|
templateUrl: 'two-factor-yubikey.component.html',
|
||||||
|
})
|
||||||
|
export class TwoFactorYubiKeyComponent {
|
||||||
|
@Output() onUpdated = new EventEmitter<boolean>();
|
||||||
|
|
||||||
|
enabled = false;
|
||||||
|
authed = false;
|
||||||
|
keys: any[];
|
||||||
|
nfc = false;
|
||||||
|
masterPassword: string;
|
||||||
|
|
||||||
|
authPromise: Promise<TwoFactorYubiKeyResponse>;
|
||||||
|
formPromise: Promise<any>;
|
||||||
|
disablePromise: Promise<any>;
|
||||||
|
|
||||||
|
private masterPasswordHash: string;
|
||||||
|
|
||||||
|
constructor(private apiService: ApiService, private i18nService: I18nService,
|
||||||
|
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||||
|
private cryptoService: CryptoService, private platformUtilsService: PlatformUtilsService) { }
|
||||||
|
|
||||||
|
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.getTwoFactorYubiKey(request);
|
||||||
|
const response = await this.authPromise;
|
||||||
|
this.authed = true;
|
||||||
|
this.processResponse(response);
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit() {
|
||||||
|
const request = new UpdateTwoFactorYubioOtpRequest();
|
||||||
|
request.masterPasswordHash = this.masterPasswordHash;
|
||||||
|
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;
|
||||||
|
try {
|
||||||
|
this.formPromise = this.apiService.putTwoFactorYubiKey(request);
|
||||||
|
const response = await this.formPromise;
|
||||||
|
await this.processResponse(response);
|
||||||
|
this.analytics.eventTrack.next({ action: 'Enabled Two-step YubiKey' });
|
||||||
|
this.processResponse(response);
|
||||||
|
this.toasterService.popAsync('success', null, this.i18nService.t('yubikeysUpdated'));
|
||||||
|
this.onUpdated.emit(true);
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
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.Yubikey;
|
||||||
|
this.disablePromise = this.apiService.putTwoFactorDisable(request);
|
||||||
|
await this.disablePromise;
|
||||||
|
this.enabled = false;
|
||||||
|
this.analytics.eventTrack.next({ action: 'Disabled Two-step YubiKey' });
|
||||||
|
this.toasterService.popAsync('success', null, this.i18nService.t('twoStepDisabled'));
|
||||||
|
this.onUpdated.emit(false);
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(key: any) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -1026,5 +1026,53 @@
|
|||||||
},
|
},
|
||||||
"twoStepDisabled": {
|
"twoStepDisabled": {
|
||||||
"message": "Two-step login provider disabled."
|
"message": "Two-step login provider disabled."
|
||||||
|
},
|
||||||
|
"twoFactorYubikeyAdd": {
|
||||||
|
"message": "Add a new YubiKey to your account"
|
||||||
|
},
|
||||||
|
"twoFactorYubikeyPlugIn": {
|
||||||
|
"message": "Plug the YubiKey (NEO or 4 series) into your computer's USB port."
|
||||||
|
},
|
||||||
|
"twoFactorYubikeySelectKey": {
|
||||||
|
"message": "Select in the first empty YubiKey input field below."
|
||||||
|
},
|
||||||
|
"twoFactorYubikeyTouchButton": {
|
||||||
|
"message": "Touch the YubiKey's button."
|
||||||
|
},
|
||||||
|
"twoFactorYubikeySaveForm": {
|
||||||
|
"message": "Save the form."
|
||||||
|
},
|
||||||
|
"twoFactorYubikeyWarning": {
|
||||||
|
"message": "Due to platform limitations, YubiKeys cannot be used on all Bitwarden applications. You should enable another two-step login provider so that you can access your account when YubiKeys cannot be used. Supported platforms:"
|
||||||
|
},
|
||||||
|
"twoFactorYubikeySupportUsb": {
|
||||||
|
"message": "Web vault, desktop application, CLI, and all browser extensions on a device with a USB port that can accept your YubiKey."
|
||||||
|
},
|
||||||
|
"twoFactorYubikeySupportMobile": {
|
||||||
|
"message": "Mobile apps on a device with NFC capabilities or a USB port that can accept your YubiKey."
|
||||||
|
},
|
||||||
|
"yubikeyX": {
|
||||||
|
"message": "YubiKey $INDEX$",
|
||||||
|
"placeholders": {
|
||||||
|
"index": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nfcSupport": {
|
||||||
|
"message": "NFC Support"
|
||||||
|
},
|
||||||
|
"twoFactorYubikeySupportsNfc": {
|
||||||
|
"message": "One of my keys supports NFC."
|
||||||
|
},
|
||||||
|
"twoFactorYubikeySupportsNfcDesc": {
|
||||||
|
"message": "If one of your YubiKeys supports NFC (such as a YubiKey NEO), you will be prompted on mobile devices whenever NFC availability is detected."
|
||||||
|
},
|
||||||
|
"yubikeysUpdated": {
|
||||||
|
"message": "YubiKeys updated"
|
||||||
|
},
|
||||||
|
"disableAllKeys": {
|
||||||
|
"message": "Disable All Keys"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,8 @@ $h4-font-size: 1rem;
|
|||||||
$h5-font-size: 1rem;
|
$h5-font-size: 1rem;
|
||||||
$h6-font-size: 1rem;
|
$h6-font-size: 1rem;
|
||||||
|
|
||||||
|
$code-font-size: 100%;
|
||||||
|
|
||||||
$navbar-padding-y: .75rem;
|
$navbar-padding-y: .75rem;
|
||||||
$grid-gutter-width: 20px;
|
$grid-gutter-width: 20px;
|
||||||
$card-spacer-y: .6rem;
|
$card-spacer-y: .6rem;
|
||||||
|
Loading…
Reference in New Issue
Block a user