add u2f support to two factor component
This commit is contained in:
parent
f673bd62d7
commit
013bf20a35
|
@ -1,4 +1,7 @@
|
||||||
import { OnInit } from '@angular/core';
|
import {
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
} from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
import { ToasterService } from 'angular2-toaster';
|
import { ToasterService } from 'angular2-toaster';
|
||||||
|
@ -12,13 +15,16 @@ import { TwoFactorEmailRequest } from '../../models/request/twoFactorEmailReques
|
||||||
|
|
||||||
import { ApiService } from '../../abstractions/api.service';
|
import { ApiService } from '../../abstractions/api.service';
|
||||||
import { AuthService } from '../../abstractions/auth.service';
|
import { AuthService } from '../../abstractions/auth.service';
|
||||||
|
import { EnvironmentService } from '../../abstractions/environment.service';
|
||||||
import { I18nService } from '../../abstractions/i18n.service';
|
import { I18nService } from '../../abstractions/i18n.service';
|
||||||
import { PlatformUtilsService } from '../../abstractions/platformUtils.service';
|
import { PlatformUtilsService } from '../../abstractions/platformUtils.service';
|
||||||
import { SyncService } from '../../abstractions/sync.service';
|
import { SyncService } from '../../abstractions/sync.service';
|
||||||
|
|
||||||
import { TwoFactorProviders } from '../../services/auth.service';
|
import { TwoFactorProviders } from '../../services/auth.service';
|
||||||
|
|
||||||
export class TwoFactorComponent implements OnInit {
|
import { U2f } from '../../misc/u2f';
|
||||||
|
|
||||||
|
export class TwoFactorComponent implements OnInit, OnDestroy {
|
||||||
token: string = '';
|
token: string = '';
|
||||||
remember: boolean = false;
|
remember: boolean = false;
|
||||||
u2fReady: boolean = false;
|
u2fReady: boolean = false;
|
||||||
|
@ -26,7 +32,7 @@ export class TwoFactorComponent implements OnInit {
|
||||||
providerType = TwoFactorProviderType;
|
providerType = TwoFactorProviderType;
|
||||||
selectedProviderType: TwoFactorProviderType = TwoFactorProviderType.Authenticator;
|
selectedProviderType: TwoFactorProviderType = TwoFactorProviderType.Authenticator;
|
||||||
u2fSupported: boolean = false;
|
u2fSupported: boolean = false;
|
||||||
u2f: any = null;
|
u2f: U2f = null;
|
||||||
title: string = '';
|
title: string = '';
|
||||||
twoFactorEmail: string = null;
|
twoFactorEmail: string = null;
|
||||||
formPromise: Promise<any>;
|
formPromise: Promise<any>;
|
||||||
|
@ -37,7 +43,8 @@ export class TwoFactorComponent implements OnInit {
|
||||||
constructor(protected authService: AuthService, protected router: Router,
|
constructor(protected authService: AuthService, protected router: Router,
|
||||||
protected analytics: Angulartics2, protected toasterService: ToasterService,
|
protected analytics: Angulartics2, protected toasterService: ToasterService,
|
||||||
protected i18nService: I18nService, protected apiService: ApiService,
|
protected i18nService: I18nService, protected apiService: ApiService,
|
||||||
protected platformUtilsService: PlatformUtilsService, protected syncService: SyncService) {
|
protected platformUtilsService: PlatformUtilsService, protected syncService: SyncService,
|
||||||
|
protected win: Window, protected environmentService: EnvironmentService) {
|
||||||
this.u2fSupported = this.platformUtilsService.supportsU2f(window);
|
this.u2fSupported = this.platformUtilsService.supportsU2f(window);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,26 +55,62 @@ export class TwoFactorComponent implements OnInit {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.win != null && this.u2fSupported) {
|
||||||
|
let customWebVaultUrl: string = null;
|
||||||
|
if (this.environmentService.baseUrl) {
|
||||||
|
customWebVaultUrl = this.environmentService.baseUrl;
|
||||||
|
}
|
||||||
|
else if (this.environmentService.webVaultUrl) {
|
||||||
|
customWebVaultUrl = this.environmentService.webVaultUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.u2f = new U2f(this.win, customWebVaultUrl, (token: string) => {
|
||||||
|
this.token = token;
|
||||||
|
this.submit();
|
||||||
|
}, (error: string) => {
|
||||||
|
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), error);
|
||||||
|
}, (info: string) => {
|
||||||
|
if (info === 'ready') {
|
||||||
|
this.u2fReady = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.selectedProviderType = this.authService.getDefaultTwoFactorProvider(this.u2fSupported);
|
this.selectedProviderType = this.authService.getDefaultTwoFactorProvider(this.u2fSupported);
|
||||||
await this.init();
|
await this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.cleanupU2f();
|
||||||
|
this.u2f = null;
|
||||||
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
if (this.selectedProviderType == null) {
|
if (this.selectedProviderType == null) {
|
||||||
this.title = this.i18nService.t('loginUnavailable');
|
this.title = this.i18nService.t('loginUnavailable');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.cleanupU2f();
|
||||||
this.title = (TwoFactorProviders as any)[this.selectedProviderType].name;
|
this.title = (TwoFactorProviders as any)[this.selectedProviderType].name;
|
||||||
const params = this.authService.twoFactorProviders.get(this.selectedProviderType);
|
const params = this.authService.twoFactorProviders.get(this.selectedProviderType);
|
||||||
switch (this.selectedProviderType) {
|
switch (this.selectedProviderType) {
|
||||||
case TwoFactorProviderType.U2f:
|
case TwoFactorProviderType.U2f:
|
||||||
if (!this.u2fSupported) {
|
if (!this.u2fSupported || this.u2f == null) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const challenges = JSON.parse(params.Challenges);
|
const challenges = JSON.parse(params.Challenges);
|
||||||
// TODO: init u2f
|
if (challenges.length > 0) {
|
||||||
|
this.u2f.init({
|
||||||
|
appId: challenges[0].appId,
|
||||||
|
challenge: challenges[0].challenge,
|
||||||
|
keys: [{
|
||||||
|
version: challenges[0].version,
|
||||||
|
keyHandle: challenges[0].keyHandle
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case TwoFactorProviderType.Duo:
|
case TwoFactorProviderType.Duo:
|
||||||
case TwoFactorProviderType.OrganizationDuo:
|
case TwoFactorProviderType.OrganizationDuo:
|
||||||
|
@ -104,7 +147,11 @@ export class TwoFactorComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.selectedProviderType === TwoFactorProviderType.U2f) {
|
if (this.selectedProviderType === TwoFactorProviderType.U2f) {
|
||||||
// TODO: stop U2f
|
if (this.u2f != null) {
|
||||||
|
this.u2f.stop();
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
} else if (this.selectedProviderType === TwoFactorProviderType.Email ||
|
} else if (this.selectedProviderType === TwoFactorProviderType.Email ||
|
||||||
this.selectedProviderType === TwoFactorProviderType.Authenticator) {
|
this.selectedProviderType === TwoFactorProviderType.Authenticator) {
|
||||||
this.token = this.token.replace(' ', '').trim();
|
this.token = this.token.replace(' ', '').trim();
|
||||||
|
@ -116,9 +163,11 @@ export class TwoFactorComponent implements OnInit {
|
||||||
this.syncService.fullSync(true);
|
this.syncService.fullSync(true);
|
||||||
this.analytics.eventTrack.next({ action: 'Logged In From Two-step' });
|
this.analytics.eventTrack.next({ action: 'Logged In From Two-step' });
|
||||||
this.router.navigate([this.successRoute]);
|
this.router.navigate([this.successRoute]);
|
||||||
} catch {
|
} catch (e) {
|
||||||
if (this.selectedProviderType === TwoFactorProviderType.U2f) {
|
if (this.selectedProviderType === TwoFactorProviderType.U2f && this.u2f != null) {
|
||||||
// TODO: start U2F again
|
this.u2f.start();
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -144,4 +193,11 @@ export class TwoFactorComponent implements OnInit {
|
||||||
|
|
||||||
this.emailPromise = null;
|
this.emailPromise = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private cleanupU2f() {
|
||||||
|
if (this.u2f != null) {
|
||||||
|
this.u2f.stop();
|
||||||
|
this.u2f.cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
export class U2f {
|
||||||
|
private iframe: HTMLIFrameElement = null;
|
||||||
|
private connectorLink: HTMLAnchorElement;
|
||||||
|
|
||||||
|
constructor(private win: Window, private webVaultUrl: string, private successCallback: Function,
|
||||||
|
private errorCallback: Function, private infoCallback: Function) {
|
||||||
|
this.connectorLink = win.document.createElement('a');
|
||||||
|
this.webVaultUrl = webVaultUrl != null && webVaultUrl !== '' ? webVaultUrl : 'https://vault.bitwarden.com';
|
||||||
|
}
|
||||||
|
|
||||||
|
init(data: any): void {
|
||||||
|
this.connectorLink.href = this.webVaultUrl + '/u2f-connector.html' +
|
||||||
|
'?data=' + this.base64Encode(JSON.stringify(data)) +
|
||||||
|
'&parent=' + encodeURIComponent(this.win.document.location.href) +
|
||||||
|
'&v=1';
|
||||||
|
|
||||||
|
this.iframe = this.win.document.getElementById('u2f_iframe') as HTMLIFrameElement;
|
||||||
|
this.iframe.src = this.connectorLink.href;
|
||||||
|
|
||||||
|
this.win.addEventListener('message', (e) => this.parseMessage(e), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
this.sendMessage('stop');
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
this.sendMessage('start');
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessage(message: any) {
|
||||||
|
if (!this.iframe || !this.iframe.src || !this.iframe.contentWindow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.iframe.contentWindow.postMessage(message, this.iframe.src);
|
||||||
|
}
|
||||||
|
|
||||||
|
base64Encode(str: string): string {
|
||||||
|
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (match, p1) => {
|
||||||
|
return String.fromCharCode(('0x' + p1) as any);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
this.win.removeEventListener('message', (e) => this.parseMessage(e), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseMessage(event: any) {
|
||||||
|
if (!this.validMessage(event)) {
|
||||||
|
this.errorCallback('Invalid message.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts: string[] = event.data.split('|');
|
||||||
|
if (parts[0] === 'success' && this.successCallback) {
|
||||||
|
this.successCallback(parts[1]);
|
||||||
|
} else if (parts[0] === 'error' && this.errorCallback) {
|
||||||
|
this.errorCallback(parts[1]);
|
||||||
|
} else if (parts[0] === 'info' && this.infoCallback) {
|
||||||
|
this.infoCallback(parts[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private validMessage(event: any) {
|
||||||
|
if (!event.origin || event.origin === '' || event.origin !== (this.connectorLink as any).origin) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return event.data.indexOf('success|') === 0 || event.data.indexOf('error|') === 0 ||
|
||||||
|
event.data.indexOf('info|') === 0;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue