Skip to content
This repository has been archived by the owner on Jun 17, 2022. It is now read-only.

Commit

Permalink
WebAuthn (#163)
Browse files Browse the repository at this point in the history
  • Loading branch information
Hinton committed Mar 15, 2021
1 parent f80e894 commit f20af0c
Show file tree
Hide file tree
Showing 15 changed files with 210 additions and 185 deletions.
17 changes: 7 additions & 10 deletions src/abstractions/api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ import { UpdateProfileRequest } from '../models/request/updateProfileRequest';
import { UpdateTwoFactorAuthenticatorRequest } from '../models/request/updateTwoFactorAuthenticatorRequest';
import { UpdateTwoFactorDuoRequest } from '../models/request/updateTwoFactorDuoRequest';
import { UpdateTwoFactorEmailRequest } from '../models/request/updateTwoFactorEmailRequest';
import { UpdateTwoFactorU2fDeleteRequest } from '../models/request/updateTwoFactorU2fDeleteRequest';
import { UpdateTwoFactorU2fRequest } from '../models/request/updateTwoFactorU2fRequest';
import { UpdateTwoFactorWebAuthnDeleteRequest } from '../models/request/updateTwoFactorWebAuthnDeleteRequest';
import { UpdateTwoFactorWebAuthnRequest } from '../models/request/updateTwoFactorWebAuthnRequest';
import { UpdateTwoFactorYubioOtpRequest } from '../models/request/updateTwoFactorYubioOtpRequest';
import { VerifyBankRequest } from '../models/request/verifyBankRequest';
import { VerifyDeleteRecoverRequest } from '../models/request/verifyDeleteRecoverRequest';
Expand Down Expand Up @@ -117,10 +117,7 @@ import { TwoFactorDuoResponse } from '../models/response/twoFactorDuoResponse';
import { TwoFactorEmailResponse } from '../models/response/twoFactorEmailResponse';
import { TwoFactorProviderResponse } from '../models/response/twoFactorProviderResponse';
import { TwoFactorRecoverResponse } from '../models/response/twoFactorRescoverResponse';
import {
ChallengeResponse,
TwoFactorU2fResponse,
} from '../models/response/twoFactorU2fResponse';
import { ChallengeResponse, TwoFactorWebAuthnResponse } from '../models/response/twoFactorWebAuthnResponse';
import { TwoFactorYubiKeyResponse } from '../models/response/twoFactorYubiKeyResponse';
import { UserKeyResponse } from '../models/response/userKeyResponse';

Expand Down Expand Up @@ -274,8 +271,8 @@ export abstract class ApiService {
getTwoFactorOrganizationDuo: (organizationId: string,
request: PasswordVerificationRequest) => Promise<TwoFactorDuoResponse>;
getTwoFactorYubiKey: (request: PasswordVerificationRequest) => Promise<TwoFactorYubiKeyResponse>;
getTwoFactorU2f: (request: PasswordVerificationRequest) => Promise<TwoFactorU2fResponse>;
getTwoFactorU2fChallenge: (request: PasswordVerificationRequest) => Promise<ChallengeResponse>;
getTwoFactorWebAuthn: (request: PasswordVerificationRequest) => Promise<TwoFactorWebAuthnResponse>;
getTwoFactorWebAuthnChallenge: (request: PasswordVerificationRequest) => Promise<ChallengeResponse>;
getTwoFactorRecover: (request: PasswordVerificationRequest) => Promise<TwoFactorRecoverResponse>;
putTwoFactorAuthenticator: (
request: UpdateTwoFactorAuthenticatorRequest) => Promise<TwoFactorAuthenticatorResponse>;
Expand All @@ -284,8 +281,8 @@ export abstract class ApiService {
putTwoFactorOrganizationDuo: (organizationId: string,
request: UpdateTwoFactorDuoRequest) => Promise<TwoFactorDuoResponse>;
putTwoFactorYubiKey: (request: UpdateTwoFactorYubioOtpRequest) => Promise<TwoFactorYubiKeyResponse>;
putTwoFactorU2f: (request: UpdateTwoFactorU2fRequest) => Promise<TwoFactorU2fResponse>;
deleteTwoFactorU2f: (request: UpdateTwoFactorU2fDeleteRequest) => Promise<TwoFactorU2fResponse>;
putTwoFactorWebAuthn: (request: UpdateTwoFactorWebAuthnRequest) => Promise<TwoFactorWebAuthnResponse>;
deleteTwoFactorWebAuthn: (request: UpdateTwoFactorWebAuthnDeleteRequest) => Promise<TwoFactorWebAuthnResponse>;
putTwoFactorDisable: (request: TwoFactorProviderRequest) => Promise<TwoFactorProviderResponse>;
putTwoFactorOrganizationDisable: (organizationId: string,
request: TwoFactorProviderRequest) => Promise<TwoFactorProviderResponse>;
Expand Down
2 changes: 1 addition & 1 deletion src/abstractions/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export abstract class AuthService {
twoFactorToken: string, remember?: boolean) => Promise<AuthResult>;
logOut: (callback: Function) => void;
getSupportedTwoFactorProviders: (win: Window) => any[];
getDefaultTwoFactorProvider: (u2fSupported: boolean) => TwoFactorProviderType;
getDefaultTwoFactorProvider: (webAuthnSupported: boolean) => TwoFactorProviderType;
makePreloginKey: (masterPassword: string, email: string) => Promise<SymmetricCryptoKey>;
authingWithApiKey: () => boolean;
authingWithSso: () => boolean;
Expand Down
2 changes: 1 addition & 1 deletion src/abstractions/platformUtils.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export abstract class PlatformUtilsService {
launchUri: (uri: string, options?: any) => void;
saveFile: (win: Window, blobData: any, blobOptions: any, fileName: string) => void;
getApplicationVersion: () => string;
supportsU2f: (win: Window) => boolean;
supportsWebAuthn: (win: Window) => boolean;
supportsDuo: () => boolean;
showToast: (type: 'error' | 'success' | 'warning' | 'info', title: string, text: string | string[],
options?: any) => void;
Expand Down
140 changes: 63 additions & 77 deletions src/angular/components/two-factor.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,18 @@ import { TwoFactorProviders } from '../../services/auth.service';
import { ConstantsService } from '../../services/constants.service';

import * as DuoWebSDK from 'duo_web_sdk';
import { U2f } from '../../misc/u2f';
import { WebAuthn } from '../../misc/webauthn';

export class TwoFactorComponent implements OnInit, OnDestroy {
token: string = '';
remember: boolean = false;
u2fReady: boolean = false;
initU2f: boolean = true;
webAuthnReady: boolean = false;
webAuthnNewTab: boolean = false;
providers = TwoFactorProviders;
providerType = TwoFactorProviderType;
selectedProviderType: TwoFactorProviderType = TwoFactorProviderType.Authenticator;
u2fSupported: boolean = false;
u2f: U2f = null;
webAuthnSupported: boolean = false;
webAuthn: WebAuthn = null;
title: string = '';
twoFactorEmail: string = null;
formPromise: Promise<any>;
Expand All @@ -54,7 +54,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy {
protected platformUtilsService: PlatformUtilsService, protected win: Window,
protected environmentService: EnvironmentService, protected stateService: StateService,
protected storageService: StorageService, protected route: ActivatedRoute) {
this.u2fSupported = this.platformUtilsService.supportsU2f(win);
this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win);
}

async ngOnInit() {
Expand All @@ -77,33 +77,32 @@ export class TwoFactorComponent implements OnInit, OnDestroy {
this.successRoute = 'lock';
}

if (this.initU2f && this.win != null && this.u2fSupported) {
let customWebVaultUrl: string = null;
if (this.environmentService.baseUrl != null) {
customWebVaultUrl = this.environmentService.baseUrl;
} else if (this.environmentService.webVaultUrl != null) {
customWebVaultUrl = this.environmentService.webVaultUrl;
if (this.win != null && this.webAuthnSupported) {
let webVaultUrl = this.environmentService.getWebVaultUrl();
if (webVaultUrl == null) {
webVaultUrl = 'https://vault.bitwarden.com';
}

this.u2f = new U2f(this.win, customWebVaultUrl, (token: string) => {
this.token = token;
this.submit();
}, (error: string) => {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), error);
}, (info: string) => {
if (info === 'ready') {
this.u2fReady = true;
this.webAuthn = new WebAuthn(this.win, webVaultUrl, this.webAuthnNewTab, this.platformUtilsService,
this.i18nService, (token: string) => {
this.token = token;
this.submit();
}, (error: string) => {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), error);
}, (info: string) => {
if (info === 'ready') {
this.webAuthnReady = true;
}
}
});
);
}

this.selectedProviderType = this.authService.getDefaultTwoFactorProvider(this.u2fSupported);
this.selectedProviderType = this.authService.getDefaultTwoFactorProvider(this.webAuthnSupported);
await this.init();
}

ngOnDestroy(): void {
this.cleanupU2f();
this.u2f = null;
this.cleanupWebAuthn();
this.webAuthn = null;
}

async init() {
Expand All @@ -112,35 +111,18 @@ export class TwoFactorComponent implements OnInit, OnDestroy {
return;
}

this.cleanupU2f();
this.cleanupWebAuthn();
this.title = (TwoFactorProviders as any)[this.selectedProviderType].name;
const providerData = this.authService.twoFactorProvidersData.get(this.selectedProviderType);
switch (this.selectedProviderType) {
case TwoFactorProviderType.U2f:
if (!this.u2fSupported || this.u2f == null) {
case TwoFactorProviderType.WebAuthn:
if (!this.webAuthnSupported || this.webAuthn == null) {
break;
}

if (providerData.Challenge != null) {
setTimeout(() => {
this.u2f.init(JSON.parse(providerData.Challenge));
}, 500);
} else {
// TODO: Deprecated. Remove in future version.
const challenges = JSON.parse(providerData.Challenges);
if (challenges != null && challenges.length > 0) {
this.u2f.init({
appId: challenges[0].appId,
challenge: challenges[0].challenge,
keys: challenges.map((c: any) => {
return {
version: c.version,
keyHandle: c.keyHandle,
};
}),
});
}
}
setTimeout(() => {
this.webAuthn.init(providerData);
}, 500);
break;
case TwoFactorProviderType.Duo:
case TwoFactorProviderType.OrganizationDuo:
Expand Down Expand Up @@ -177,9 +159,9 @@ export class TwoFactorComponent implements OnInit, OnDestroy {
return;
}

if (this.selectedProviderType === TwoFactorProviderType.U2f) {
if (this.u2f != null) {
this.u2f.stop();
if (this.selectedProviderType === TwoFactorProviderType.WebAuthn) {
if (this.webAuthn != null) {
this.webAuthn.stop();
} else {
return;
}
Expand All @@ -189,33 +171,37 @@ export class TwoFactorComponent implements OnInit, OnDestroy {
}

try {
this.formPromise = this.authService.logInTwoFactor(this.selectedProviderType, this.token, this.remember);
const response: AuthResult = await this.formPromise;
const disableFavicon = await this.storageService.get<boolean>(ConstantsService.disableFaviconKey);
await this.stateService.save(ConstantsService.disableFaviconKey, !!disableFavicon);
if (this.onSuccessfulLogin != null) {
this.onSuccessfulLogin();
}
this.platformUtilsService.eventTrack('Logged In From Two-step');
if (response.resetMasterPassword) {
this.successRoute = 'set-password';
}
if (this.onSuccessfulLoginNavigate != null) {
this.onSuccessfulLoginNavigate();
} else {
this.router.navigate([this.successRoute], {
queryParams: {
identifier: this.identifier,
},
});
}
await this.doSubmit();
} catch {
if (this.selectedProviderType === TwoFactorProviderType.U2f && this.u2f != null) {
this.u2f.start();
if (this.selectedProviderType === TwoFactorProviderType.WebAuthn && this.webAuthn != null) {
this.webAuthn.start();
}
}
}

async doSubmit() {
this.formPromise = this.authService.logInTwoFactor(this.selectedProviderType, this.token, this.remember);
const response: AuthResult = await this.formPromise;
const disableFavicon = await this.storageService.get<boolean>(ConstantsService.disableFaviconKey);
await this.stateService.save(ConstantsService.disableFaviconKey, !!disableFavicon);
if (this.onSuccessfulLogin != null) {
this.onSuccessfulLogin();
}
this.platformUtilsService.eventTrack('Logged In From Two-step');
if (response.resetMasterPassword) {
this.successRoute = 'set-password';
}
if (this.onSuccessfulLoginNavigate != null) {
this.onSuccessfulLoginNavigate();
} else {
this.router.navigate([this.successRoute], {
queryParams: {
identifier: this.identifier,
},
});
}
}

async sendEmail(doToast: boolean) {
if (this.selectedProviderType !== TwoFactorProviderType.Email) {
return;
Expand All @@ -238,10 +224,10 @@ export class TwoFactorComponent implements OnInit, OnDestroy {
this.emailPromise = null;
}

private cleanupU2f() {
if (this.u2f != null) {
this.u2f.stop();
this.u2f.cleanup();
private cleanupWebAuthn() {
if (this.webAuthn != null) {
this.webAuthn.stop();
this.webAuthn.cleanup();
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/cli/services/cliPlatformUtils.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export class CliPlatformUtilsService implements PlatformUtilsService {
return this.packageJson.version;
}

supportsU2f(win: Window) {
supportsWebAuthn(win: Window) {
return false;
}

Expand Down
6 changes: 2 additions & 4 deletions src/electron/services/electronPlatformUtils.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,8 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService {
return remote.app.getVersion();
}

supportsU2f(win: Window): boolean {
// Not supported in Electron at this time.
// ref: https://github.com/electron/electron/issues/3226
return false;
supportsWebAuthn(win: Window): boolean {
return true;
}

supportsDuo(): boolean {
Expand Down
1 change: 1 addition & 0 deletions src/enums/twoFactorProviderType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export enum TwoFactorProviderType {
U2f = 4,
Remember = 5,
OrganizationDuo = 6,
WebAuthn = 7,
}
35 changes: 24 additions & 11 deletions src/misc/u2f.ts → src/misc/webauthn.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,37 @@
export class U2f {
import { I18nService } from '../abstractions/i18n.service';
import { PlatformUtilsService } from '../abstractions/platformUtils.service';

export class WebAuthn {
private iframe: HTMLIFrameElement = null;
private connectorLink: HTMLAnchorElement;
private parseFunction = this.parseMessage.bind(this);

constructor(private win: Window, private webVaultUrl: string, private successCallback: Function,
private errorCallback: Function, private infoCallback: Function) {
constructor(private win: Window, private webVaultUrl: string, private webAuthnNewTab: boolean,
private platformUtilsService: PlatformUtilsService, private i18nService: I18nService,
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';
const params = new URLSearchParams({
data: this.base64Encode(JSON.stringify(data)),
parent: encodeURIComponent(this.win.document.location.href),
btnText: encodeURIComponent(this.i18nService.t('webAuthnAuthenticate')),
v: '1',
});

this.iframe = this.win.document.getElementById('u2f_iframe') as HTMLIFrameElement;
this.iframe.src = this.connectorLink.href;
if (this.webAuthnNewTab) {
// Firefox fallback which opens the webauthn page in a new tab
params.append('locale', this.i18nService.translationLocale);
this.platformUtilsService.launchUri(`${this.webVaultUrl}/webauthn-fallback-connector.html?${params}`);
} else {
this.connectorLink.href = `${this.webVaultUrl}/webauthn-connector.html?${params}`;
this.iframe = this.win.document.getElementById('webauthn_iframe') as HTMLIFrameElement;
this.iframe.allow = 'publickey-credentials-get ' + new URL(this.webVaultUrl).origin;
this.iframe.src = this.connectorLink.href;

this.win.addEventListener('message', this.parseFunction, false);
this.win.addEventListener('message', this.parseFunction, false);
}
}

stop() {
Expand Down
7 changes: 0 additions & 7 deletions src/models/request/updateTwoFactorU2fRequest.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { PasswordVerificationRequest } from './passwordVerificationRequest';

export class UpdateTwoFactorU2fDeleteRequest extends PasswordVerificationRequest {
export class UpdateTwoFactorWebAuthnDeleteRequest extends PasswordVerificationRequest {
id: number;
}
7 changes: 7 additions & 0 deletions src/models/request/updateTwoFactorWebAuthnRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { PasswordVerificationRequest } from './passwordVerificationRequest';

export class UpdateTwoFactorWebAuthnRequest extends PasswordVerificationRequest {
deviceResponse: PublicKeyCredential;
name: string;
id: number;
}

0 comments on commit f20af0c

Please sign in to comment.