Search code examples
javascriptangularoauth-2.0electronopenid-connect

Electron - how to get an auth token from BrowserWindow to the main Electron App


I have an Angular Electron app that uses a BrowserWindow to log in using a third-party OpenID Connect Identity Provider. Additionally, I have my own Backend that implements the OpenID Connect standard. The backend runs under localhost:5000.

Package versions:

Angular: 11.1.0
electron: 9.1.0
ngx-electron: 2.2.0

The Flow is like this:

  • Electron opens a BrowserWindow with localhost:5000/connect/authorize as URL (including required query parameters)
  • Backend redirects to third-party Identity Provider
  • User logs in
  • Third Party redirects to Backend with auth info
  • Backend redirects to the provided returnUri (Frontend callback)
  • BrowserWindow calls the callback function and has the auth token in the URL (as expected)

--> How do I get the auth token from the BrowserWindow to the main Electron app? Actually, I need the callback function to be called in the main Electron app

Login logic, that is called in the main Electron app:

public login() {
  // Everything in here is called in the main Electron app

  const authWindow = new this.electron.remote.BrowserWindow({
      width: 800, 
      height: 600, 
      show: false, 
      webPreferences: {
        nodeIntegration: false,
        webSecurity: false
      }
    });

    authWindow.loadURL(myAuthurl);
    authWindow.show();
    authWindow.webContents.openDevTools();

    const defaultSession = this.electron.remote.session.defaultSession;

    const { session: { webRequest } } = authWindow.webContents; 

    webRequest.onBeforeRequest(null, async request => {
      console.log(request);
    });

    webRequest.onBeforeRedirect(null, async request => {
      console.log(request); // Callback function never called

    });

    defaultSession.webRequest.onBeforeRequest(null, async request => {
      console.log(request); // Callback function never called
    });

    defaultSession.webRequest.onBeforeRequest(null, request => {
      console.log(request); // Callback function never called
    });


    defaultSession.webRequest.onBeforeRedirect(null, request => {
      console.log(request); // Callback function never called
    });

    authWindow.on('closed', (event) => {
      console.log(event); // Callback function called when the window is closed but with no data
    });
}

Callback Logic, called in BrowserWindow (But it's the same Angular app):

// this method is called after successful log in
// here I'm still in the BrowserWindow

public callback()
  // here's the data I need. How do I "send" this data to the main Electron app?
  const hash = window.location.hash;
} 

Edit: I tried ipcRenderer, but the on callback is never triggered:

// executed in the main Electron app
const ipc = this.electron.ipcRenderer;

ipc.on('authtoken', (event, arg) => {
  console.log(arg);
})

ipc.send('authtoken', 'DATA');

Any ideas about what I'm missing here? Are there better approaches?


Solution

  • It's important to understand Electron's process model, as it can be quite complicated and challenging to get right upon first use. Thus, I recommend reading Electron's process model documentation as well as the official IPC tutorial for communicating between the processes.

    In your case, you'll have to send the token from the Renderer process to the Main process where you store it (or do whatever you want to with it). This is done using ipcRenderer.send() in the BrowserWindow and ipcMain.on() in the Main process.

    // BrowserWindow doing the authentication stuff
    const { ipcRenderer } = require ("electron");
    
    ipcRenderer.send ("authtoken", "DATA");
    

    And the counterpart in the Main process:

    // Where your imports are
    const { ipcMain } = require ("electron");
    
    // ...
    
    ipcMain.on ("authtoken", (event, arg) => {
        // arg will be the data you sent from the auth window
    });
    

    In case you open the authentication window from within the Main process, I suggest storing a reference to it, say let authwindow = new BrowserWindow(...) so you could close it right away upon receiving the token in the Main process using authwindow.destroy().

    However, if you don't do this but instead open the window from another BrowserWindow, you can still send an event to that first window, given that you have a reference to it in the Main process. For example:

    // Main process, where you create the initial window
    // Where your imports are
    const { ipcMain } = require ("electron");
    
    // ...
    let mainWindow = new BrowserWindow (/* ... */);
    
    ipcMain.on ("authtoken", (event, arg) => {
        // arg will be the data you sent from the auth window
        mainWindow.webContents.send ("close-auth");
    });
    

    And receive this event in the main window and close the auth window:

    // Main window
    const { ipcRenderer } = require ("electron");
    
    public login() {
      // Everything in here is called in the main Electron app
    
      const authWindow = new this.electron.remote.BrowserWindow({
          width: 800, 
          height: 600, 
          show: false, 
          webPreferences: {
            nodeIntegration: false,
            webSecurity: false
          }
        });
    
        authWindow.loadURL(myAuthurl);
        authWindow.show();
        authWindow.webContents.openDevTools();
    
        const defaultSession = this.electron.remote.session.defaultSession;
    
        ipcRenderer.once ("close-auth", (event, arg) => { authWindow.destroy (); });
    }
    

    (#once() will cause subsequent signals to be ignored if the callback function already ran once. If login() is called again, this will be registered again and work as expected, but if we had used #on() we would grow the number of registered listeners with every function call. #once() is also available on ipcMain but shouldn't be needed often.)

    One last quick note: remote is deprecated and thus I suggest to open the authentication window from the Main process, removing remote and do all the stuff that would have required remote from the Main process, communicating with the different BrowserWindows via IPC like I showed you here.

    Also, I suggest not to rename Electron packages upon import; if you call the IPC package ipc in all process contexts, you will be confused sooner or later. If you keep ipcMain and ipcRenderer it should be immediately clear in which kind of process you are, should you choose to refactor your code later on.