Check out the free virtual workshops on how to take your SaaS app to the next level in the enterprise-ready identity journey!

Build a Desktop App with Electron and Authentication

Build a Desktop App with Electron and Authentication

Electron is a framework for building cross-platform desktop applications with web technologies like JavaScript, HTML, and CSS. It was created for GitHub’s Atom editor and has achieved widespread adoption since. Electron powers several apps that I use on a daily basis: Slack, Kitematic, and Visual Studio Code to name a few.

Electron 2.0 was released in early May 2018, along with changes to the project to adhere to strict semantic versioning. This is good news for developers because it means patch releases will be more stable and new features will come in major versions only. When open source projects use semantic versioning correctly, end users don’t see breaking changes as often and tend to be productive.

Electron 3.0 was released on September 18, 2018 and contains major version bumps and some new features. See the Electron blog for more information.

Electron 4.0 was released on December 20, 2018 and includes major version bumps for Chromium (v69), Node (v10.11), and V8 (v6.9). It also removes support for OS X Mavericks. See the Electron blog for more information.

Developing desktop applications with web technologies is an appealing notion. This tweet from Gerard Sans nails it in my opinion:

Do you have web development skills? Great! You have what’s needed to build a desktop app with Electron!

In this article, I’ll show you how to create an Electron app with TypeScript, AppAuth-JS, and OpenID Connect (OIDC). You’ll learn how to add authentication and secure an Electron app for your users.

What is AppAuth?

AppAuth is a project that aims to create client SDKs for native apps. It makes it possible to implement authentication and authorization in your apps using OIDC and OAuth 2.0. It has SDKs available for iOS, macOS, Android, and JavaScript environments. AppAuth-JS is the SDK for JavaScript clients. AppAuth also supports the PKCE extension to OAuth to make public clients more secure.

Pretty much every application depends upon a secure identity management system. For most developers who are building Electron apps, there’s a decision to be made between rolling your own authentication/authorization or plugging in a hosted identity service like Okta.

Build Desktop Apps with Electron

I’m going to make things easy for you. Rather than building an app from scratch, you’ll use a pre-existing example. The AppAuth-JS project has two examples, a server-side example with Node at src/node_app/index.ts, and an appauth-js-electron-sample. Clone the Electron example to begin.

git clone https://github.com/googlesamples/appauth-js-electron-sample.git okta-electron-example

Open the package.json in this project and make the following changes.

@@ -6,8 +6,8 @@
   "scripts": {
     "compile": "tsc",
     "watch": "tsc --watch",
-    "start": "npm run-script compile && node_modules/.bin/electron .",
-    "dev": "npm run-script watch & node_modules/.bin/electron ."
+    "start": "npm run compile && npx electron .",
+    "dev": "npm run compile && npm run watch & npx electron ."
   },
   "files": [
     "built/**"
@@ -19,13 +19,13 @@
   "author": "rahulrav",
   "license": "MIT",
   "dependencies": {
-    "@openid/appauth": "^0.3.5",
+    "@openid/appauth": "^1.2.0",
     "@types/react": "^16.3.17",
     "@types/react-dom": "^16.0.6",
-    "electron": "^2.0.2",
     "material-design-lite": "^1.3.0"
   },
   "devDependencies": {
-    "typescript": "^2.9.1"
+    "electron": "^4.0.1",
+    "typescript": "^3.2.0"
   }
 }

The changes in the “scripts” are not necessary, but they make compilation happen before you run npm run dev or npm start. You’re also upgrading Electron, moving its dependency to be a devDependency, and upgrading TypeScript to the latest version.

In addition, you’ll need to make some changes to flow.ts since you just upgraded to AppAuth 1.2.0. The upgrade allows for PKCE (Proof Key for Code Exchange), and a couple constructors need to be changed to take in objects instead of arguments. It might be cumbersome to pick through the changes below. You can copy this file instead.

NOTE: If the changes below are already in this file, it’s likely this pull request was merged and you can skip this part.

+import { NodeCrypto } from '@openid/appauth/built/node_support/';
 
 export class AuthStateEmitter extends EventEmitter {
   static ON_TOKEN_RESPONSE = "on_token_response";
@@ -83,7 +75,11 @@ export class AuthFlow {
     this.notifier.setAuthorizationListener((request, response, error) => {
       log("Authorization request complete ", request, response, error);
       if (response) {
-        this.makeRefreshTokenRequest(response.code)
+        let codeVerifier: string | undefined;
+        if(request.internal && request.internal.code_verifier) {
+          codeVerifier = request.internal.code_verifier;
+        }
+        this.makeRefreshTokenRequest(response.code, codeVerifier)
           .then(result => this.performWithFreshTokens())
           .then(() => {
             this.authStateEmitter.emit(AuthStateEmitter.ON_TOKEN_RESPONSE);
@@ -115,14 +111,13 @@ export class AuthFlow {
     }
 
     // create a request
-    const request = new AuthorizationRequest(
-      clientId,
-      redirectUri,
-      scope,
-      AuthorizationRequest.RESPONSE_TYPE_CODE,
-      undefined /* state */,
-      extras
-    );
+    const request = new AuthorizationRequest({
+      client_id: clientId,
+      redirect_uri: redirectUri,
+      scope: scope,
+      response_type: AuthorizationRequest.RESPONSE_TYPE_CODE,
+      extras: extras
+    }, new NodeCrypto());
 
     log("Making authorization request ", this.configuration, request);
 
@@ -132,19 +127,25 @@ export class AuthFlow {
     );
   }
 
-  private makeRefreshTokenRequest(code: string): Promise<void> {
+  private makeRefreshTokenRequest(code: string, codeVerifier?: string): Promise<void> {
     if (!this.configuration) {
       log("Unknown service configuration");
       return Promise.resolve();
     }
+
+    const extras: StringMap = {};
+    if(codeVerifier) {
+      extras.code_verifier = codeVerifier;
+    }
+
     // use the code to make the token request.
-    let request = new TokenRequest(
-      clientId,
-      redirectUri,
-      GRANT_TYPE_AUTHORIZATION_CODE,
-      code,
-      undefined
-    );
+    let request = new TokenRequest({
+      client_id: clientId,
+      redirect_uri: redirectUri,
+      grant_type: GRANT_TYPE_AUTHORIZATION_CODE,
+      code: code,
+      extras: extras
+    });
 
     return this.tokenHandler
       .performTokenRequest(this.configuration, request)
@@ -179,13 +180,12 @@ export class AuthFlow {
       // do nothing
       return Promise.resolve(this.accessTokenResponse.accessToken);
     }
-    let request = new TokenRequest(
-      clientId,
-      redirectUri,
-      GRANT_TYPE_REFRESH_TOKEN,
-      undefined,
-      this.refreshToken
-    );
+    let request = new TokenRequest({
+      client_id: clientId,
+      redirect_uri: redirectUri,
+      grant_type: GRANT_TYPE_REFRESH_TOKEN,
+      refresh_token: this.refreshToken
+    });
     return this.tokenHandler
       .performTokenRequest(this.configuration, request)
       .then(response => {

Navigate to the cloned directory, install dependencies with npm, and run the app.

cd okta-electron-example
npm i
npm start

It should start the app and show a Sign-In link.

blog/electron-react-appauth-js/initial-load.png

If you have a Google account, click Sign-In, log in, and you’ll be redirected back to your app. You should see your avatar and name displayed.

After Google Sign-In

The diagram below shows how this authorization flow happens using OpenID Connect.

OIDC Flow

At this point, you can see that authentication with Google is working. In the next sections, I’ll show you how to use Okta instead of Google.

Why Use Okta for Authentication?

You might be asking: why should I use Okta when authentication with Google works? The reason is simple; if you want to manage the users of your app (and you inevitably will), Okta makes it possible. With Google, anyone with a Google Account can log in, but you have no way of revoking access or updating user’s permissions because you can’t manage your users through Google. Okta lets you manage your users, as well as modify their attributes and permissions. Better yet, you can still use Google as a social login mechanism with Okta!

Use Okta for Authentication with OIDC in Your Desktop App

Okta’s goal is to make identity management a lot easier, more secure, and more scalable than what you’re used to. Okta is a cloud service that allows developers to create, edit, and securely store user accounts and user account data, and connect them with one or multiple applications. Our API enables you to:

Are you ready? Register for a forever-free developer account today! When you’re finished, complete the steps below to create a Native OIDC app.

  1. Log in to your developer account on developer.okta.com.
  2. Navigate to Applications and click on Add Application.
  3. Select Native and click Next.
  4. Give the application a name (e.g., My Electron App) and add http://localhost:8000 as a Login redirect URI.
  5. For Grant type allowed, select Refresh Token in addition to Authorization Code.
  6. Click Done.

Now you can use your app settings to change from using Google to Okta. Modify flow.ts to use your Okta app’s settings.

const openIdConnectUrl = 'https://{yourOktaDomain}/oauth2/default';
const clientId = '{yourClientId}';
const redirectUri = 'http://localhost:8000';

You’ll also want to update app.ts to use your app’s /userinfo endpoint.

let request =
    new Request('https://{yourOktaDomain}/oauth2/default/v1/userinfo', {
      headers: new Headers({'Authorization': `Bearer ${accessToken}`}),
      method: 'GET',
      cache: 'no-cache'
    });

Add PKCE Support to Your Desktop App

PKCE (pronounced “pixy”) is a security extension for OAuth 2.0 for public clients on mobile (and desktop) clients. It’s designed to prevent interception of the authorization code by a malicious application that runs on the same device.

    +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
    | End Device (e.g., Smartphone)  |
    |                                |
    | +-------------+   +----------+ | (6) Access Token  +----------+
    | |Legitimate   |   | Malicious|<--------------------|          |
    | |OAuth 2.0 App|   | App      |-------------------->|          |
    | +-------------+   +----------+ | (5) Authorization |          |
    |        |    ^          ^       |        Grant      |          |
    |        |     \         |       |                   |          |
    |        |      \   (4)  |       |                   |          |
    |    (1) |       \  Authz|       |                   |          |
    |   Authz|        \ Code |       |                   |  Authz   |
    | Request|         \     |       |                   |  Server  |
    |        |          \    |       |                   |          |
    |        |           \   |       |                   |          |
    |        v            \  |       |                   |          |
    | +----------------------------+ |                   |          |
    | |                            | | (3) Authz Code    |          |
    | |     Operating System/      |<--------------------|          |
    | |         Browser            |-------------------->|          |
    | |                            | | (2) Authz Request |          |
    | +----------------------------+ |                   +----------+
    +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+

The PKCE working group provides an excellent explanation I’ve included below. You can read more in the official RFC.

“To mitigate this attack, PKCE uses a dynamically created cryptographically random key called a “code verifier”. A unique code verifier is created for every authorization request, and its transformed value, called “code challenge”, is sent to the authorization server to obtain the authorization code. The authorization code obtained is then sent to the token endpoint with the “code verifier”, and the server compares it with the previously received request code so that it can perform the proof of possession of the “code verifier” by the client. This works as the mitigation since the attacker would not know this one-time key since it is sent over TLS and cannot be intercepted.”

The diagram below shows how PKCE works with your app and Okta.

Auth Code Flow with PKCE

You don’t need to change any code in your Electron app to add PKCE. That was done when you upgraded flow.ts to use AppAuth 1.2.0.

Start your app with npm start, and try to login. You should be able to. However, when you click on USER INFO, you won’t see your user’s name or avatar. Open Chrome Developer Tools with View > Toggle Developer Tools to see why.

Electron's Developer Tools

To fix this issue, change the scope variable in flow.ts to include profile. While you’re at it, add offline_access so your app can work without an internet connection.

const scope = 'openid profile offline_access';

Refresh your app (Command+R on Mac, Ctrl+R on Windows/Linux), and now you should see the name when clicking on USER INFO.

Name from User Info Endpoint

Add an Avatar in Okta

You might notice that the user info endpoint doesn’t return an avatar. The code in app.ts sets the avatar based on a picture attribute.

private updateUi() {
  this.handleSignIn.textContent = SIGN_OUT;
  this.fetchUserInfo.style.display = '';
  if (this.userInfo) {
    this.userProfileImage.src = `${this.userInfo.picture}?sz=96`;
    this.userName.textContent = this.userInfo.name;
    this.showSnackBar(
        {message: `Welcome ${this.userInfo.name}`, timeout: 4000});
    this.userCard.style.display = '';
  }
}

You can delete ?sz=96 in the above code since this example doesn’t use it.

To add a picture attribute to your user, log in to your Okta dashboard and navigate to Users > Profile Editor. Click on the first “user” and add a picture attribute. Click Save.

Add Picture Attribute

Navigate back to the Profile Editor and click on Mappings for your Electron App. Create a mapping from user.picture to picture and select to apply the mapping on user create and update. Click Save Mapping and Apply Updates Now.

Add Picture Mapping

Now go to Users > People, select a user, navigate to the Profile tab, and click Edit. Add a value for picture at the bottom. For example, you can use the URL of our Okta Developer logo.

https://www.okta.com/sites/all/themes/Okta/images/logos/developer/Dev_Logo-02_Large.png

Now if you click on the user info link, you should see an avatar associated with your user.

User Info Picture

TIP: If you’re going to use this in production, I’d recommend you use a smaller image for the picture (e.g., with 150x150 dimensions). You can also base64 encode an image and use its value for your picture.

Below is a screenshot that shows this app with some additional polish and a base64 image value for my account.

Polished Matt

Package Your Desktop App For Production

To package this app for production distribution, you can use electron-builder. Install electron-builder with npm.

npm i -D electron-builder@20.38.4

Add a build section to your package.json:

"build": {
  "appId": "com.okta.developer.electron",
  "productName": "Electron Awesomeness",
  "mac": {
    "category": "public.app-category.developer-tools"
  }
}

Then add pack, dist, and postinstall scripts.

"scripts": {
  ...
  "pack": "npm run compile && electron-builder --dir",
  "dist": "npm run compile && electron-builder",
  "postinstall": "electron-builder install-app-deps"
}

To package your app for production, use the following commands:

  • npm run pack will generate the package directory without really packaging it. This is useful for testing purposes.
  • npm run dist will package in a distributable format (e.g., dmg, Windows installer, deb package).

NOTE: If the app doesn’t start after packaging, it’s likely because you don’t have code signing configured. To disable Code Signing when building for macOS, run export CSC_IDENTITY_AUTO_DISCOVERY=false. If you have an Apple Developer Account, open Xcode, go to Preferences > Accounts and make sure you’re logged in, and your development certificates are downloaded.

Electron Example App Source Code

You can find the source code for this article at https://github.com/oktadeveloper/okta-appauth-js-electron-example.

I made some minor adjustments (e.g., optimizing imports, changing double quotes to single quotes), but nothing major. To see a diff between this project and the original, click here.

Learn More About Electron, AppAuth, and OIDC

There you have it! Go forth and use your web skills to create amazing desktop applications!

To learn more about Electron, AppAuth, React, and OIDC, check out the following resources:

Got questions? Please leave a comment below, ping me on Twitter, or ask it on our Developer Forums.

Like what you learned here? Follow @oktadev, like us on Facebook, follow us on LinkedIn, or watch our videos on YouTube.

Changelog:

Matt Raible is a well-known figure in the Java community and has been building web applications for most of his adult life. For over 20 years, he has helped developers learn and adopt open source frameworks and use them effectively. He's a web developer, Java Champion, and Developer Advocate at Okta. Matt has been a speaker at many conferences worldwide, including Devnexus, Devoxx Belgium, Devoxx France, Jfokus, and JavaOne. He is the author of The Angular Mini-Book, The JHipster Mini-Book, Spring Live, and contributed to Pro JSP. He is a frequent contributor to open source and a member of the JHipster development team. You can find him online @mraible and raibledesigns.com.

Okta Developer Blog Comment Policy

We welcome relevant and respectful comments. Off-topic comments may be removed.