From e0f43860428d29b2d68488841fc9950d6c60ced1 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Wed, 17 Mar 2021 22:14:26 +0100 Subject: [PATCH] Add support for WebAuthn to browser extension (#1379) --- jslib | 2 +- src/_locales/en/messages.json | 14 ++++++--- src/background/main.background.ts | 17 ++++++++--- src/background/runtime.background.ts | 19 +++++++++++- src/browser/browserApi.ts | 4 +-- src/content/{sso.ts => message_handler.ts} | 9 ++++++ src/manifest.json | 2 +- src/popup/accounts/two-factor.component.html | 20 ++++++------- src/popup/accounts/two-factor.component.ts | 31 ++++++++++---------- src/popup/scss/misc.scss | 13 ++++++++ src/popup/services/services.module.ts | 11 ++----- src/services/browserPlatformUtils.service.ts | 8 ++--- webpack.config.js | 2 +- 13 files changed, 97 insertions(+), 55 deletions(-) rename src/content/{sso.ts => message_handler.ts} (56%) diff --git a/jslib b/jslib index f80e89465f..f20af0cd7c 160000 --- a/jslib +++ b/jslib @@ -1 +1 @@ -Subproject commit f80e89465ffc004705d2941301c0ffb6bfd71d1a +Subproject commit f20af0cd7c90adc07783950bed197b5d47892d6f diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index aca1797240..5c18dfebd0 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -798,6 +798,12 @@ "insertU2f": { "message": "Insert your security key into your computer's USB port. If it has a button, touch it." }, + "webAuthnNewTab": { + "message": "Continue the WebAuthn 2FA verification in the new tab." + }, + "webAuthnAuthenticate": { + "message": "Authenticate WebAutn" + }, "loginUnavailable": { "message": "Login Unavailable" }, @@ -837,11 +843,11 @@ "message": "Verify with Duo Security for your organization using the Duo Mobile app, SMS, phone call, or U2F security key.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, - "u2fDesc": { - "message": "Use any FIDO U2F enabled security key to access your account." + "webAuthnTitle": { + "message": "FIDO2 WebAuthn" }, - "u2fTitle": { - "message": "FIDO U2F Security Key" + "webAuthnDesc": { + "message": "Use any WebAuthn enabled security key to access your account." }, "emailTitle": { "message": "Email" diff --git a/src/background/main.background.ts b/src/background/main.background.ts index 7d251d7fad..382c4ba16c 100644 --- a/src/background/main.background.ts +++ b/src/background/main.background.ts @@ -179,9 +179,6 @@ export default class MainBackground { this.apiService = new ApiService(this.tokenService, this.platformUtilsService, (expired: boolean) => this.logout(expired)); this.userService = new UserService(this.tokenService, this.storageService); - this.authService = new AuthService(this.cryptoService, this.apiService, this.userService, - this.tokenService, this.appIdService, this.i18nService, this.platformUtilsService, - this.messagingService, this.vaultTimeoutService, this.consoleLogService); this.settingsService = new SettingsService(this.userService, this.storageService); this.cipherService = new CipherService(this.cryptoService, this.userService, this.settingsService, this.apiService, this.storageService, this.i18nService, () => this.searchService); @@ -246,7 +243,7 @@ export default class MainBackground { this.runtimeBackground = new RuntimeBackground(this, this.autofillService, this.cipherService, this.platformUtilsService as BrowserPlatformUtilsService, this.storageService, this.i18nService, this.analytics, this.notificationsService, this.systemService, this.vaultTimeoutService, - this.environmentService, this.policyService, this.userService); + this.environmentService, this.policyService, this.userService, this.messagingService); this.nativeMessagingBackground = new NativeMessagingBackground(this.storageService, this.cryptoService, this.cryptoFunctionService, this.vaultTimeoutService, this.runtimeBackground, this.i18nService, this.userService, this.messagingService, this.appIdService); this.commandsBackground = new CommandsBackground(this, this.passwordGenerationService, @@ -261,12 +258,24 @@ export default class MainBackground { this.webRequestBackground = new WebRequestBackground(this.platformUtilsService, this.cipherService, this.vaultTimeoutService); this.windowsBackground = new WindowsBackground(this); + + const that = this; + this.authService = new AuthService(this.cryptoService, this.apiService, this.userService, + this.tokenService, this.appIdService, this.i18nService, this.platformUtilsService, + new class extends MessagingServiceAbstraction { + // AuthService should send the messages to the background not popup. + send = (subscriber: string, arg: any = {}) => { + const message = Object.assign({}, { command: subscriber }, arg); + that.runtimeBackground.processMessage(message, that, null); + } + }(), this.vaultTimeoutService, this.consoleLogService); } async bootstrap() { this.analytics.ga('send', 'pageview', '/background.html'); this.containerService.attachToWindow(window); + (this.authService as AuthService).init(); await (this.vaultTimeoutService as VaultTimeoutService).init(true); await (this.i18nService as I18nService).init(); await (this.eventService as EventService).init(true); diff --git a/src/background/runtime.background.ts b/src/background/runtime.background.ts index 916c55e8cc..1bfdbd8b85 100644 --- a/src/background/runtime.background.ts +++ b/src/background/runtime.background.ts @@ -7,6 +7,7 @@ import { LoginView } from 'jslib/models/view/loginView'; import { CipherService } from 'jslib/abstractions/cipher.service'; import { EnvironmentService } from 'jslib/abstractions/environment.service'; import { I18nService } from 'jslib/abstractions/i18n.service'; +import { MessagingService } from 'jslib/abstractions/messaging.service'; import { NotificationsService } from 'jslib/abstractions/notifications.service'; import { PolicyService } from 'jslib/abstractions/policy.service'; import { StorageService } from 'jslib/abstractions/storage.service'; @@ -39,7 +40,7 @@ export default class RuntimeBackground { private analytics: Analytics, private notificationsService: NotificationsService, private systemService: SystemService, private vaultTimeoutService: VaultTimeoutService, private environmentService: EnvironmentService, private policyService: PolicyService, - private userService: UserService) { + private userService: UserService, private messagingService: MessagingService) { // onInstalled listener must be wired up before anything else, so we do it in the ctor chrome.runtime.onInstalled.addListener((details: any) => { @@ -176,6 +177,22 @@ export default class RuntimeBackground { } catch { } break; + case 'webAuthnResult': + let vaultUrl2 = this.environmentService.getWebVaultUrl(); + if (vaultUrl2 == null) { + vaultUrl2 = 'https://vault.bitwarden.com'; + } + + if (msg.referrer == null || Utils.getHostname(vaultUrl2) !== msg.referrer) { + return; + } + + const params = `webAuthnResponse=${encodeURIComponent(msg.data)};remember=${msg.remember}`; + BrowserApi.createNewTab(`popup/index.html?uilocation=popout#/2fa;${params}`, undefined, false); + break; + case 'reloadPopup': + this.messagingService.send('reloadPopup'); + break; default: break; } diff --git a/src/browser/browserApi.ts b/src/browser/browserApi.ts index 0993d79799..8f23c0911c 100644 --- a/src/browser/browserApi.ts +++ b/src/browser/browserApi.ts @@ -87,8 +87,8 @@ export class BrowserApi { return Promise.resolve(chrome.extension.getViews({ type: 'popup' }).length > 0); } - static createNewTab(url: string, extensionPage: boolean = false) { - chrome.tabs.create({ url: url }); + static createNewTab(url: string, extensionPage: boolean = false, active: boolean = true) { + chrome.tabs.create({ url: url, active: active }); } static messageListener(name: string, callback: (message: any, sender: any, response: any) => void) { diff --git a/src/content/sso.ts b/src/content/message_handler.ts similarity index 56% rename from src/content/sso.ts rename to src/content/message_handler.ts index 8c57d72de3..4358a97945 100644 --- a/src/content/sso.ts +++ b/src/content/message_handler.ts @@ -10,4 +10,13 @@ window.addEventListener('message', event => { referrer: event.source.location.hostname, }); } + + if (event.data.command && (event.data.command === 'webAuthnResult')) { + chrome.runtime.sendMessage({ + command: event.data.command, + data: event.data.data, + remember: event.data.remember, + referrer: event.source.location.hostname, + }); + } }, false); diff --git a/src/manifest.json b/src/manifest.json index 213fba3d03..e82913d5d5 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -44,7 +44,7 @@ { "all_frames": false, "js": [ - "content/sso.js" + "content/message_handler.js" ], "matches": [ "http://*/*", diff --git a/src/popup/accounts/two-factor.component.html b/src/popup/accounts/two-factor.component.html index 5b2ee5e333..db6518b817 100644 --- a/src/popup/accounts/two-factor.component.html +++ b/src/popup/accounts/two-factor.component.html @@ -9,7 +9,7 @@
@@ -59,15 +59,9 @@
- -
- -
-

{{'insertU2f' | i18n}}

- -
-
-
+ +
+
@@ -76,6 +70,11 @@
+ +
+

{{'webAuthnNewTab' | i18n}}

+
+
@@ -104,4 +103,3 @@
- diff --git a/src/popup/accounts/two-factor.component.ts b/src/popup/accounts/two-factor.component.ts index 7624d642f1..a6ac66e5bb 100644 --- a/src/popup/accounts/two-factor.component.ts +++ b/src/popup/accounts/two-factor.component.ts @@ -15,6 +15,7 @@ import { ApiService } from 'jslib/abstractions/api.service'; import { AuthService } from 'jslib/abstractions/auth.service'; import { EnvironmentService } from 'jslib/abstractions/environment.service'; import { I18nService } from 'jslib/abstractions/i18n.service'; +import { MessagingService } from 'jslib/abstractions/messaging.service'; import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service'; import { StateService } from 'jslib/abstractions/state.service'; import { StorageService } from 'jslib/abstractions/storage.service'; @@ -43,22 +44,31 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { environmentService: EnvironmentService, private ngZone: NgZone, private broadcasterService: BroadcasterService, private changeDetectorRef: ChangeDetectorRef, private popupUtilsService: PopupUtilsService, stateService: StateService, - storageService: StorageService, route: ActivatedRoute) { + storageService: StorageService, route: ActivatedRoute, private messagingService: MessagingService) { super(authService, router, i18nService, apiService, platformUtilsService, window, environmentService, stateService, storageService, route); super.onSuccessfulLogin = () => { return syncService.fullSync(true); }; super.successRoute = '/tabs/vault'; + this.webAuthnNewTab = this.platformUtilsService.isFirefox() || this.platformUtilsService.isSafari(); } async ngOnInit() { - const isFirefox = this.platformUtilsService.isFirefox(); - if (this.popupUtilsService.inPopup(window) && isFirefox && - this.win.navigator.userAgent.indexOf('Windows NT 10.0;') > -1) { - // ref: https://bugzilla.mozilla.org/show_bug.cgi?id=1562620 - this.initU2f = false; + if (this.route.snapshot.paramMap.has('webAuthnResponse')) { + // WebAuthn fallback response + this.selectedProviderType = TwoFactorProviderType.WebAuthn; + this.token = this.route.snapshot.paramMap.get('webAuthnResponse'); + super.onSuccessfulLogin = async () => { + this.syncService.fullSync(true); + this.messagingService.send('reloadPopup'); + window.close(); + }; + this.remember = this.route.snapshot.paramMap.get('remember') === 'true'; + await this.doSubmit(); + return; } + await super.ngOnInit(); if (this.selectedProviderType == null) { return; @@ -73,15 +83,6 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { } } - if (!this.initU2f && this.selectedProviderType === TwoFactorProviderType.U2f && - this.popupUtilsService.inPopup(window)) { - const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('popupU2fCloseMessage'), - null, this.i18nService.t('yes'), this.i18nService.t('no')); - if (confirmed) { - this.popupUtilsService.popOut(window); - } - } - const queryParamsSub = this.route.queryParams.subscribe(async qParams => { if (qParams.sso === 'true') { super.onSuccessfulLogin = () => { diff --git a/src/popup/scss/misc.scss b/src/popup/scss/misc.scss index 9212295394..49fc31ab73 100644 --- a/src/popup/scss/misc.scss +++ b/src/popup/scss/misc.scss @@ -176,6 +176,19 @@ p.lead { } } +#web-authn-frame { + background: url('../images/loading.svg') 0 0 no-repeat; + width: 100%; + height: 310px; + margin-bottom: -10px; + + iframe { + width: 100%; + height: 100%; + border: none; + } +} + app-root > #loading { display: flex; text-align: center; diff --git a/src/popup/services/services.module.ts b/src/popup/services/services.module.ts index 210b14a40b..cbd36ecd72 100644 --- a/src/popup/services/services.module.ts +++ b/src/popup/services/services.module.ts @@ -66,11 +66,6 @@ function getBgService(service: string) { export const stateService = new StateService(); export const messagingService = new BrowserMessagingService(); -export const authService = new AuthService(getBgService('cryptoService')(), - getBgService('apiService')(), getBgService('userService')(), - getBgService('tokenService')(), getBgService('appIdService')(), - getBgService('i18nService')(), getBgService('platformUtilsService')(), - messagingService, getBgService('vaultTimeoutService')(), getBgService('consoleLogService')()); export const searchService = new PopupSearchService(getBgService('searchService')(), getBgService('cipherService')(), getBgService('consoleLogService')()); @@ -86,7 +81,7 @@ export function initFactory(platformUtilsService: PlatformUtilsService, i18nServ window.document.body.classList.add('body-sm'); } - document.body.style.setProperty('height',`${window.innerHeight}px`,'important'); + document.body.style.setProperty('height', `${window.innerHeight}px`, 'important'); } if (BrowserApi.getBackgroundPage() != null) { @@ -108,8 +103,6 @@ export function initFactory(platformUtilsService: PlatformUtilsService, i18nServ window.document.documentElement.classList.add('locale_' + i18nService.translationLocale); window.document.documentElement.classList.add('theme_' + theme); - authService.init(); - const analytics = new Analytics(window, () => BrowserApi.gaFilter(), null, null, null, () => { const bgPage = BrowserApi.getBackgroundPage(); if (bgPage == null || bgPage.bitwardenMain == null) { @@ -133,7 +126,7 @@ export function initFactory(platformUtilsService: PlatformUtilsService, i18nServ PopupUtilsService, BroadcasterService, { provide: MessagingService, useValue: messagingService }, - { provide: AuthServiceAbstraction, useValue: authService }, + { provide: AuthServiceAbstraction, useFactory: getBgService('authService'), deps: [] }, { provide: StateServiceAbstraction, useValue: stateService }, { provide: SearchServiceAbstraction, useValue: searchService }, { provide: AuditService, useFactory: getBgService('auditService'), deps: [] }, diff --git a/src/services/browserPlatformUtils.service.ts b/src/services/browserPlatformUtils.service.ts index 6cfc322c46..b29aaff394 100644 --- a/src/services/browserPlatformUtils.service.ts +++ b/src/services/browserPlatformUtils.service.ts @@ -126,12 +126,8 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService return BrowserApi.getApplicationVersion(); } - supportsU2f(win: Window): boolean { - if (win != null && (win as any).u2f != null) { - return true; - } - - return this.isChrome() || this.isOpera() || this.isVivaldi() || this.isEdge(); + supportsWebAuthn(win: Window): boolean { + return (typeof(PublicKeyCredential) !== 'undefined'); } supportsDuo(): boolean { diff --git a/webpack.config.js b/webpack.config.js index 801f843618..e9d8eaec28 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -137,7 +137,7 @@ const config = { 'content/autofiller': './src/content/autofiller.ts', 'content/notificationBar': './src/content/notificationBar.ts', 'content/shortcuts': './src/content/shortcuts.ts', - 'content/sso': './src/content/sso.ts', + 'content/message_handler': './src/content/message_handler.ts', 'notification/bar': './src/notification/bar.js', }, optimization: {