@@ -13,11 +13,7 @@ import {
1313} from 'electron' ;
1414
1515import { initialize } from 'electron-react-titlebar/main' ;
16- import {
17- getAuthenticatorManager ,
18- setupWebAuthn ,
19- webauthnPageScript ,
20- } from 'electron-webauthn-linux' ;
16+ import { setupWebAuthn } from 'electron-webauthn-linux' ;
2117import windowStateKeeper from 'electron-window-state' ;
2218import { emptyDirSync , ensureFileSync } from 'fs-extra' ;
2319import minimist from 'minimist' ;
@@ -261,194 +257,6 @@ const createWindow = () => {
261257 } ) ;
262258
263259 app . on ( 'web-contents-created' , ( _e , contents ) => {
264- // Inject WebAuthn page script into ALL content types on Linux via CDP.
265- // This covers both service webviews AND popup windows (e.g. Google passkey settings).
266- // Page.addScriptToEvaluateOnNewDocument runs BEFORE page scripts,
267- // in the main world — the only reliable way to monkey-patch
268- // navigator.credentials before sites like Google check it.
269- if ( isLinux ) {
270- contents . on ( 'console-message' , ( _event , _level , message ) => {
271- if (
272- message . includes ( 'webauthn' ) ||
273- message . includes ( 'WebAuthn' ) ||
274- message . includes ( 'DIAG' )
275- ) {
276- debug ( 'WebView console:' , message ) ;
277- }
278- } ) ;
279-
280- try {
281- contents . debugger . attach ( '1.3' ) ;
282- const contentType = contents . getType ( ) ;
283- debug (
284- `Debugger attached to ${ contentType } , injecting WebAuthn page script` ,
285- ) ;
286-
287- // Enable Page domain so addScriptToEvaluateOnNewDocument persists across navigations
288- contents . debugger . sendCommand ( 'Page.enable' , { } ) . catch ( error => {
289- debug ( 'CDP Page.enable failed:' , error ) ;
290- } ) ;
291-
292- // Inject page script before any page JS runs
293- contents . debugger
294- . sendCommand ( 'Page.addScriptToEvaluateOnNewDocument' , {
295- source : webauthnPageScript ,
296- worldName : '' ,
297- runImmediately : true ,
298- } )
299- . then ( result => {
300- debug ( 'WebAuthn page script registered via CDP:' , result ) ;
301- } )
302- . catch ( error => {
303- debug ( 'CDP addScriptToEvaluateOnNewDocument failed:' , error ) ;
304- } ) ;
305-
306- // Set up Runtime.addBinding for popup windows that lack a preload script.
307- // This creates a __webauthnBridge function in the page context that triggers
308- // Runtime.bindingCalled events in our debugger listener.
309- contents . debugger
310- . sendCommand ( 'Runtime.addBinding' , {
311- name : '__webauthnBridge' ,
312- } )
313- . catch ( error => {
314- debug ( 'CDP Runtime.addBinding failed:' , error ) ;
315- } ) ;
316-
317- contents . debugger . sendCommand ( 'Runtime.enable' , { } ) . catch ( error => {
318- debug ( 'CDP Runtime.enable failed:' , error ) ;
319- } ) ;
320-
321- // Handle CDP binding calls from popup windows
322- contents . debugger . on ( 'message' , ( _event , method , params ) => {
323- if (
324- method === 'Runtime.bindingCalled' &&
325- params . name === '__webauthnBridge'
326- ) {
327- const manager = getAuthenticatorManager ( ) ;
328- if ( ! manager ) {
329- debug ( 'CDP bridge: manager not available' ) ;
330- return ;
331- }
332- try {
333- const {
334- id,
335- method : webauthnMethod ,
336- arg,
337- } = JSON . parse ( params . payload ) ;
338- const ctxId = params . executionContextId ;
339- debug (
340- `CDP bridge call: method=${ webauthnMethod } id=${ id } ctxId=${ ctxId } ` ,
341- ) ;
342-
343- const sendResponse = ( error : string | null , result : any ) => {
344- debug (
345- `CDP bridge response: id=${ id } error=${ error } hasResult=${ result != null } ` ,
346- ) ;
347- const response = JSON . stringify ( { id, error, result } ) ;
348- contents . debugger
349- . sendCommand ( 'Runtime.evaluate' , {
350- expression : `window.__webauthnCallback(${ JSON . stringify ( response ) } )` ,
351- contextId : ctxId ,
352- } )
353- . then ( ( ) => {
354- debug ( `CDP bridge callback delivered: id=${ id } ` ) ;
355- } )
356- . catch ( error_ => {
357- debug ( 'CDP Runtime.evaluate callback failed:' , error_ ) ;
358- } ) ;
359- } ;
360-
361- switch ( webauthnMethod ) {
362- case 'hasCredentials' : {
363- manager
364- . getAvailableBackendsForGet ( arg )
365- . then ( backends => {
366- debug (
367- `CDP bridge hasCredentials(${ arg } ): ${ backends . length } backends` ,
368- ) ;
369- sendResponse ( null , backends . length > 0 ) ;
370- } )
371- . catch ( error => {
372- debug (
373- `CDP bridge hasCredentials error: ${ error . message } ` ,
374- ) ;
375- sendResponse ( error . message , null ) ;
376- } ) ;
377-
378- break ;
379- }
380- case 'create' : {
381- manager
382- . getAvailableBackendsForCreate ( )
383- . then ( async backends => {
384- debug (
385- `CDP bridge create: ${ backends . length } backends available` ,
386- ) ;
387- if ( backends . length === 0 )
388- throw new Error ( 'No WebAuthn authenticator available' ) ;
389- const result = await manager . createCredential (
390- arg ,
391- backends [ 0 ] ,
392- ) ;
393- debug (
394- 'CDP bridge create: credential created successfully' ,
395- ) ;
396- sendResponse ( null , result ) ;
397- } )
398- . catch ( error => {
399- debug ( `CDP bridge create error: ${ error . message } ` ) ;
400- sendResponse ( error . message , null ) ;
401- } ) ;
402-
403- break ;
404- }
405- case 'get' : {
406- const rpId = arg . rpId || '' ;
407- debug (
408- `CDP bridge get: rpId=${ rpId } allowCredentials=${ arg . allowCredentials ?. length || 0 } ` ,
409- ) ;
410- manager
411- . getAvailableBackendsForGet (
412- rpId ,
413- arg . allowCredentials ?. map ( ( c : any ) => c . id ) ,
414- )
415- . then ( async backends => {
416- debug (
417- `CDP bridge get: ${ backends . length } matching backends` ,
418- ) ;
419- if ( backends . length === 0 )
420- throw new Error (
421- 'NoCredentials: No passkeys found for this site.' ,
422- ) ;
423- const result = await manager . getAssertion (
424- arg ,
425- backends [ 0 ] ,
426- ) ;
427- debug ( 'CDP bridge get: assertion retrieved successfully' ) ;
428- sendResponse ( null , result ) ;
429- } )
430- . catch ( error => {
431- debug ( `CDP bridge get error: ${ error . message } ` ) ;
432- sendResponse ( error . message , null ) ;
433- } ) ;
434-
435- break ;
436- }
437- default : {
438- debug ( `CDP bridge: unknown method '${ webauthnMethod } '` ) ;
439- break ;
440- }
441- }
442- } catch ( error : any ) {
443- debug ( 'CDP bridge call parse error:' , error . message ) ;
444- }
445- }
446- } ) ;
447- } catch ( error ) {
448- debug ( 'Failed to attach debugger for WebAuthn injection:' , error ) ;
449- }
450- }
451-
452260 if ( contents . getType ( ) === 'webview' ) {
453261 enableWebContents ( contents ) ;
454262
@@ -514,6 +322,24 @@ const createWindow = () => {
514322 const popupDomain = getDomain ( popupHost ) ;
515323 const currentDomain = getDomain ( currentHost ) ;
516324 if ( popupDomain && currentDomain && popupDomain === currentDomain ) {
325+ // On Linux, give popup windows a WebAuthn preload so passkey
326+ // flows work in auth popups (they don't get the recipe preload).
327+ if ( isLinux ) {
328+ return {
329+ action : 'allow' ,
330+ overrideBrowserWindowOptions : {
331+ webPreferences : {
332+ preload : join (
333+ __dirname ,
334+ 'webview' ,
335+ 'webauthn-popup-preload.js' ,
336+ ) ,
337+ contextIsolation : true ,
338+ sandbox : true ,
339+ } ,
340+ } ,
341+ } ;
342+ }
517343 return { action : 'allow' } ;
518344 }
519345 } catch {
0 commit comments