mirror of
https://github.com/bitwarden/browser.git
synced 2025-03-27 16:10:08 +01:00
WebAuthn (#633)
This commit is contained in:
parent
35ecbcc11a
commit
1ea8762eeb
2
jslib
2
jslib
@ -1 +1 @@
|
|||||||
Subproject commit f80e89465ffc004705d2941301c0ffb6bfd71d1a
|
Subproject commit f20af0cd7c90adc07783950bed197b5d47892d6f
|
@ -33,16 +33,10 @@
|
|||||||
required appAutofocus appInputVerbatim autocomplete="new-password">
|
required appAutofocus appInputVerbatim autocomplete="new-password">
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="selectedProviderType === providerType.U2f">
|
<ng-container *ngIf="selectedProviderType === providerType.WebAuthn">
|
||||||
<p class="text-center" *ngIf="!u2fReady">
|
<div id="web-authn-frame" class="mb-3">
|
||||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"
|
<iframe id="webauthn_iframe"></iframe>
|
||||||
aria-hidden="true"></i>
|
</div>
|
||||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
|
||||||
</p>
|
|
||||||
<ng-container *ngIf="u2fReady">
|
|
||||||
<p class="text-center">{{'insertU2f' | i18n}}</p>
|
|
||||||
<img src="../../images/u2fkey.jpg" alt="" class="rounded img-fluid mb-3">
|
|
||||||
</ng-container>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="selectedProviderType === providerType.Duo ||
|
<ng-container *ngIf="selectedProviderType === providerType.Duo ||
|
||||||
selectedProviderType === providerType.OrganizationDuo">
|
selectedProviderType === providerType.OrganizationDuo">
|
||||||
@ -51,7 +45,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<i class="fa fa-spinner text-muted fa-spin pull-right" title="{{'loading' | i18n}}"
|
<i class="fa fa-spinner text-muted fa-spin pull-right" title="{{'loading' | i18n}}"
|
||||||
*ngIf="form.loading && selectedProviderType === providerType.U2f" aria-hidden="true"></i>
|
*ngIf="form.loading && selectedProviderType === providerType.WebAuthn" aria-hidden="true"></i>
|
||||||
<div class="form-check" *ngIf="selectedProviderType != null">
|
<div class="form-check" *ngIf="selectedProviderType != null">
|
||||||
<input id="remember" type="checkbox" name="Remember" class="form-check-input"
|
<input id="remember" type="checkbox" name="Remember" class="form-check-input"
|
||||||
[(ngModel)]="remember">
|
[(ngModel)]="remember">
|
||||||
@ -65,7 +59,7 @@
|
|||||||
<div class="d-flex mb-3">
|
<div class="d-flex mb-3">
|
||||||
<button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading"
|
<button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading"
|
||||||
*ngIf="selectedProviderType != null && selectedProviderType !== providerType.Duo &&
|
*ngIf="selectedProviderType != null && selectedProviderType !== providerType.Duo &&
|
||||||
selectedProviderType !== providerType.OrganizationDuo && selectedProviderType !== providerType.U2f">
|
selectedProviderType !== providerType.OrganizationDuo && selectedProviderType !== providerType.WebAuthn">
|
||||||
<span>
|
<span>
|
||||||
<i class="fa fa-sign-in" aria-hidden="true"></i> {{'continue' | i18n}}
|
<i class="fa fa-sign-in" aria-hidden="true"></i> {{'continue' | i18n}}
|
||||||
</span>
|
</span>
|
||||||
@ -84,4 +78,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<ng-template #twoFactorOptions></ng-template>
|
<ng-template #twoFactorOptions></ng-template>
|
||||||
<iframe id="u2f_iframe" hidden></iframe>
|
|
||||||
|
@ -135,8 +135,8 @@ import { TwoFactorDuoComponent } from './settings/two-factor-duo.component';
|
|||||||
import { TwoFactorEmailComponent } from './settings/two-factor-email.component';
|
import { TwoFactorEmailComponent } from './settings/two-factor-email.component';
|
||||||
import { TwoFactorRecoveryComponent } from './settings/two-factor-recovery.component';
|
import { TwoFactorRecoveryComponent } from './settings/two-factor-recovery.component';
|
||||||
import { TwoFactorSetupComponent } from './settings/two-factor-setup.component';
|
import { TwoFactorSetupComponent } from './settings/two-factor-setup.component';
|
||||||
import { TwoFactorU2fComponent } from './settings/two-factor-u2f.component';
|
|
||||||
import { TwoFactorVerifyComponent } from './settings/two-factor-verify.component';
|
import { TwoFactorVerifyComponent } from './settings/two-factor-verify.component';
|
||||||
|
import { TwoFactorWebAuthnComponent } from './settings/two-factor-webauthn.component';
|
||||||
import { TwoFactorYubiKeyComponent } from './settings/two-factor-yubikey.component';
|
import { TwoFactorYubiKeyComponent } from './settings/two-factor-yubikey.component';
|
||||||
import { UpdateKeyComponent } from './settings/update-key.component';
|
import { UpdateKeyComponent } from './settings/update-key.component';
|
||||||
import { UpdateLicenseComponent } from './settings/update-license.component';
|
import { UpdateLicenseComponent } from './settings/update-license.component';
|
||||||
@ -399,8 +399,8 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
|||||||
TwoFactorOptionsComponent,
|
TwoFactorOptionsComponent,
|
||||||
TwoFactorRecoveryComponent,
|
TwoFactorRecoveryComponent,
|
||||||
TwoFactorSetupComponent,
|
TwoFactorSetupComponent,
|
||||||
TwoFactorU2fComponent,
|
|
||||||
TwoFactorVerifyComponent,
|
TwoFactorVerifyComponent,
|
||||||
|
TwoFactorWebAuthnComponent,
|
||||||
TwoFactorYubiKeyComponent,
|
TwoFactorYubiKeyComponent,
|
||||||
UnsecuredWebsitesReportComponent,
|
UnsecuredWebsitesReportComponent,
|
||||||
UpdateKeyComponent,
|
UpdateKeyComponent,
|
||||||
@ -454,7 +454,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
|||||||
TwoFactorEmailComponent,
|
TwoFactorEmailComponent,
|
||||||
TwoFactorOptionsComponent,
|
TwoFactorOptionsComponent,
|
||||||
TwoFactorRecoveryComponent,
|
TwoFactorRecoveryComponent,
|
||||||
TwoFactorU2fComponent,
|
TwoFactorWebAuthnComponent,
|
||||||
TwoFactorYubiKeyComponent,
|
TwoFactorYubiKeyComponent,
|
||||||
UpdateKeyComponent,
|
UpdateKeyComponent,
|
||||||
],
|
],
|
||||||
|
@ -51,4 +51,4 @@
|
|||||||
<ng-template #duoTemplate></ng-template>
|
<ng-template #duoTemplate></ng-template>
|
||||||
<ng-template #emailTemplate></ng-template>
|
<ng-template #emailTemplate></ng-template>
|
||||||
<ng-template #yubikeyTemplate></ng-template>
|
<ng-template #yubikeyTemplate></ng-template>
|
||||||
<ng-template #u2fTemplate></ng-template>
|
<ng-template #webAuthnTemplate></ng-template>
|
||||||
|
@ -23,7 +23,7 @@ import { TwoFactorAuthenticatorComponent } from './two-factor-authenticator.comp
|
|||||||
import { TwoFactorDuoComponent } from './two-factor-duo.component';
|
import { TwoFactorDuoComponent } from './two-factor-duo.component';
|
||||||
import { TwoFactorEmailComponent } from './two-factor-email.component';
|
import { TwoFactorEmailComponent } from './two-factor-email.component';
|
||||||
import { TwoFactorRecoveryComponent } from './two-factor-recovery.component';
|
import { TwoFactorRecoveryComponent } from './two-factor-recovery.component';
|
||||||
import { TwoFactorU2fComponent } from './two-factor-u2f.component';
|
import { TwoFactorWebAuthnComponent } from './two-factor-webauthn.component';
|
||||||
import { TwoFactorYubiKeyComponent } from './two-factor-yubikey.component';
|
import { TwoFactorYubiKeyComponent } from './two-factor-yubikey.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -34,9 +34,9 @@ export class TwoFactorSetupComponent implements OnInit {
|
|||||||
@ViewChild('recoveryTemplate', { read: ViewContainerRef, static: true }) recoveryModalRef: ViewContainerRef;
|
@ViewChild('recoveryTemplate', { read: ViewContainerRef, static: true }) recoveryModalRef: ViewContainerRef;
|
||||||
@ViewChild('authenticatorTemplate', { read: ViewContainerRef, static: true }) authenticatorModalRef: ViewContainerRef;
|
@ViewChild('authenticatorTemplate', { read: ViewContainerRef, static: true }) authenticatorModalRef: ViewContainerRef;
|
||||||
@ViewChild('yubikeyTemplate', { read: ViewContainerRef, static: true }) yubikeyModalRef: ViewContainerRef;
|
@ViewChild('yubikeyTemplate', { read: ViewContainerRef, static: true }) yubikeyModalRef: ViewContainerRef;
|
||||||
@ViewChild('u2fTemplate', { read: ViewContainerRef, static: true }) u2fModalRef: ViewContainerRef;
|
|
||||||
@ViewChild('duoTemplate', { read: ViewContainerRef, static: true }) duoModalRef: ViewContainerRef;
|
@ViewChild('duoTemplate', { read: ViewContainerRef, static: true }) duoModalRef: ViewContainerRef;
|
||||||
@ViewChild('emailTemplate', { read: ViewContainerRef, static: true }) emailModalRef: ViewContainerRef;
|
@ViewChild('emailTemplate', { read: ViewContainerRef, static: true }) emailModalRef: ViewContainerRef;
|
||||||
|
@ViewChild('webAuthnTemplate', { read: ViewContainerRef, static: true }) webAuthnModalRef: ViewContainerRef;
|
||||||
|
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
providers: any[] = [];
|
providers: any[] = [];
|
||||||
@ -117,10 +117,10 @@ export class TwoFactorSetupComponent implements OnInit {
|
|||||||
this.updateStatus(enabled, TwoFactorProviderType.Email);
|
this.updateStatus(enabled, TwoFactorProviderType.Email);
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case TwoFactorProviderType.U2f:
|
case TwoFactorProviderType.WebAuthn:
|
||||||
const u2fComp = this.openModal(this.u2fModalRef, TwoFactorU2fComponent);
|
const webAuthnComp = this.openModal(this.webAuthnModalRef, TwoFactorWebAuthnComponent);
|
||||||
u2fComp.onUpdated.subscribe((enabled: boolean) => {
|
webAuthnComp.onUpdated.subscribe((enabled: boolean) => {
|
||||||
this.updateStatus(enabled, TwoFactorProviderType.U2f);
|
this.updateStatus(enabled, TwoFactorProviderType.WebAuthn);
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
@ -59,8 +59,8 @@ export class TwoFactorVerifyComponent {
|
|||||||
case TwoFactorProviderType.Email:
|
case TwoFactorProviderType.Email:
|
||||||
this.formPromise = this.apiService.getTwoFactorEmail(request);
|
this.formPromise = this.apiService.getTwoFactorEmail(request);
|
||||||
break;
|
break;
|
||||||
case TwoFactorProviderType.U2f:
|
case TwoFactorProviderType.WebAuthn:
|
||||||
this.formPromise = this.apiService.getTwoFactorU2f(request);
|
this.formPromise = this.apiService.getTwoFactorWebAuthn(request);
|
||||||
break;
|
break;
|
||||||
case TwoFactorProviderType.Authenticator:
|
case TwoFactorProviderType.Authenticator:
|
||||||
this.formPromise = this.apiService.getTwoFactorAuthenticator(request);
|
this.formPromise = this.apiService.getTwoFactorAuthenticator(request);
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 class="modal-title" id="2faU2fTitle">
|
<h2 class="modal-title" id="2faU2fTitle">
|
||||||
{{'twoStepLogin' | i18n}}
|
{{'twoStepLogin' | i18n}}
|
||||||
<small>FIDO U2F</small>
|
<small>{{'webAuthnTitle' | i18n}}</small>
|
||||||
</h2>
|
</h2>
|
||||||
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
|
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
@ -19,27 +19,20 @@
|
|||||||
{{'twoStepLoginProviderEnabled' | i18n}}
|
{{'twoStepLoginProviderEnabled' | i18n}}
|
||||||
</app-callout>
|
</app-callout>
|
||||||
<app-callout type="warning">
|
<app-callout type="warning">
|
||||||
<p>{{'twoFactorU2fWarning' | i18n}}</p>
|
<p>{{'twoFactorWebAuthnWarning' | i18n}}</p>
|
||||||
<ul class="mb-0">
|
<ul class="mb-0">
|
||||||
<li>{{'twoFactorU2fSupportWeb' | i18n}}</li>
|
<li>{{'twoFactorWebAuthnSupportWeb' | i18n}}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</app-callout>
|
</app-callout>
|
||||||
<img src="../../images/two-factor/4.png" class="float-right ml-5" alt="">
|
<img src="../../images/two-factor/7.png" class="float-right ml-5" alt="">
|
||||||
<ul class="fa-ul">
|
<ul class="fa-ul">
|
||||||
<li *ngFor="let k of keys; let i = index" #removeKeyBtn [appApiAction]="k.removePromise">
|
<li *ngFor="let k of keys; let i = index" #removeKeyBtn [appApiAction]="k.removePromise">
|
||||||
<i class="fa-li fa fa-key"></i>
|
<i class="fa-li fa fa-key"></i>
|
||||||
<strong *ngIf="!k.configured || !k.name">{{'u2fkeyX' | i18n : i + 1}}</strong>
|
<strong *ngIf="!k.configured || !k.name">{{'webAuthnkeyX' | i18n : i + 1}}</strong>
|
||||||
<strong *ngIf="k.configured && k.name">{{k.name}}</strong>
|
<strong *ngIf="k.configured && k.name">{{k.name}}</strong>
|
||||||
<ng-container *ngIf="k.configured && !removeKeyBtn.loading">
|
<ng-container *ngIf="k.configured && !removeKeyBtn.loading">
|
||||||
<ng-container *ngIf="k.compromised">
|
<ng-container *ngIf="k.migrated">
|
||||||
<i class="fa fa-fw fa-exclamation-triangle text-warning" aria-hidden="true"
|
<span>{{'webAuthnMigrated' | i18n}}</span>
|
||||||
title="{{'keyCompromised' | i18n}}"></i>
|
|
||||||
<span class="sr-only">{{'keyCompromised' | i18n}}</span>
|
|
||||||
</ng-container>
|
|
||||||
<ng-container *ngIf="!k.compromised">
|
|
||||||
<i class="fa fa-fw fa-check text-success" aria-hidden="true"
|
|
||||||
title="{{'enabled' | i18n}}"></i>
|
|
||||||
<span class="sr-only">{{'enabled' | i18n}}</span>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="keysConfiguredCount > 1 && k.configured">
|
<ng-container *ngIf="keysConfiguredCount > 1 && k.configured">
|
||||||
@ -51,7 +44,7 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<hr>
|
<hr>
|
||||||
<p>{{'twoFactorU2fAdd' | i18n}}:</p>
|
<p>{{'twoFactorWebAuthnAdd' | i18n}}:</p>
|
||||||
<ol>
|
<ol>
|
||||||
<li>{{'twoFactorU2fGiveName' | i18n}}</li>
|
<li>{{'twoFactorU2fGiveName' | i18n}}</li>
|
||||||
<li>{{'twoFactorU2fPlugInReadKey' | i18n}}</li>
|
<li>{{'twoFactorU2fPlugInReadKey' | i18n}}</li>
|
||||||
@ -66,7 +59,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" (click)="readKey()" class="btn btn-outline-secondary mr-2"
|
<button type="button" (click)="readKey()" class="btn btn-outline-secondary mr-2"
|
||||||
[disabled]="readKeyBtn.loading || u2fListening || !keyIdAvailable" #readKeyBtn
|
[disabled]="readKeyBtn.loading || webAuthnListening || !keyIdAvailable" #readKeyBtn
|
||||||
[appApiAction]="challengePromise">
|
[appApiAction]="challengePromise">
|
||||||
{{'readKey' | i18n}}
|
{{'readKey' | i18n}}
|
||||||
</button>
|
</button>
|
||||||
@ -74,22 +67,22 @@
|
|||||||
<i class="fa fa-spinner fa-spin text-muted" aria-hidden="true"></i>
|
<i class="fa fa-spinner fa-spin text-muted" aria-hidden="true"></i>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="!readKeyBtn.loading">
|
<ng-container *ngIf="!readKeyBtn.loading">
|
||||||
<ng-container *ngIf="u2fListening">
|
<ng-container *ngIf="webAuthnListening">
|
||||||
<i class="fa fa-spinner fa-spin text-muted" aria-hidden="true"></i>
|
<i class="fa fa-spinner fa-spin text-muted" aria-hidden="true"></i>
|
||||||
{{'twoFactorU2fWaiting' | i18n}}...
|
{{'twoFactorU2fWaiting' | i18n}}...
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="u2fResponse">
|
<ng-container *ngIf="webAuthnResponse">
|
||||||
<i class="fa fa-check-circle text-success" aria-hidden="true"></i>
|
<i class="fa fa-check-circle text-success" aria-hidden="true"></i>
|
||||||
{{'twoFactorU2fClickSave' | i18n}}
|
{{'twoFactorU2fClickSave' | i18n}}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="u2fError">
|
<ng-container *ngIf="webAuthnError">
|
||||||
<i class="fa fa-warning text-danger" aria-hidden="true"></i>
|
<i class="fa fa-warning text-danger" aria-hidden="true"></i>
|
||||||
{{'twoFactorU2fProblemReadingTryAgain' | i18n}}
|
{{'twoFactorU2fProblemReadingTryAgain' | i18n}}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="submit" class="btn btn-primary" [disabled]="form.loading || !u2fResponse">
|
<button type="submit" class="btn btn-primary" [disabled]="form.loading || !webAuthnResponse">
|
||||||
<i class="fa fa-spinner fa-spin" *ngIf="form.loading" title="{{'loading' | i18n}}"
|
<i class="fa fa-spinner fa-spin" *ngIf="form.loading" title="{{'loading' | i18n}}"
|
||||||
aria-hidden="true"></i>
|
aria-hidden="true"></i>
|
||||||
<span *ngIf="!form.loading">{{'save' | i18n}}</span>
|
<span *ngIf="!form.loading">{{'save' | i18n}}</span>
|
@ -1,8 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
NgZone,
|
NgZone,
|
||||||
OnDestroy,
|
|
||||||
OnInit,
|
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
|
||||||
import { ToasterService } from 'angular2-toaster';
|
import { ToasterService } from 'angular2-toaster';
|
||||||
@ -15,48 +13,35 @@ import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
|||||||
import { TwoFactorProviderType } from 'jslib/enums/twoFactorProviderType';
|
import { TwoFactorProviderType } from 'jslib/enums/twoFactorProviderType';
|
||||||
|
|
||||||
import { PasswordVerificationRequest } from 'jslib/models/request/passwordVerificationRequest';
|
import { PasswordVerificationRequest } from 'jslib/models/request/passwordVerificationRequest';
|
||||||
import { UpdateTwoFactorU2fDeleteRequest } from 'jslib/models/request/updateTwoFactorU2fDeleteRequest';
|
import { UpdateTwoFactorWebAuthnDeleteRequest } from 'jslib/models/request/updateTwoFactorWebAuthnDeleteRequest';
|
||||||
import { UpdateTwoFactorU2fRequest } from 'jslib/models/request/updateTwoFactorU2fRequest';
|
import { UpdateTwoFactorWebAuthnRequest } from 'jslib/models/request/updateTwoFactorWebAuthnRequest';
|
||||||
import {
|
import {
|
||||||
ChallengeResponse,
|
ChallengeResponse,
|
||||||
TwoFactorU2fResponse,
|
TwoFactorWebAuthnResponse,
|
||||||
} from 'jslib/models/response/twoFactorU2fResponse';
|
} from 'jslib/models/response/twoFactorWebAuthnResponse';
|
||||||
|
|
||||||
import { TwoFactorBaseComponent } from './two-factor-base.component';
|
import { TwoFactorBaseComponent } from './two-factor-base.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-two-factor-u2f',
|
selector: 'app-two-factor-webauthn',
|
||||||
templateUrl: 'two-factor-u2f.component.html',
|
templateUrl: 'two-factor-webauthn.component.html',
|
||||||
})
|
})
|
||||||
export class TwoFactorU2fComponent extends TwoFactorBaseComponent implements OnInit, OnDestroy {
|
export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent {
|
||||||
type = TwoFactorProviderType.U2f;
|
type = TwoFactorProviderType.WebAuthn;
|
||||||
name: string;
|
name: string;
|
||||||
keys: any[];
|
keys: any[];
|
||||||
keyIdAvailable: number = null;
|
keyIdAvailable: number = null;
|
||||||
keysConfiguredCount = 0;
|
keysConfiguredCount = 0;
|
||||||
u2fError: boolean;
|
webAuthnError: boolean;
|
||||||
u2fListening: boolean;
|
webAuthnListening: boolean;
|
||||||
u2fResponse: string;
|
webAuthnResponse: PublicKeyCredential;
|
||||||
challengePromise: Promise<ChallengeResponse>;
|
challengePromise: Promise<ChallengeResponse>;
|
||||||
formPromise: Promise<any>;
|
formPromise: Promise<any>;
|
||||||
|
|
||||||
private u2fScript: HTMLScriptElement;
|
|
||||||
|
|
||||||
constructor(apiService: ApiService, i18nService: I18nService,
|
constructor(apiService: ApiService, i18nService: I18nService,
|
||||||
analytics: Angulartics2, toasterService: ToasterService,
|
analytics: Angulartics2, toasterService: ToasterService,
|
||||||
platformUtilsService: PlatformUtilsService, private ngZone: NgZone) {
|
platformUtilsService: PlatformUtilsService, private ngZone: NgZone) {
|
||||||
super(apiService, i18nService, analytics, toasterService, platformUtilsService);
|
super(apiService, i18nService, analytics, toasterService, 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auth(authResponse: any) {
|
auth(authResponse: any) {
|
||||||
@ -65,18 +50,18 @@ export class TwoFactorU2fComponent extends TwoFactorBaseComponent implements OnI
|
|||||||
}
|
}
|
||||||
|
|
||||||
submit() {
|
submit() {
|
||||||
if (this.u2fResponse == null || this.keyIdAvailable == null) {
|
if (this.webAuthnResponse == null || this.keyIdAvailable == null) {
|
||||||
// Should never happen.
|
// Should never happen.
|
||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
}
|
}
|
||||||
const request = new UpdateTwoFactorU2fRequest();
|
const request = new UpdateTwoFactorWebAuthnRequest();
|
||||||
request.masterPasswordHash = this.masterPasswordHash;
|
request.masterPasswordHash = this.masterPasswordHash;
|
||||||
request.deviceResponse = this.u2fResponse;
|
request.deviceResponse = this.webAuthnResponse;
|
||||||
request.id = this.keyIdAvailable;
|
request.id = this.keyIdAvailable;
|
||||||
request.name = this.name;
|
request.name = this.name;
|
||||||
|
|
||||||
return super.enable(async () => {
|
return super.enable(async () => {
|
||||||
this.formPromise = this.apiService.putTwoFactorU2f(request);
|
this.formPromise = this.apiService.putTwoFactorWebAuthn(request);
|
||||||
const response = await this.formPromise;
|
const response = await this.formPromise;
|
||||||
await this.processResponse(response);
|
await this.processResponse(response);
|
||||||
});
|
});
|
||||||
@ -90,18 +75,18 @@ export class TwoFactorU2fComponent extends TwoFactorBaseComponent implements OnI
|
|||||||
if (this.keysConfiguredCount <= 1 || key.removePromise != null) {
|
if (this.keysConfiguredCount <= 1 || key.removePromise != null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const name = key.name != null ? key.name : this.i18nService.t('u2fkeyX', key.id);
|
const name = key.name != null ? key.name : this.i18nService.t('webAuthnkeyX', key.id);
|
||||||
const confirmed = await this.platformUtilsService.showDialog(
|
const confirmed = await this.platformUtilsService.showDialog(
|
||||||
this.i18nService.t('removeU2fConfirmation'), name,
|
this.i18nService.t('removeU2fConfirmation'), name,
|
||||||
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
|
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const request = new UpdateTwoFactorU2fDeleteRequest();
|
const request = new UpdateTwoFactorWebAuthnDeleteRequest();
|
||||||
request.id = key.id;
|
request.id = key.id;
|
||||||
request.masterPasswordHash = this.masterPasswordHash;
|
request.masterPasswordHash = this.masterPasswordHash;
|
||||||
try {
|
try {
|
||||||
key.removePromise = this.apiService.deleteTwoFactorU2f(request);
|
key.removePromise = this.apiService.deleteTwoFactorWebAuthn(request);
|
||||||
const response = await key.removePromise;
|
const response = await key.removePromise;
|
||||||
key.removePromise = null;
|
key.removePromise = null;
|
||||||
await this.processResponse(response);
|
await this.processResponse(response);
|
||||||
@ -115,41 +100,41 @@ export class TwoFactorU2fComponent extends TwoFactorBaseComponent implements OnI
|
|||||||
const request = new PasswordVerificationRequest();
|
const request = new PasswordVerificationRequest();
|
||||||
request.masterPasswordHash = this.masterPasswordHash;
|
request.masterPasswordHash = this.masterPasswordHash;
|
||||||
try {
|
try {
|
||||||
this.challengePromise = this.apiService.getTwoFactorU2fChallenge(request);
|
this.challengePromise = this.apiService.getTwoFactorWebAuthnChallenge(request);
|
||||||
const challenge = await this.challengePromise;
|
const challenge = await this.challengePromise;
|
||||||
this.readDevice(challenge);
|
this.readDevice(challenge);
|
||||||
} catch { }
|
} catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
private readDevice(u2fChallenge: ChallengeResponse) {
|
private readDevice(webAuthnChallenge: ChallengeResponse) {
|
||||||
// tslint:disable-next-line
|
// tslint:disable-next-line
|
||||||
console.log('listening for key...');
|
console.log('listening for key...');
|
||||||
this.resetU2f(true);
|
this.resetWebAuthn(true);
|
||||||
(window as any).u2f.register(u2fChallenge.appId, [{
|
|
||||||
version: u2fChallenge.version,
|
navigator.credentials.create({
|
||||||
challenge: u2fChallenge.challenge,
|
publicKey: webAuthnChallenge,
|
||||||
}], [], (data: any) => {
|
}).then((data: PublicKeyCredential) => {
|
||||||
this.ngZone.run(() => {
|
this.ngZone.run(() => {
|
||||||
this.u2fListening = false;
|
this.webAuthnListening = false;
|
||||||
if (data.errorCode) {
|
this.webAuthnResponse = data;
|
||||||
this.u2fError = true;
|
|
||||||
// tslint:disable-next-line
|
|
||||||
console.log('error: ' + data.errorCode);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.u2fResponse = JSON.stringify(data);
|
|
||||||
});
|
});
|
||||||
}, 15);
|
}).catch(err => {
|
||||||
|
// tslint:disable-next-line
|
||||||
|
console.error(err);
|
||||||
|
this.resetWebAuthn(false);
|
||||||
|
// TODO: Should we display the actual error?
|
||||||
|
this.webAuthnError = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private resetU2f(listening = false) {
|
private resetWebAuthn(listening = false) {
|
||||||
this.u2fResponse = null;
|
this.webAuthnResponse = null;
|
||||||
this.u2fError = false;
|
this.webAuthnError = false;
|
||||||
this.u2fListening = listening;
|
this.webAuthnListening = listening;
|
||||||
}
|
}
|
||||||
|
|
||||||
private processResponse(response: TwoFactorU2fResponse) {
|
private processResponse(response: TwoFactorWebAuthnResponse) {
|
||||||
this.resetU2f();
|
this.resetWebAuthn();
|
||||||
this.keys = [];
|
this.keys = [];
|
||||||
this.keyIdAvailable = null;
|
this.keyIdAvailable = null;
|
||||||
this.name = null;
|
this.name = null;
|
||||||
@ -162,13 +147,13 @@ export class TwoFactorU2fComponent extends TwoFactorBaseComponent implements OnI
|
|||||||
this.keys.push({
|
this.keys.push({
|
||||||
id: i, name: key[0].name,
|
id: i, name: key[0].name,
|
||||||
configured: true,
|
configured: true,
|
||||||
compromised: key[0].compromised,
|
migrated: key[0].migrated,
|
||||||
removePromise: null,
|
removePromise: null,
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.keys.push({ id: i, name: null, configured: false, compromised: false, removePromise: null });
|
this.keys.push({ id: i, name: null, configured: false, removePromise: null });
|
||||||
if (this.keyIdAvailable == null) {
|
if (this.keyIdAvailable == null) {
|
||||||
this.keyIdAvailable = i;
|
this.keyIdAvailable = i;
|
||||||
}
|
}
|
62
src/connectors/common-webauthn.ts
Normal file
62
src/connectors/common-webauthn.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
export function buildDataString(assertedCredential: PublicKeyCredential) {
|
||||||
|
const response = assertedCredential.response as AuthenticatorAssertionResponse;
|
||||||
|
|
||||||
|
const authData = new Uint8Array(response.authenticatorData);
|
||||||
|
const clientDataJSON = new Uint8Array(response.clientDataJSON);
|
||||||
|
const rawId = new Uint8Array(assertedCredential.rawId);
|
||||||
|
const sig = new Uint8Array(response.signature);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
id: assertedCredential.id,
|
||||||
|
rawId: coerceToBase64Url(rawId),
|
||||||
|
type: assertedCredential.type,
|
||||||
|
extensions: assertedCredential.getClientExtensionResults(),
|
||||||
|
response: {
|
||||||
|
authenticatorData: coerceToBase64Url(authData),
|
||||||
|
clientDataJson: coerceToBase64Url(clientDataJSON),
|
||||||
|
signature: coerceToBase64Url(sig),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return JSON.stringify(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function b64Decode(str: string) {
|
||||||
|
return decodeURIComponent(Array.prototype.map.call(atob(str), (c: string) => {
|
||||||
|
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
|
||||||
|
}).join(''));
|
||||||
|
}
|
||||||
|
|
||||||
|
// From https://github.com/abergs/fido2-net-lib/blob/b487a1d47373ea18cd752b4988f7262035b7b54e/Demo/wwwroot/js/helpers.js#L34
|
||||||
|
// License: https://github.com/abergs/fido2-net-lib/blob/master/LICENSE.txt
|
||||||
|
function coerceToBase64Url(thing: any) {
|
||||||
|
// Array or ArrayBuffer to Uint8Array
|
||||||
|
if (Array.isArray(thing)) {
|
||||||
|
thing = Uint8Array.from(thing);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (thing instanceof ArrayBuffer) {
|
||||||
|
thing = new Uint8Array(thing);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uint8Array to base64
|
||||||
|
if (thing instanceof Uint8Array) {
|
||||||
|
let str = '';
|
||||||
|
const len = thing.byteLength;
|
||||||
|
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
str += String.fromCharCode(thing[i]);
|
||||||
|
}
|
||||||
|
thing = window.btoa(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof thing !== 'string') {
|
||||||
|
throw new Error('could not coerce to string');
|
||||||
|
}
|
||||||
|
|
||||||
|
// base64 to base64url
|
||||||
|
// NOTE: "=" at the end of challenge is optional, strip it off here
|
||||||
|
thing = thing.replace(/\+/g, '-').replace(/\//g, '_').replace(/=*$/g, '');
|
||||||
|
|
||||||
|
return thing;
|
||||||
|
}
|
15
src/connectors/common.ts
Normal file
15
src/connectors/common.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
export function getQsParam(name: string) {
|
||||||
|
const url = window.location.href;
|
||||||
|
name = name.replace(/[\[\]]/g, '\\$&');
|
||||||
|
const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)');
|
||||||
|
const results = regex.exec(url);
|
||||||
|
|
||||||
|
if (!results) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!results[2]) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return decodeURIComponent(results[2].replace(/\+/g, ' '));
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import * as DuoWebSDK from 'duo_web_sdk';
|
import * as DuoWebSDK from 'duo_web_sdk';
|
||||||
|
import { getQsParam } from './common';
|
||||||
|
|
||||||
// tslint:disable-next-line
|
// tslint:disable-next-line
|
||||||
require('./duo.scss');
|
require('./duo.scss');
|
||||||
@ -27,22 +28,6 @@ document.addEventListener('DOMContentLoaded', event => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function getQsParam(name: string) {
|
|
||||||
const url = window.location.href;
|
|
||||||
name = name.replace(/[\[\]]/g, '\\$&');
|
|
||||||
const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)');
|
|
||||||
const results = regex.exec(url);
|
|
||||||
|
|
||||||
if (!results) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (!results[2]) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return decodeURIComponent(results[2].replace(/\+/g, ' '));
|
|
||||||
}
|
|
||||||
|
|
||||||
function invokeCSCode(data: string) {
|
function invokeCSCode(data: string) {
|
||||||
try {
|
try {
|
||||||
(window as any).invokeCSharpAction(data);
|
(window as any).invokeCSharpAction(data);
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
// tslint:disable-next-line
|
import { getQsParam } from './common';
|
||||||
|
|
||||||
|
// tslint:disable-next-line
|
||||||
require('./sso.scss');
|
require('./sso.scss');
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', event => {
|
document.addEventListener('DOMContentLoaded', event => {
|
||||||
@ -19,22 +21,6 @@ document.addEventListener('DOMContentLoaded', event => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function getQsParam(name: string) {
|
|
||||||
const url = window.location.href;
|
|
||||||
name = name.replace(/[\[\]]/g, '\\$&');
|
|
||||||
const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)');
|
|
||||||
const results = regex.exec(url);
|
|
||||||
|
|
||||||
if (!results) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (!results[2]) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return decodeURIComponent(results[2].replace(/\+/g, ' '));
|
|
||||||
}
|
|
||||||
|
|
||||||
function initiateBrowserSso(code: string, state: string) {
|
function initiateBrowserSso(code: string, state: string) {
|
||||||
window.postMessage({ command: 'authResult', code: code, state: state }, '*');
|
window.postMessage({ command: 'authResult', code: code, state: state }, '*');
|
||||||
const handOffMessage = ('; ' + document.cookie).split('; ssoHandOffMessage=').pop().split(';').shift();
|
const handOffMessage = ('; ' + document.cookie).split('; ssoHandOffMessage=').pop().split(';').shift();
|
||||||
|
36
src/connectors/webauthn-fallback.html
Normal file
36
src/connectors/webauthn-fallback.html
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Bitwarden WebAuthn Connector</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="layout_frontend">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center mt-5">
|
||||||
|
<div class="col-5">
|
||||||
|
<img src="../images/logo-dark@2x.png" class="mb-4 logo" alt="Bitwarden">
|
||||||
|
<div id="spinner">
|
||||||
|
<p class="text-center">
|
||||||
|
<i class="fa fa-spinner fa-spin fa-2x text-muted" title="Loading" aria-hidden="true"></i>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div id="content" class="card mt-4 d-none">
|
||||||
|
<div class="card-body ng-star-inserted">
|
||||||
|
<p id="msg" class="text-center"></p>
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" id="remember" name="remember">
|
||||||
|
<label class="form-check-label" for="remember" id="remember-label"></label>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<p class="text-center mb-0">
|
||||||
|
<button id="webauthn-button" onClick="javascript:init()" class="btn btn-primary btn-lg"></button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
118
src/connectors/webauthn-fallback.ts
Normal file
118
src/connectors/webauthn-fallback.ts
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import { getQsParam } from './common';
|
||||||
|
import { b64Decode, buildDataString } from './common-webauthn';
|
||||||
|
|
||||||
|
// tslint:disable-next-line
|
||||||
|
require('./webauthn.scss');
|
||||||
|
|
||||||
|
let parentUrl: string = null;
|
||||||
|
let parentOrigin: string = null;
|
||||||
|
let sentSuccess = false;
|
||||||
|
|
||||||
|
let locales: any = {};
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
const locale = getQsParam('locale');
|
||||||
|
|
||||||
|
const filePath = `locales/${locale}/messages.json?cache=${process.env.CACHE_TAG}`;
|
||||||
|
const localesResult = await fetch(filePath);
|
||||||
|
locales = await localesResult.json();
|
||||||
|
|
||||||
|
document.getElementById('msg').innerText = translate('webAuthnFallbackMsg');
|
||||||
|
document.getElementById('remember-label').innerText = translate('rememberMe');
|
||||||
|
document.getElementById('webauthn-button').innerText = translate('webAuthnAuthenticate');
|
||||||
|
|
||||||
|
document.getElementById('spinner').classList.add('d-none');
|
||||||
|
const content = document.getElementById('content');
|
||||||
|
content.classList.add('d-block');
|
||||||
|
content.classList.remove('d-none');
|
||||||
|
});
|
||||||
|
|
||||||
|
function translate(id: string) {
|
||||||
|
return locales[id]?.message || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
(window as any).init = () => {
|
||||||
|
start();
|
||||||
|
};
|
||||||
|
|
||||||
|
function start() {
|
||||||
|
if (sentSuccess) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!('credentials' in navigator)) {
|
||||||
|
error(translate('webAuthnNotSupported'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = getQsParam('data');
|
||||||
|
if (!data) {
|
||||||
|
error('No data.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
parentUrl = getQsParam('parent');
|
||||||
|
if (!parentUrl) {
|
||||||
|
error('No parent.');
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
parentUrl = decodeURIComponent(parentUrl);
|
||||||
|
parentOrigin = new URL(parentUrl).origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
let json: any;
|
||||||
|
try {
|
||||||
|
const jsonString = b64Decode(data);
|
||||||
|
json = JSON.parse(jsonString);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
error('Cannot parse data.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
initWebAuthn(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initWebAuthn(obj: any) {
|
||||||
|
const challenge = obj.challenge.replace(/-/g, '+').replace(/_/g, '/');
|
||||||
|
obj.challenge = Uint8Array.from(atob(challenge), c => c.charCodeAt(0));
|
||||||
|
|
||||||
|
// fix escaping. Change this to coerce
|
||||||
|
obj.allowCredentials.forEach((listItem: any) => {
|
||||||
|
const fixedId = listItem.id.replace(/\_/g, '/').replace(/\-/g, '+');
|
||||||
|
listItem.id = Uint8Array.from(atob(fixedId), c => c.charCodeAt(0));
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const assertedCredential = await navigator.credentials.get({ publicKey: obj }) as PublicKeyCredential;
|
||||||
|
|
||||||
|
if (sentSuccess) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataString = buildDataString(assertedCredential);
|
||||||
|
const remember = (document.getElementById('remember') as HTMLInputElement).checked;
|
||||||
|
window.postMessage({ command: 'webAuthnResult', data: dataString, remember: remember }, '*');
|
||||||
|
|
||||||
|
sentSuccess = true;
|
||||||
|
success(translate('webAuthnSuccess'));
|
||||||
|
} catch (err) {
|
||||||
|
error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function error(message: string) {
|
||||||
|
const el = document.getElementById('msg');
|
||||||
|
el.innerHTML = message;
|
||||||
|
el.classList.add('alert');
|
||||||
|
el.classList.add('alert-danger');
|
||||||
|
}
|
||||||
|
|
||||||
|
function success(message: string) {
|
||||||
|
(document.getElementById('webauthn-button') as HTMLButtonElement).disabled = true;
|
||||||
|
|
||||||
|
const el = document.getElementById('msg');
|
||||||
|
el.innerHTML = message;
|
||||||
|
el.classList.add('alert');
|
||||||
|
el.classList.add('alert-success');
|
||||||
|
}
|
16
src/connectors/webauthn.html
Normal file
16
src/connectors/webauthn.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Bitwarden WebAuthn Connector</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body style="background: transparent;">
|
||||||
|
<img src="../images/u2fkey.jpg" class="rounded img-fluid mb-3">
|
||||||
|
<div class="text-center">
|
||||||
|
<button id="webauthn-button" class="btn btn-primary" onclick="javascript:executeWebAuthn()"></button>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
5
src/connectors/webauthn.scss
Normal file
5
src/connectors/webauthn.scss
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
@import "../scss/styles.scss";
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-width: 0px !important;
|
||||||
|
}
|
122
src/connectors/webauthn.ts
Normal file
122
src/connectors/webauthn.ts
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import { getQsParam } from './common';
|
||||||
|
import { b64Decode, buildDataString } from './common-webauthn';
|
||||||
|
|
||||||
|
// tslint:disable-next-line
|
||||||
|
require('./webauthn.scss');
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
init();
|
||||||
|
|
||||||
|
const text = getQsParam('btnText');
|
||||||
|
if (text) {
|
||||||
|
document.getElementById('webauthn-button').innerText = decodeURI(text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let parentUrl: string = null;
|
||||||
|
let parentOrigin: string = null;
|
||||||
|
let stopWebAuthn = false;
|
||||||
|
let sentSuccess = false;
|
||||||
|
let obj: any = null;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
start();
|
||||||
|
onMessage();
|
||||||
|
info('ready');
|
||||||
|
}
|
||||||
|
|
||||||
|
function start() {
|
||||||
|
sentSuccess = false;
|
||||||
|
|
||||||
|
if (!('credentials' in navigator)) {
|
||||||
|
error('WebAuthn is not supported in this browser.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = getQsParam('data');
|
||||||
|
if (!data) {
|
||||||
|
error('No data.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
parentUrl = getQsParam('parent');
|
||||||
|
if (!parentUrl) {
|
||||||
|
error('No parent.');
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
parentUrl = decodeURIComponent(parentUrl);
|
||||||
|
parentOrigin = new URL(parentUrl).origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const jsonString = b64Decode(data);
|
||||||
|
obj = JSON.parse(jsonString);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
error('Cannot parse data.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const challenge = obj.challenge.replace(/-/g, '+').replace(/_/g, '/');
|
||||||
|
obj.challenge = Uint8Array.from(atob(challenge), c => c.charCodeAt(0));
|
||||||
|
|
||||||
|
// fix escaping. Change this to coerce
|
||||||
|
obj.allowCredentials.forEach((listItem: any) => {
|
||||||
|
const fixedId = listItem.id.replace(/\_/g, '/').replace(/\-/g, '+');
|
||||||
|
listItem.id = Uint8Array.from(atob(fixedId), c => c.charCodeAt(0));
|
||||||
|
});
|
||||||
|
|
||||||
|
stopWebAuthn = false;
|
||||||
|
|
||||||
|
if (navigator.userAgent.indexOf(' Safari/') !== -1 && navigator.userAgent.indexOf('Chrome') === -1) {
|
||||||
|
// TODO: Hide image, show button
|
||||||
|
} else {
|
||||||
|
executeWebAuthn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeWebAuthn() {
|
||||||
|
if (stopWebAuthn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigator.credentials.get({ publicKey: obj })
|
||||||
|
.then(success)
|
||||||
|
.catch(err => error('WebAuth Error: ' + err));
|
||||||
|
}
|
||||||
|
|
||||||
|
(window as any).executeWebAuthn = executeWebAuthn;
|
||||||
|
|
||||||
|
function onMessage() {
|
||||||
|
window.addEventListener('message', event => {
|
||||||
|
if (!event.origin || event.origin === '' || event.origin !== parentOrigin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.data === 'stop') {
|
||||||
|
stopWebAuthn = true;
|
||||||
|
}
|
||||||
|
else if (event.data === 'start' && stopWebAuthn) {
|
||||||
|
start();
|
||||||
|
}
|
||||||
|
}, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function error(message: string) {
|
||||||
|
parent.postMessage('error|' + message, parentUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
function success(assertedCredential: PublicKeyCredential) {
|
||||||
|
if (sentSuccess) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataString = buildDataString(assertedCredential);
|
||||||
|
parent.postMessage('success|' + dataString, parentUrl);
|
||||||
|
sentSuccess = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function info(message: string) {
|
||||||
|
parent.postMessage('info|' + message, parentUrl);
|
||||||
|
}
|
||||||
|
|
BIN
src/images/two-factor/7.png
Normal file
BIN
src/images/two-factor/7.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
@ -725,6 +725,15 @@
|
|||||||
"u2fTitle": {
|
"u2fTitle": {
|
||||||
"message": "FIDO U2F Security Key"
|
"message": "FIDO U2F Security Key"
|
||||||
},
|
},
|
||||||
|
"webAuthnTitle": {
|
||||||
|
"message": "FIDO2 WebAuthn"
|
||||||
|
},
|
||||||
|
"webAuthnDesc": {
|
||||||
|
"message": "Use any WebAuthn enabled security key to access your account."
|
||||||
|
},
|
||||||
|
"webAuthnMigrated": {
|
||||||
|
"message": "(Migrated from FIDO)"
|
||||||
|
},
|
||||||
"emailTitle": {
|
"emailTitle": {
|
||||||
"message": "Email"
|
"message": "Email"
|
||||||
},
|
},
|
||||||
@ -1260,6 +1269,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"webAuthnkeyX": {
|
||||||
|
"message": "WebAuthn Key $INDEX$",
|
||||||
|
"placeholders": {
|
||||||
|
"index": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"nfcSupport": {
|
"nfcSupport": {
|
||||||
"message": "NFC Support"
|
"message": "NFC Support"
|
||||||
},
|
},
|
||||||
@ -1305,6 +1323,9 @@
|
|||||||
"removeU2fConfirmation": {
|
"removeU2fConfirmation": {
|
||||||
"message": "Are you sure you want to remove this security key?"
|
"message": "Are you sure you want to remove this security key?"
|
||||||
},
|
},
|
||||||
|
"twoFactorWebAuthnAdd": {
|
||||||
|
"message": "Add a WebAuthn security key to your account"
|
||||||
|
},
|
||||||
"readKey": {
|
"readKey": {
|
||||||
"message": "Read Key"
|
"message": "Read Key"
|
||||||
},
|
},
|
||||||
@ -1338,6 +1359,12 @@
|
|||||||
"twoFactorU2fProblemReadingTryAgain": {
|
"twoFactorU2fProblemReadingTryAgain": {
|
||||||
"message": "There was a problem reading the security key. Try again."
|
"message": "There was a problem reading the security key. Try again."
|
||||||
},
|
},
|
||||||
|
"twoFactorWebAuthnWarning": {
|
||||||
|
"message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should enable another two-step login provider so that you can access your account when WebAuthn cannot be used. Supported platforms:"
|
||||||
|
},
|
||||||
|
"twoFactorWebAuthnSupportWeb": {
|
||||||
|
"message": "Web vault and browser extensions on a desktop/laptop with a WebAuthn enabled browser (Chrome, Opera, Vivaldi, or Firefox with FIDO U2F enabled)."
|
||||||
|
},
|
||||||
"twoFactorRecoveryYourCode": {
|
"twoFactorRecoveryYourCode": {
|
||||||
"message": "Your Bitwarden two-step login recovery code"
|
"message": "Your Bitwarden two-step login recovery code"
|
||||||
},
|
},
|
||||||
@ -3762,5 +3789,17 @@
|
|||||||
},
|
},
|
||||||
"dateParsingError": {
|
"dateParsingError": {
|
||||||
"message": "There was an error saving your deletion and expiration dates."
|
"message": "There was an error saving your deletion and expiration dates."
|
||||||
|
},
|
||||||
|
"webAuthnFallbackMsg": {
|
||||||
|
"message": "To verify your 2FA please click the button below."
|
||||||
|
},
|
||||||
|
"webAuthnAuthenticate": {
|
||||||
|
"message": "Authenticate WebAutn"
|
||||||
|
},
|
||||||
|
"webAuthnNotSupported": {
|
||||||
|
"message": "WebAuthn is not supported in this browser."
|
||||||
|
},
|
||||||
|
"webAuthnSuccess": {
|
||||||
|
"message": "<strong>WebAuthn verified successfully!</strong><br>You may close this tab."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -604,6 +604,17 @@ app-user-billing {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#web-authn-frame {
|
||||||
|
background: url('../images/loading.svg') 0 0 no-repeat;
|
||||||
|
height: 290px;
|
||||||
|
|
||||||
|
iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#bt-dropin-container {
|
#bt-dropin-container {
|
||||||
background: url('../images/loading.svg') 0 0 no-repeat;
|
background: url('../images/loading.svg') 0 0 no-repeat;
|
||||||
min-height: 50px;
|
min-height: 50px;
|
||||||
|
@ -158,11 +158,8 @@ export class WebPlatformUtilsService implements PlatformUtilsService {
|
|||||||
return process.env.APPLICATION_VERSION || '-';
|
return process.env.APPLICATION_VERSION || '-';
|
||||||
}
|
}
|
||||||
|
|
||||||
supportsU2f(win: Window): boolean {
|
supportsWebAuthn(win: Window): boolean {
|
||||||
if (win != null && (win as any).u2f != null) {
|
return (typeof(PublicKeyCredential) !== 'undefined');
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return this.isChrome() || ((this.isEdge() || this.isOpera() || this.isVivaldi()) && !Utils.isMobileBrowser);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
supportsDuo(): boolean {
|
supportsDuo(): boolean {
|
||||||
|
@ -88,6 +88,16 @@ const plugins = [
|
|||||||
filename: 'u2f-connector.html',
|
filename: 'u2f-connector.html',
|
||||||
chunks: ['connectors/u2f'],
|
chunks: ['connectors/u2f'],
|
||||||
}),
|
}),
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
template: './src/connectors/webauthn.html',
|
||||||
|
filename: 'webauthn-connector.html',
|
||||||
|
chunks: ['connectors/webauthn'],
|
||||||
|
}),
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
template: './src/connectors/webauthn-fallback.html',
|
||||||
|
filename: 'webauthn-fallback-connector.html',
|
||||||
|
chunks: ['connectors/webauthn-fallback'],
|
||||||
|
}),
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
template: './src/connectors/sso.html',
|
template: './src/connectors/sso.html',
|
||||||
filename: 'sso-connector.html',
|
filename: 'sso-connector.html',
|
||||||
@ -158,6 +168,8 @@ const config = {
|
|||||||
'app/polyfills': './src/app/polyfills.ts',
|
'app/polyfills': './src/app/polyfills.ts',
|
||||||
'app/main': './src/app/main.ts',
|
'app/main': './src/app/main.ts',
|
||||||
'connectors/u2f': './src/connectors/u2f.js',
|
'connectors/u2f': './src/connectors/u2f.js',
|
||||||
|
'connectors/webauthn': './src/connectors/webauthn.ts',
|
||||||
|
'connectors/webauthn-fallback': './src/connectors/webauthn-fallback.ts',
|
||||||
'connectors/duo': './src/connectors/duo.ts',
|
'connectors/duo': './src/connectors/duo.ts',
|
||||||
'connectors/sso': './src/connectors/sso.ts',
|
'connectors/sso': './src/connectors/sso.ts',
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user