TL;DR: In this article, you will learn how to secure Electron applications with OpenID Connect and OAuth 2.0. You don't need to be an expert in any of these technologies to follow this article along because the instructions will guide you through the whole thing. However, if you want to learn about OpenID Connect, you can check this documentation and, if you want to learn about OAuth, you can check this one. For the reference implementation of the solution described here, you can check this GitHub repository.

"Learn how to secure your @electronjs applications with @openid Connect and OAuth 2.0"

Prerequisites

To follow this article along without trouble, you are expected to have some basic skills with Node.js and JavaScript. JavaScript is mandatory but, if you don't know Node.js, you might still be able to move through the article.

However, you do have to have Node.js and NPM properly installed in your machine. Also, some familiarity with Shell, Bash, and alike is desirable. If you are on Windows, PowerShell might be the solution to your needs.

What Will you Build

To demonstrate how to secure Electron apps with OpenID Connect and OAuth 2.0, you are going to put together two projects. The first one is, as you might expect, an Electron application. The second one is a (very) basic RESTful API that you will use Node.js and Express to run. The backend will contain a single endpoint (/private) that will represent an API that only performs its job if the incoming requests have access tokens. If these requests do have valid access tokens, this endpoint will respond with a static message saying "Only authenticated users can read this message".

Although this backend API is basic, this single endpoint is enough to demonstrate how you would secure your real-world APIs (e.g., RESTful resources). That is, if you were developing a to-do list, a shopping cart feature, or anything like that, the approach would be the same.

"An Access Token is a string representing granted permissions." - OAuth 2.0 documentation @ Auth0

The flow of the application will be the following:

  1. Your users will start the Electron application.
  2. As they were not signed in before, you Electron app will show them a login screen (a page provided by the authorization server).
  3. After authenticating, the authorization server will provide your Electron app a code.
  4. The Electron app will issue a request to a token endpoint to exchange this code for an access token and an id token.
  5. After getting back these tokens, the Electron app will show a secured route with where users will be able to click a button to fetch the static message from the /private endpoint on the API.

Although simple, this workflow shows the whole picture of how you will have to integrate and secure your backend APIs and Electron apps while using OAuth 2.0 and OpenID Connect. The following screenshot shows what the app will look like when ready.

Electron application secured with OpenID Connect and OAuth 2.0

Note: The flow explained above is also known as the Authorization Code Grant Flow and it is part of OAuth 2.0.

Electron Overview

Basically speaking, Electron is a platform that enables developers to use Node.js to build native applications for desktops. If your desktop is running Windows, Linux, or MacOS, then it will probably be able to run an Electron application. That is, virtually, the whole world (when it comes to desktops) can run Electron apps.

In case you are interested in some history, the Electron platform was created by GitHub to support Atom, their open-source editor for desktops. However, nowadays, Electron is used by different sizes of companies to build a wide variety of different applications. For example, Slack, Skype, and Visual Studio Code are apps that leverage Electron.

In the following subsections, you will broadly learn about two different processes that make the foundation of Electron apps: the Main process and the Renderer processes. If you already know about both, feel free to jump to the next section. Otherwise, read the quick explanation as it matters for the rest of the article.

Electron Main Process

In Electron, all applications start with the Main process which, by the way, is unique to each application. What makes this process special is that it is the only process that can call the native APIs available in Electron.

For example, you can only achieve tasks like reading a file from a computer and creating browser windows through the Main process. Never directly by Renderer processes and the scripts running on it.

"The @electronjs Main process can help you secure sensitive information from the web page being showed."

Electron Renderer Processes

On the other hand, the Renderer processes are processes triggered by the type above to show and handle the execution of your browser windows. To block access to native/sensitive APIs (like the ones mentioned before), you can specify that your Renderer processes are not integrated into your Main process. For that, you have to set nodeIntegration: false while creating Renderer processes (e.g., instances of BrowserWindow).

That is, making your Renderer processes detached from the Main process, you can rest assured that the remote resources that they load won't damage your users' computers. As such, you can use the Main process to keep sensitive data away from third-party scripts (e.g., scripts downloaded from the web).

Auth0 Introduction

As the goal of this article is to show you how to secure an Electron application with OpenID Connect and OAuth 2.0, you will need an Identity and Access Management system that adheres to these standards. Auth0, a global leader in Identity-as-a-Service (IDaaS), provides thousands of enterprise customers with a Universal Identity Platform that is certified on the OpenID Connect standard. Also, Auth0 provides developers with a great experience through a huge set of open-source libraries that facilitate the process of securing their apps. Lastly, Auth0 holds many important certifications like GDPR, HIPAA, and SOC 2 that can help you deliver solutions that comply with international requirements. As such, choosing Auth0 to secure your applications is a no-brainer.

So, if you don't have an Auth0 account yet, you can sign up for a free one right now. With your free Auth0 account, you will have access to the following features:

Defining an Auth0 API

After creating your Auth0 account, you will be redirected to your dashboard. From there, you will have to go to the APIs section to register an API that represents the backend API (a.k.a., the Resource Server) that you will develop. Electron apps normally have business logic running on the server side, and what you are doing here will enable you to secure access to your APIs by obtaining an access token for them.

After going to the APIs section, click on Create API. Clicking on it will make Auth0 show a dialog with three fields:

  • Name: Here, you will have to insert a name to represent your API in your Auth0 account. For example: "My Resource Server".
  • Identifier: In this field, you will have to insert a logical identifier for this API. For example: https://my-resource-server.
  • Signing Algorithm: For this field, you can leave the default option (RS256), which is way more secure than the other.

Creating an Auth0 API for your Resource Server.

After fulfilling these fields, click on Create to finish the process.

Defining an Auth0 Application

Now that you have registered your Resource Server (i.e., the backend API) in your Auth0 account, you will have to create an Application to represent your Electron app on Auth0. So, click on the Applications section on the vertical menu and then click on Create Application.

On the dialog shown, you will have to insert a name for your application (for example, "My Electron App") and then you will have to choose the type of the application (in this case, you will have to choose Native). Then, when you click on the Create button, Auth0 will create your Application for you and redirect you to its Quick Start section. From there, you will have to click on the Settings tab to change the configuration of your Auth0 Application and to copy some values from it.

So, after heading to the Settings tab, search for the Allowed Callback URLs fields and insert file:///callback on it. You are probably wondering what this weird URL is and why you need it.

The reason why you need this URL is that, while authenticating through Auth0, your users will see the Universal Login Page of Auth0 and, after the authentication process (successful or not), Auth0 will call an URL back to inform the result. For security reasons, Auth0 will call only URLs registered on this field.

Now, regarding the structure of the URL, as you are developing a native application with Electron, you will need to make it listen to the file:// protocol and a custom URI (in this case, /callback), so it knows when it is being called.

So, after configuring your Auth0 Application, you can click on the Save button and leave this page open. You will need to copy a few values from it soon.

Creating the Backend API

After spending some time learning about the theoretical aspect of this article, it is time to start typing some code. In this section, you will quickly build a backend API that will play the Resource Server role in your application.

So, without wasting more time, open a terminal window and create a directory to work as the project root for both your sub-projects (i.e., you will put your backend API and your Electron app inside this directory):

# create the project root
mkdir electron-openid-oauth

After that, you will create a directory for your backend API and initialize it as an NPM project:

# move into your project root
cd electron-openid-oauth

# create a dir to the backend
mkdir backend

# move into the new dir
cd backend

# init NPM with default config
npm init -y

As this backend API will be very simple, you will have only three dependencies on it:

  • express: the most popular web application framework for Node.js.
  • express-jwt: a module to validate JWTs (access tokens, in this case) that sets the req.user property with some attributes related to the current user.
  • jwks-rsa: a library to retrieve RSA public keys from a JWKS (JSON Web Key Set) endpoint to validate access tokens.

To install these dependencies, issue the following command from the backend directory:

npm i express express-jwt jwks-rsa

After installing these libraries, you can create a file called index.js in the backend directory and add the following code to it:

const express = require('express');
const jwt = require('express-jwt');
const jwksRsa = require('jwks-rsa');
const envVariables = require('./env-variables');

const app = express();

app.use(jwt({
  // Dynamically provide a signing key based on the kid in the header and the singing keys provided by the JWKS endpoint.
  secret: jwksRsa.expressJwtSecret({
    cache: true,
    rateLimit: true,
    jwksRequestsPerMinute: 5,
    jwksUri: `https://${envVariables.auth0Domain}/.well-known/jwks.json`
  }),

  // Validate the audience and the issuer.
  audience: envVariables.apiIdentifier,
  issuer: `https://${envVariables.auth0Domain}/`,
  algorithms: ['RS256']
}));

app.get('/private', (req, res) => res.send('Only authenticated users can read this message.'));

app.listen(3000, () => console.log('Example app listening on port 3000!'));

Although not extensive, the code above does some interesting things. For starters, this script creates an Express app. After that, it configures a middleware (app.use(jwt({...}))) to restrict access to any route defined after the middleware. Then, it wraps up by defining an endpoint (i.e., /private) that is only accessible by authenticated users.

The middleware responsible for verifying access tokens, the one created with jwt and jwksRsa, checks for tokens in the Authorization headers of requests hitting the /private endpoint to validate if they were issued by the Authorization Server (in this case, by Auth0).

If you are curious to learn more about JWKS, you can check this excellent article that Shawn Meyer (an Auth0 employee) wrote.

Backend API Environment Variables

Now, if you take a close look, you will notice that the script defined above makes use of a module called env-variables. As you can imagine, the goal of this module is to hold variables that are dependent on the environment.

In this article, you won't make your application run in other environments. However, it is always a good idea to avoid hard-coding these variables so you don't end up pushing them to your version control system (which can expose some credentials or make lives of contributors harder).

So, to define your environment variables, create a file called env-variables.json in the backend directory and add the following object to it:

{
  "apiIdentifier": "<API_IDENTIFIER>",
  "auth0Domain": "<YOUR_AUTH0_TENANT>.auth0.com"
}

Note: You will have to replace <API_IDENTIFIER> with the identifier set while creating an Auth0 API to represent your Resource Server. Also, you will have to replace <YOUR_AUTH0_TENANT> with the subdomain chosen while creating your Auth0 account (or another Auth0 tenant). For example, you could replace both placeholders with https://my-resource-server-api and by my-auth0-subdomain, respectively.

After creating this file, you can start and leave your backend API running:

# make sure you are in the backend directory
node index

Running the Node.js Resource Server locally.

Creating the Electron Application

Now that you have finished creating the Node.js backend API, you can start focusing on your Electron application. This application, as mentioned, won't have fancy or complicated features. You will focus on building a simple Electron app that uses an Authorization Server to authenticate users and to get access tokens that authorize the app to consume data from the Resource Server.

So, in a new terminal (since the first one is running the backend API), move into your project root, create a directory for the Electron app, and start it as an NPM project:

# move into the project root
cd electron-openid-oauth

# create a dir for your Electron app
mkdir frontend

# move into it
cd frontend

# init NPM with default properties
npm init -y

After that, open the package.json file and replace the main and the script properties with the following:

{
  "main": "main.js",
  "scripts": {
    "start": "electron ./"
  },
}

Just make sure you leave the other properties in this file untouched.

Installing the Electron App Dependencies

Just like your backend API, your Electron app won't have too many dependencies. In fact, your app will have only four dependencies:

  • axios: a promise based HTTP client for the browser and node.js.
  • electron: the platform itself so you can run your Electron app.
  • jwt-decode: a library that will enable your app to decode and interpret ID Tokens issued by the Authorization Server (i.e., by Auth0).
  • request: an HTTP client library that you will use in the Main process to send requests to your backend API.

To install these dependencies, issue the following commands:

# you need electron as a dev dependency
npm i -D electron

# and the other two as normal dependencies
npm i axios jwt-decode request

Soon, you will use these libraries while developing your Electron app.

Managing Different Environments on the Electron App

As you recall, you backend API makes use of a file called env-variables.json to define variables that are dependent on the environment. In your Electron application, you will use the same strategy to define three variables that might change from environment to environment:

  1. apiIdentifier: This variable will refer to the identifier you set while creating an Auth0 API to represent your Resource Server (exactly as you did in your backend project).
  2. auth0Domain: The domain of your Auth0 tenant.
  3. clientId: The Client ID of the Auth0 Application you created to represent your Electron app.

So, create a new file called env-variables.json in your frontend directory and insert the following object into it:

{
  "apiIdentifier": "<API_IDENTIFIER>",
  "auth0Domain": "<YOUR_AUTH0_TENANT>.auth0.com",
  "clientId": "<YOUR_AUTH0_CLIENT_ID>"
}

Note: You will have to replace <API_IDENTIFIER>, <YOUR_AUTH0_TENANT>, and <YOUR_AUTH0_CLIENT_ID> with your values. You can replace the first two placeholders with the same values you used in the backend project. For the latter, <YOUR_AUTH0_CLIENT_ID>, you will have to copy the Client ID generated by Auth0 when you created your Auth0 Application.

Persisting Data with the Electron App

One thing that you haven't heard about so far is that your Electron app will leverage Refresh Tokens to avoid asking users to authenticate all the time explicitly. If you don't know, a Refresh Token is a special kind of token that contains the information required to obtain a new access token and/or id token. That is, when your users authenticate, Auth0 will actually send to your Electron app three tokens: an access token, an id token, and a refresh token. Then, when your users close your app and reopen it (or when the access token and the id token expire), your app will have to use the refresh token to get new versions of the other two.

As such, you will need to provide your Electron app a way to store (persist) these refresh tokens on disk (otherwise you wouldn't have them when your users reopen your app). To do so, you will use an NPM module called keytar. As described by the official documentation, keytar is:

"A native Node module to get, add, replace, and delete passwords in system's keychain. On macOS, the passwords are managed by the Keychain, on Linux they are managed by the Secret Service API/libsecret, and on Windows they are managed by Credential Vault." - https://github.com/atom/node-keytar

To install this module, issue the following command on the terminal:

# make sure you are on the frontend directory
npm i keytar

In the next section, you will see keytar in action.

Authenticating Users in Electron Applications

After installing keytar, the next thing you will do is to create another service. This one will be responsible for the authentication process. Mainly, the module that you are about to create will:

  1. provide a function (getAuthenticationURL) that returns the complete URL of the Authorization Server that users have to visit to authenticate;
  2. provide a function (refreshTokens) that verifies if there is a Refresh Token available to the current user and, if positive, use it to exchange for a new access token;
  3. and provide a function (loadTokens) to parse the URL called back after the authentication process to get the code included to fetch different tokens (an access token, the Refresh Token, and an ID Token).

So, create a new file called auth-service.js under the services directory and insert the following code into it:

const jwtDecode = require('jwt-decode');
const request = require('request');
const url = require('url');
const envVariables = require('../env-variables');
const keytar = require('keytar');
const os = require('os');

const {apiIdentifier, auth0Domain, clientId} = envVariables;

const redirectUri = `file:///callback`;

const keytarService = 'electron-openid-oauth';
const keytarAccount = os.userInfo().username;

let accessToken = null;
let profile = null;
let refreshToken = null;

function getAccessToken() {
  return accessToken;
}

function getProfile() {
  return profile;
}

function getAuthenticationURL() {
  return 'https://' + auth0Domain + '/authorize?' +
    'audience=' + apiIdentifier + '&' +
    'scope=openid profile offline_access&' +
    'response_type=code&' +
    'client_id=' + clientId + '&' +
    'redirect_uri=' + redirectUri;
}

function refreshTokens() {
  return new Promise(async (resolve, reject) => {
    const refreshToken = await keytar.getPassword(keytarService, keytarAccount);

    if (!refreshToken) return reject();

    const refreshOptions = {
      method: 'POST',
      url: `https://${auth0Domain}/oauth/token`,
      headers: {'content-type': 'application/json'},
      body: {
        grant_type: 'refresh_token',
        client_id: clientId,
        refresh_token: refreshToken,
      },
      json: true,
    };

    request(refreshOptions, function (error, response, body) {
      if (error) {
        logout();
        return reject();
      }

      accessToken = body.access_token;
      profile = jwtDecode(body.id_token);

      resolve();
    });
  });
}

function loadTokens(callbackURL) {
  return new Promise((resolve, reject) => {
    const urlParts = url.parse(callbackURL, true);
    const query = urlParts.query;

    const exchangeOptions = {
      'grant_type': 'authorization_code',
      'client_id': clientId,
      'code': query.code,
      'redirect_uri': redirectUri,
    };

    const options = {
      method: 'POST',
      url: `https://${auth0Domain}/oauth/token`,
      headers: {
        'content-type': 'application/json'
      },
      body: JSON.stringify(exchangeOptions),
    };

    request(options, (error, resp, body) => {
      if (error) {
        logout();
        return reject(error);
      }

      const responseBody = JSON.parse(body);
      accessToken = responseBody.access_token;
      profile = jwtDecode(responseBody.id_token);
      refreshToken = responseBody.refresh_token;

      keytar.setPassword(keytarService, keytarAccount, refreshToken);

      resolve();
    });
  });
}

async function logout() {
  await keytar.deletePassword(keytarService, keytarAccount);
  accessToken = null;
  profile = null;
  refreshToken = null;
}

module.exports = {
  getAccessToken,
  getAuthenticationURL,
  getProfile,
  loadTokens,
  logout,
  refreshTokens,
};

Alongside with the functions mentioned earlier, you will find the following functions in this module:

  • getAccessToken: A function that simply returns the current accessToken.
  • getProfile: A function that returns an object with the user profile. This service extracts this object from the ID Token when one is received.
  • logout: A function that clears the current session by removing the Refresh Token from the disk and by erasing the accessToken, profile, and refreshToken variables.

Also, at the beginning of this file, you will find the definition of a few constants:

  • apiIdentifier, auth0Domain, and clientId: Electron will use these constants while interacting with the Authorization Server (i.e., Auth0).
  • redirectUri: This constant defines what URL Auth0 will call after finishing the authentication process.
  • keytarService and keytarAccount: Theses constants define what keytar will use to persist/retrieve the refresh token on/from the disk.

Now, as your users will need a window to interact with the Authorization Server, you will need to make your Main process (which you will define soon) create an instance of BrowserWindow. The sole purpose of this window will be rendering the login page of the Authorization Server for your users.

To manage this window, create a new directory called main inside the frontend directory and add a file called auth-process.js to it. Inside this file, you will insert the following code:

const {BrowserWindow} = require('electron');
const authService = require('../services/auth-service');
const createAppWindow = require('../main/app-process');

let win = null;

function createAuthWindow() {
  destroyAuthWin();

  // Create the browser window.
  win = new BrowserWindow({
    width: 1000,
    height: 600,
    webPreferences: {
      nodeIntegration: false,
    },
  });

  win.loadURL(authService.getAuthenticationURL());

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

  const filter = {
    urls: [
      'file:///callback*'
    ]
  };

  webRequest.onBeforeRequest(filter, async ({url}) => {
    await authService.loadTokens(url);
    createAppWindow();
    return destroyAuthWin();
  });

  win.on('authenticated', () => {
    destroyAuthWin();
  });

  win.on('closed', () => {
    win = null;
  });
}

function destroyAuthWin() {
  if (!win) return;
  win.close();
  win = null;
}

module.exports = createAuthWindow;

The functionality that this module exposes is quite simple. First, it defines and exposes a function called createAuthWindow that creates a new instance of BrowserWindow and that loads the Authorization Server URL into it (note that, since this is a window that will load resources from the web, you are making it detached from the Main process by using nodeIntegration: false). That is, through these BrowserWindow instances, your users will be able to authenticate themselves. Second, it defines a function called destroyAuthWin that your app can use to close the current BrowserWindow instance when it doesn't need it anymore.

What is important to notice is that Auth0 will call the file:///calback URL right after your users authenticate. As such, you are defining a listener (through the onBeforeRequest function) that Electron will trigger when Auth0 calls the callback URL. The goal of this listener is to load users' tokens (authService.loadTokens(url)) and, after that, create the main window of your app (createAppWindow()) and destroy the current one (destroyAuthWin()).

Creating the Electron App Main Process

Similarly to the last module that you created, you will need to create a new module that will render your Electron application. In this case, you will define a module that will handle the Renderer process responsible for showing a web page with your app.

So, to define this module, create a new file called app-process.js in the main directory. Then, add the following code to this file:

const {BrowserWindow} = require('electron');

function createAppWindow() {
  let win = new BrowserWindow({
    width: 1000,
    height: 600,
  });

  win.loadFile('./renderers/home.html');

  win.on('closed', () => {
    win = null;
  });
}

module.exports = createAppWindow;

As you can see, the only thing that this will do is to load the ./renderers/home.html file (which you still need to create). This is going to be the only route (and browser window) that your authenticated users will use, besides the authorization server login page.

Then, the last module you will have to create is the module that orchestrates the whole thing. To define this module, create a file called main.js inside the frontend directory and insert the following code to it:

const {app} = require('electron');

const createAuthWindow = require('./main/auth-process');
const createAppWindow = require('./main/app-process');
const authService = require('./services/auth-service');

async function showWindow() {
  try {
    await authService.refreshTokens();
    return createAppWindow();
  } catch (err) {
    createAuthWindow();
  }
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', showWindow);

// Quit when all windows are closed.
app.on('window-all-closed', () => {
  app.quit();
});

app.on('activate', () => {
  // On macOS, it's common to re-create a window in the app when the
  // dock icon is clicked, and there are no other windows open.
  if (win === null) {
    showWindow();
  }
});

There are two things that worth explaining about this module:

  1. You are defining a function called showWindow that tries to load tokens with the Refresh Token. If this function succeeds, it loads the Electron app (createAppWindow). If not, it loads the Authorization Server login page (createAuthWindow).
  2. You are attaching the showWindow to the ready event that Electron emits after finishing its initialization process. You are doing so to open the Electron app for users with the Refresh Token and the Authorization Server login page for unauthenticated users.

For the Node.js code that your app will need, that is it. Now, to complete your Electron application, you need to define a good-looking web page for your app.

Creating the Electron App Renderer Process

Now, to complete this tutorial and to have an Electron application secured with OpenID Connect and OAuth 2.0, you will need to create a few files to form the web page that your app loads. So, first, you will create a new directory called renderers inside the frontend directory. After that, you will create a file called home.html inside this new directory. In this file, you will place the following code:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <meta http-equiv="Content-Security-Policy" content="script-src 'self'">
  <title>Electron App</title>
  <link rel="stylesheet"
        href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"
        integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO"
        crossorigin="anonymous">

  <link rel="stylesheet" href="home.css">
</head>

<body>
<nav class="navbar fixed-top navbar-expand-lg navbar-dark bg-dark">
  <div class="profile">
    <div><a class="navbar-brand" href="#">Electron App</a></div>
    <img id="picture"/>
    <span id="name"></span>
    <button id="logout" class="btn">Logout</button>
  </div>
</nav>
<div class="container-fluid">
  <div class="row">
    <div class="col-sm">
      <h1>Electron App</h1>
      <p>This Electron application is secured with OpenID Connect and OAuth 2.0.</p>
    </div>
  </div>
  <div class="row">
    <div class="col-sm">
      <div id="success" class="alert alert-success"></div>
      <div id="message" class="jumbotron"></div>
      <button id="secured-request" class="btn btn-primary">Get Private Message</button>
    </div>
  </div>

</div>
</body>

<script src="home.js"></script>
</html>

Although you can see a lot of HTML elements, this web page is actually quite simple. One thing you might notice if you analyze the code carefully is that this page uses Bootstrap to make the User Interface (UI) more appealing. So, most of the elements in this page are blocks to structure what is a very simple UI. When you run your application and authenticate yourself, you will see that the web page is structured with:

  • a navigation bar (the nav.navbar element) that shows the user's name, its profile picture, and a button to logout;
  • an HTML title (h1) with Electron App as its text;
  • a paragraph mentioning that this app is secured with OpenID Connect and OAuth 2.0;
  • a block (styled with the alert-success class) that will show a message telling your users that they are successfully authenticated.
  • another block (this time styled with the jumbotron class) to show the message fetched from the backend API;
  • and a button (styled with btn-primary) to trigger the request to the backend API.

What you won't find inside this file is the JavaScript code needed to make the web page alive. You will only find a reference to it (the script tag pointing to home.js).

So, to create the JavaScript code that supports this web page, create a new file called home.js inside the renderers directory and insert the following code into it:

const {remote} = require('electron');
const axios = require('axios');
const authService = remote.require('./services/auth-service');

const webContents = remote.getCurrentWebContents();

webContents.on('dom-ready', () => {
  const profile = authService.getProfile();
  document.getElementById('picture').src = profile.picture;
  document.getElementById('name').innerText = profile.name;
  document.getElementById('success').innerText = 'You successfully used OpenID Connect and OAuth 2.0 to authenticate.';
});

document.getElementById('logout').onclick = () => {
  authService.logout();
  remote.getCurrentWindow().close();
};

document.getElementById('secured-request').onclick = () => {
  axios.get('http://localhost:3000/private', {
    headers: {
      'Authorization': `Bearer ${authService.getAccessToken()}`,
    },
  }).then((response) => {
    const messageJumbotron = document.getElementById('message');
    messageJumbotron.innerText = response.data;
    messageJumbotron.style.display = 'block';
  }).catch((error) => {
    if (error) throw new Error(error);
  });
};

The code in this file is quite easy to understand. It all starts with the definition of webContents. You define this constant to attach a listener to the dom-ready event of the current browser window. When Electron triggers this event, you make your app load and show the current user profile.

After that, you define handlers for the onclick events of the logout and the secured-request buttons. For the first one, you delegate the process to authService.logout and then close the current window (which ends the Main process as a whole). For the second one, you use axios to issue a request to http://localhost:3000/private to get the secured message.

What is important to notice here is that the requests you issue with axios must contain the access token retrieved in the authentication process. Otherwise, you will get an HTTP 401 Unauthorized response.

Note: You can always use the comments area down below to ask questions when something is not clear.

Lastly, the only thing missing is a file called home.css to enhance the looking of the web page. So, create this file inside the renderers directory and insert the following CSS code into it:

body {
  padding-top: 70px;
  padding-bottom: 30px;
}

div.profile {
  display: grid;
  grid-template-columns: 1fr auto auto auto;
  align-items: start;
  width: 100%;
}

div.profile span {
  display: inline-block;
  align-self: center;
  color: #ccc;
  margin-left: 10px;
  margin-right: 25px;
}

div.profile img {
  width: 30px;
  border-radius: 50%;
  align-self: center;
}

div#message {
  display: none;
}

Done! You can now run and enjoy your Electron application while knowing that it is secured with enterprise-grade solutions. By the way, to run your application, head to the frontend directory in your terminal and issue the following command:

# as you have defined the start script before
npm start

Note: If you run into an error saying that "keytar was compiled against a different Node.js version using", you will have to install electron-rebuild and run it locally ./node_modules/.bin/electron-rebuild, as explained here.

As this is the first time you are running your application, you will see the Authorization Server login page:

The Authorization Server login page.

Note: Users will also see this screen after they logout from your Electron application.

Now, after properly authenticating, your Electron application will show you the only web page available (i.e., home.html). In this page, you will be able to see your name, your profile picture, a button to logout, a few messages, and a button to fetch a secured message from the Resource Server:

Running an Electron application secured with OpenID Connect and OAuth 2.0.

"I just built an @electronjs application that is secured with @openid Connect and OAuth 2.0"

Conclusion

In this article, you had a chance to deal with a lot of interesting subjects. You briefly learned how Electron works and the different processes it provides (i.e., the Main and the Renderers processes). After that, you created an Auth0 account to use it while securing your Electron app.

Lastly, you dived deep into creating a backend API with Node.js and a frontend application with Electron from scratch. Throughout the process, you learned how to secure both projects (the frontend and the backend) with OpenID Connect and OAuth 2.0.

In the end, you finished with a basic Electron app that relies on an Authorization Server (Auth0) to authenticate users and to get tokens to enable your Client (the Electron app) to communicate with a Resource Server (the Node.js backend API). Pretty cool, right?