Add support for WebAuthn to browser extension (#1379)

This commit is contained in:
Oscar Hinton 2021-03-17 22:14:26 +01:00 committed by GitHub
parent 24d172e3b9
commit e0f4386042
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 97 additions and 55 deletions

2
jslib

@ -1 +1 @@
Subproject commit f80e89465ffc004705d2941301c0ffb6bfd71d1a Subproject commit f20af0cd7c90adc07783950bed197b5d47892d6f

View File

@ -798,6 +798,12 @@
"insertU2f": { "insertU2f": {
"message": "Insert your security key into your computer's USB port. If it has a button, touch it." "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": { "loginUnavailable": {
"message": "Login Unavailable" "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.", "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." "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated."
}, },
"u2fDesc": { "webAuthnTitle": {
"message": "Use any FIDO U2F enabled security key to access your account." "message": "FIDO2 WebAuthn"
}, },
"u2fTitle": { "webAuthnDesc": {
"message": "FIDO U2F Security Key" "message": "Use any WebAuthn enabled security key to access your account."
}, },
"emailTitle": { "emailTitle": {
"message": "Email" "message": "Email"

View File

@ -179,9 +179,6 @@ export default class MainBackground {
this.apiService = new ApiService(this.tokenService, this.platformUtilsService, this.apiService = new ApiService(this.tokenService, this.platformUtilsService,
(expired: boolean) => this.logout(expired)); (expired: boolean) => this.logout(expired));
this.userService = new UserService(this.tokenService, this.storageService); 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.settingsService = new SettingsService(this.userService, this.storageService);
this.cipherService = new CipherService(this.cryptoService, this.userService, this.settingsService, this.cipherService = new CipherService(this.cryptoService, this.userService, this.settingsService,
this.apiService, this.storageService, this.i18nService, () => this.searchService); 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.runtimeBackground = new RuntimeBackground(this, this.autofillService, this.cipherService,
this.platformUtilsService as BrowserPlatformUtilsService, this.storageService, this.i18nService, this.platformUtilsService as BrowserPlatformUtilsService, this.storageService, this.i18nService,
this.analytics, this.notificationsService, this.systemService, this.vaultTimeoutService, 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.nativeMessagingBackground = new NativeMessagingBackground(this.storageService, this.cryptoService, this.cryptoFunctionService,
this.vaultTimeoutService, this.runtimeBackground, this.i18nService, this.userService, this.messagingService, this.appIdService); this.vaultTimeoutService, this.runtimeBackground, this.i18nService, this.userService, this.messagingService, this.appIdService);
this.commandsBackground = new CommandsBackground(this, this.passwordGenerationService, this.commandsBackground = new CommandsBackground(this, this.passwordGenerationService,
@ -261,12 +258,24 @@ export default class MainBackground {
this.webRequestBackground = new WebRequestBackground(this.platformUtilsService, this.cipherService, this.webRequestBackground = new WebRequestBackground(this.platformUtilsService, this.cipherService,
this.vaultTimeoutService); this.vaultTimeoutService);
this.windowsBackground = new WindowsBackground(this); 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() { async bootstrap() {
this.analytics.ga('send', 'pageview', '/background.html'); this.analytics.ga('send', 'pageview', '/background.html');
this.containerService.attachToWindow(window); this.containerService.attachToWindow(window);
(this.authService as AuthService).init();
await (this.vaultTimeoutService as VaultTimeoutService).init(true); await (this.vaultTimeoutService as VaultTimeoutService).init(true);
await (this.i18nService as I18nService).init(); await (this.i18nService as I18nService).init();
await (this.eventService as EventService).init(true); await (this.eventService as EventService).init(true);

View File

@ -7,6 +7,7 @@ import { LoginView } from 'jslib/models/view/loginView';
import { CipherService } from 'jslib/abstractions/cipher.service'; import { CipherService } from 'jslib/abstractions/cipher.service';
import { EnvironmentService } from 'jslib/abstractions/environment.service'; import { EnvironmentService } from 'jslib/abstractions/environment.service';
import { I18nService } from 'jslib/abstractions/i18n.service'; import { I18nService } from 'jslib/abstractions/i18n.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { NotificationsService } from 'jslib/abstractions/notifications.service'; import { NotificationsService } from 'jslib/abstractions/notifications.service';
import { PolicyService } from 'jslib/abstractions/policy.service'; import { PolicyService } from 'jslib/abstractions/policy.service';
import { StorageService } from 'jslib/abstractions/storage.service'; import { StorageService } from 'jslib/abstractions/storage.service';
@ -39,7 +40,7 @@ export default class RuntimeBackground {
private analytics: Analytics, private notificationsService: NotificationsService, private analytics: Analytics, private notificationsService: NotificationsService,
private systemService: SystemService, private vaultTimeoutService: VaultTimeoutService, private systemService: SystemService, private vaultTimeoutService: VaultTimeoutService,
private environmentService: EnvironmentService, private policyService: PolicyService, 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 // onInstalled listener must be wired up before anything else, so we do it in the ctor
chrome.runtime.onInstalled.addListener((details: any) => { chrome.runtime.onInstalled.addListener((details: any) => {
@ -176,6 +177,22 @@ export default class RuntimeBackground {
} }
catch { } catch { }
break; 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: default:
break; break;
} }

View File

@ -87,8 +87,8 @@ export class BrowserApi {
return Promise.resolve(chrome.extension.getViews({ type: 'popup' }).length > 0); return Promise.resolve(chrome.extension.getViews({ type: 'popup' }).length > 0);
} }
static createNewTab(url: string, extensionPage: boolean = false) { static createNewTab(url: string, extensionPage: boolean = false, active: boolean = true) {
chrome.tabs.create({ url: url }); chrome.tabs.create({ url: url, active: active });
} }
static messageListener(name: string, callback: (message: any, sender: any, response: any) => void) { static messageListener(name: string, callback: (message: any, sender: any, response: any) => void) {

View File

@ -10,4 +10,13 @@ window.addEventListener('message', event => {
referrer: event.source.location.hostname, 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); }, false);

View File

@ -44,7 +44,7 @@
{ {
"all_frames": false, "all_frames": false,
"js": [ "js": [
"content/sso.js" "content/message_handler.js"
], ],
"matches": [ "matches": [
"http://*/*", "http://*/*",

View File

@ -9,7 +9,7 @@
<div class="right"> <div class="right">
<button type="submit" appBlurClick [disabled]="form.loading" *ngIf="selectedProviderType != null && selectedProviderType !== providerType.Duo && <button type="submit" appBlurClick [disabled]="form.loading" *ngIf="selectedProviderType != null && selectedProviderType !== providerType.Duo &&
selectedProviderType !== providerType.OrganizationDuo && selectedProviderType !== providerType.OrganizationDuo &&
(selectedProviderType !== providerType.U2f || form.loading)"> (selectedProviderType !== providerType.WebAuthn || form.loading)">
<span [hidden]="form.loading">{{'continue' | i18n}}</span> <span [hidden]="form.loading">{{'continue' | i18n}}</span>
<i class="fa fa-spinner fa-lg fa-spin" [hidden]="!form.loading" aria-hidden="true"></i> <i class="fa fa-spinner fa-lg fa-spin" [hidden]="!form.loading" aria-hidden="true"></i>
</button> </button>
@ -59,15 +59,9 @@
</div> </div>
</div> </div>
</ng-container> </ng-container>
<ng-container *ngIf="selectedProviderType === providerType.U2f"> <ng-container *ngIf="selectedProviderType === providerType.WebAuthn && !webAuthnNewTab">
<div class="content text-center"> <div id="web-authn-frame"><iframe id="webauthn_iframe"></iframe></div>
<span *ngIf="!u2fReady" class="text-center"><i class="fa fa-spinner fa-spin"></i></span> <div class="box">
<div *ngIf="u2fReady">
<p>{{'insertU2f' | i18n}}</p>
<img src="../images/u2fkey.jpg" alt="" class="img-rounded img-responsive" />
</div>
</div>
<div class="box first">
<div class="box-content"> <div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow> <div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="remember">{{'rememberMe' | i18n}}</label> <label for="remember">{{'rememberMe' | i18n}}</label>
@ -76,6 +70,11 @@
</div> </div>
</div> </div>
</ng-container> </ng-container>
<ng-container *ngIf="selectedProviderType === providerType.WebAuthn && webAuthnNewTab">
<div class="content text-center" *ngIf="webAuthnNewTab">
<p class="text-center">{{'webAuthnNewTab' | i18n}}</p>
</div>
</ng-container>
<ng-container *ngIf="selectedProviderType === providerType.Duo || <ng-container *ngIf="selectedProviderType === providerType.Duo ||
selectedProviderType === providerType.OrganizationDuo"> selectedProviderType === providerType.OrganizationDuo">
<div id="duo-frame"><iframe id="duo_iframe"></iframe></div> <div id="duo-frame"><iframe id="duo_iframe"></iframe></div>
@ -104,4 +103,3 @@
</div> </div>
</content> </content>
</form> </form>
<iframe id="u2f_iframe" hidden></iframe>

View File

@ -15,6 +15,7 @@ import { ApiService } from 'jslib/abstractions/api.service';
import { AuthService } from 'jslib/abstractions/auth.service'; import { AuthService } from 'jslib/abstractions/auth.service';
import { EnvironmentService } from 'jslib/abstractions/environment.service'; import { EnvironmentService } from 'jslib/abstractions/environment.service';
import { I18nService } from 'jslib/abstractions/i18n.service'; import { I18nService } from 'jslib/abstractions/i18n.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service'; import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { StateService } from 'jslib/abstractions/state.service'; import { StateService } from 'jslib/abstractions/state.service';
import { StorageService } from 'jslib/abstractions/storage.service'; import { StorageService } from 'jslib/abstractions/storage.service';
@ -43,22 +44,31 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
environmentService: EnvironmentService, private ngZone: NgZone, environmentService: EnvironmentService, private ngZone: NgZone,
private broadcasterService: BroadcasterService, private changeDetectorRef: ChangeDetectorRef, private broadcasterService: BroadcasterService, private changeDetectorRef: ChangeDetectorRef,
private popupUtilsService: PopupUtilsService, stateService: StateService, private popupUtilsService: PopupUtilsService, stateService: StateService,
storageService: StorageService, route: ActivatedRoute) { storageService: StorageService, route: ActivatedRoute, private messagingService: MessagingService) {
super(authService, router, i18nService, apiService, platformUtilsService, window, environmentService, super(authService, router, i18nService, apiService, platformUtilsService, window, environmentService,
stateService, storageService, route); stateService, storageService, route);
super.onSuccessfulLogin = () => { super.onSuccessfulLogin = () => {
return syncService.fullSync(true); return syncService.fullSync(true);
}; };
super.successRoute = '/tabs/vault'; super.successRoute = '/tabs/vault';
this.webAuthnNewTab = this.platformUtilsService.isFirefox() || this.platformUtilsService.isSafari();
} }
async ngOnInit() { async ngOnInit() {
const isFirefox = this.platformUtilsService.isFirefox(); if (this.route.snapshot.paramMap.has('webAuthnResponse')) {
if (this.popupUtilsService.inPopup(window) && isFirefox && // WebAuthn fallback response
this.win.navigator.userAgent.indexOf('Windows NT 10.0;') > -1) { this.selectedProviderType = TwoFactorProviderType.WebAuthn;
// ref: https://bugzilla.mozilla.org/show_bug.cgi?id=1562620 this.token = this.route.snapshot.paramMap.get('webAuthnResponse');
this.initU2f = false; 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(); await super.ngOnInit();
if (this.selectedProviderType == null) { if (this.selectedProviderType == null) {
return; 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 => { const queryParamsSub = this.route.queryParams.subscribe(async qParams => {
if (qParams.sso === 'true') { if (qParams.sso === 'true') {
super.onSuccessfulLogin = () => { super.onSuccessfulLogin = () => {

View File

@ -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 { app-root > #loading {
display: flex; display: flex;
text-align: center; text-align: center;

View File

@ -66,11 +66,6 @@ function getBgService<T>(service: string) {
export const stateService = new StateService(); export const stateService = new StateService();
export const messagingService = new BrowserMessagingService(); export const messagingService = new BrowserMessagingService();
export const authService = new AuthService(getBgService<CryptoService>('cryptoService')(),
getBgService<ApiService>('apiService')(), getBgService<UserService>('userService')(),
getBgService<TokenService>('tokenService')(), getBgService<AppIdService>('appIdService')(),
getBgService<I18nService>('i18nService')(), getBgService<PlatformUtilsService>('platformUtilsService')(),
messagingService, getBgService<VaultTimeoutService>('vaultTimeoutService')(), getBgService<ConsoleLogService>('consoleLogService')());
export const searchService = new PopupSearchService(getBgService<SearchService>('searchService')(), export const searchService = new PopupSearchService(getBgService<SearchService>('searchService')(),
getBgService<CipherService>('cipherService')(), getBgService<ConsoleLogService>('consoleLogService')()); getBgService<CipherService>('cipherService')(), getBgService<ConsoleLogService>('consoleLogService')());
@ -86,7 +81,7 @@ export function initFactory(platformUtilsService: PlatformUtilsService, i18nServ
window.document.body.classList.add('body-sm'); 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) { 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('locale_' + i18nService.translationLocale);
window.document.documentElement.classList.add('theme_' + theme); window.document.documentElement.classList.add('theme_' + theme);
authService.init();
const analytics = new Analytics(window, () => BrowserApi.gaFilter(), null, null, null, () => { const analytics = new Analytics(window, () => BrowserApi.gaFilter(), null, null, null, () => {
const bgPage = BrowserApi.getBackgroundPage(); const bgPage = BrowserApi.getBackgroundPage();
if (bgPage == null || bgPage.bitwardenMain == null) { if (bgPage == null || bgPage.bitwardenMain == null) {
@ -133,7 +126,7 @@ export function initFactory(platformUtilsService: PlatformUtilsService, i18nServ
PopupUtilsService, PopupUtilsService,
BroadcasterService, BroadcasterService,
{ provide: MessagingService, useValue: messagingService }, { provide: MessagingService, useValue: messagingService },
{ provide: AuthServiceAbstraction, useValue: authService }, { provide: AuthServiceAbstraction, useFactory: getBgService<AuthService>('authService'), deps: [] },
{ provide: StateServiceAbstraction, useValue: stateService }, { provide: StateServiceAbstraction, useValue: stateService },
{ provide: SearchServiceAbstraction, useValue: searchService }, { provide: SearchServiceAbstraction, useValue: searchService },
{ provide: AuditService, useFactory: getBgService<AuditService>('auditService'), deps: [] }, { provide: AuditService, useFactory: getBgService<AuditService>('auditService'), deps: [] },

View File

@ -126,12 +126,8 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService
return BrowserApi.getApplicationVersion(); return BrowserApi.getApplicationVersion();
} }
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.isOpera() || this.isVivaldi() || this.isEdge();
} }
supportsDuo(): boolean { supportsDuo(): boolean {

View File

@ -137,7 +137,7 @@ const config = {
'content/autofiller': './src/content/autofiller.ts', 'content/autofiller': './src/content/autofiller.ts',
'content/notificationBar': './src/content/notificationBar.ts', 'content/notificationBar': './src/content/notificationBar.ts',
'content/shortcuts': './src/content/shortcuts.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', 'notification/bar': './src/notification/bar.js',
}, },
optimization: { optimization: {