Electron + Auth0 | Setting up PKCE Authentication for Authenticated API Access

Sebastian Scholl
6 min readJan 10, 2019

--

While reading this, please keep in mind that it depends on properly setting up an Application and Api in your Auth0 account.

Several months ago I found myself beating my head against the wall while integrating Auth0 into an Electron app. The main issues were seen when handling redirects in the production application on OSX. It made me rethink the implementation.

Being that Electron apps are native desktop apps, it felt appropriate to use PKCE authentication. This is “The Authorization Code [PKCE] is the OAuth 2.0 grant that native apps use in order to access an API”. The Electron app being built had the requirement of communicating with another company owned APIs.

The working solution ended up being to create an Authentication Window (separate from the main window) that would toggle open when authentication was required. All communication with Auth0 to handle the PKCE flow was then handled on a backend process, communicating any payloads or data through the ipcRenderer.

At the end of the day, I was really happy with this. It worked perfectly and didn’t depend on any front-end logic (Vue, React, Angular, etc…) to handle authentication.

Here’s the implementation:

Step 1: Create an Authentication Handler

For the sake of tidying up the background.js file, I created a separate directory for “handlers” and created a authenticator.js file.

// app/src/handlers/authenticator.jsimport AuthCodePKCE from '../../auth'let authenticationWindow
let pkceAuthenticator
// Auth processes
export const authController = (ipcMain, BrowserWindow, mainWindow) => {
const createAuthenticationWindow = () => {
const window = new BrowserWindow({
webPreferences: {
webSecurity: false
},
parent: mainWindow,
resizable: false,
modal: true,
height: 475,
width: 300,
show: true
})
// Create a new instance of the PKCE helper
pkceAuthenticator = new AuthCodePKCE()
window.loadURL(pkceAuthenticator.authorizationUrl())
window.once('ready-to-show', () => window.show())
return window
}
ipcMain.on('authenticate_user', event => authenticationWindow = createAuthenticationWindow())return {
close () {
authenticationWindow.close()
},
fetchToken (code) {
pkceAuthenticator.fetchAccessToken(code)
.then(authResult => {
mainWindow.webContents.send('authentication-success', authResult)
authenticationWindow.close()
})
}
}
}

A few things are being handled here. The first is that we are storing the logic for creating the authentication window in a single function called createAuthenticationWindow, which is called when the ipcMain detects an “authenticate_user” event. One thing to note here (though it’s optional) is that the new BrowserWindow being created is specified to be a modal with its parent set as the mainWindow. This works really well, as it handles the window positioning, makes sure the user doesn’t exit the authentication window and temporarily disables the user from navigating the app.

Second, the authController itself returns two functions. The first one close() closes the authentication window. The second, fetchToken() is what we will be using in the second Step 3 to retrieve an API token after the user has successfully authenticated.

The third thing to note is that the authentication window is NOT loading a local file or template, but a specific URL provided by the AuthCodePKCE that is imported at the beginning of this file.

// app/src/auth.jsimport crypto from 'crypto'
import * as queryString from 'query-string'
import { domain, clientId } from './utils/init'
import * as requestPromise from 'request-promise'
const base64URLEncode = (str) => {
return str.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '')
}
const sha256 = (buffer) => {
return crypto.createHash('sha256').update(buffer).digest()
}
// Authorization Code with PKCE is the OAuth 2.0
// grant that native applications use in order
// to access an API.
export default class AuthCodePKCE {
// The verifier and the challenge need to be stored
// for subsequent requests. Make sure to keep a reference
// to the AuthCodePKCE instance during grant flow
constructor () {
this.verifier = base64URLEncode(crypto.randomBytes(32))
this.challenge = base64URLEncode(sha256(this.verifier))
}
// Generate link to send the user to the authorization URL
// with the code_challenge and the method used to generate it
authorizationUrl () {
return `https://${domain}/authorize?${queryString.stringify({
redirect_uri: `my-app://desktop.com/auth`,
code_challenge: this.challenge,
code_challenge_method: 'S256',
audience: 'my/api',
scope: 'read:stuff',
response_type: 'code',
client_id: clientId
})}`
}
// Exchange code for an Access Token that can be used to call your API.
// Using the Authorization Code (code) from the previous step, you will
// need to POST to the Token URL sending also the code_verifier
fetchAccessToken (code) {
return requestPromise({
method: 'POST',
uri: `https://${domain}/oauth/token`,
body: {
redirect_uri: `my-app://desktop.com/auth`,
grant_type: 'authorization_code',
code_verifier: this.verifier,
client_id: clientId,
code: code
},
json: true
})
}
}

This simple class handles the two main aspects of PKCE auth, the creation (and storage) of a challenge and a verifier. If you want to read more about what those are, read about them on Auth0 Docs. That aside, what’s worth paying attention to here is the authorizationUrl() method that we just used in our authentication.js file.

Essentially, it’s creating a URL string that points to Auth0 at https://<auth0-domain>/authorize? and has a number of required/optional query params. Namely, these query params include the code challenge and the method the code challenge was created with. This URL will load Auth0’s hosted sign in page in the Electron authentication window we created (you can customize that page on Auth0).

Step 2: Use the Handler to Fetch a Code

Now, let’s look at our background.js file. Please keep in mind that I trimmed this file down a bit, so it may be missing some Electron required javascript.

// app/src/background.jsimport { authHandler } from './handlers/authenticator.js'import { isDev, PROTOCOL_PREFIX } from './utils/init'
import { format as formatUrl, parse as parseUrl } from 'url'
import { app, protocol, BrowserWindow, ipcMain } from 'electron'
// Helper function for creating and formatting paths
const baseUrl = (opts = {}) => {
let base = {
pathname: path.join(__dirname, 'index.html'),
protocol: 'file',
slashes: true
}
if (isDev) {
base = parseUrl(process.env.WEBPACK_DEV_SERVER_URL)
}
return formatUrl(Object.assign(base, opts))
}
// Define protocol prefix (e.g. "my-app-protocol")
protocol.registerStandardSchemes([PROTOCOL_PREFIX])
let mainWindow
let authenticator
const createMainWindow = () => {const window = new BrowserWindow({
webPreferences: {
webSecurity: false
},
width: 500,
height: 500,
show: false,
minWidth: 780,
minHeight: 475,
icon: path.join(__dirname, 'build/icons/png/64x64.png')
})
if (!isDev) createProtocol('app')window.loadURL(baseUrl())window.on('closed', () => mainWindow = null)
window.once('ready-to-show', () => window.show())
return window
}
// Use the protocolRouter to match 'auth' in request urlconst protocolRouter = (request) => {
let url = parseUrl(request.url)
if (url.path.match(/auth\?code/)) {
// Get code from url query and send code to ipcMain event
authenticator.fetchToken(url.query.split('=')[1])
}
}
app.on('ready', async () => {
mainWindow = createMainWindow()
protocol.registerHttpProtocol(PROTOCOL_PREFIX, protocolRouter)
// Set up authenticator
authenticator = authHandler(ipcMain, BrowserWindow, mainWindow)
})

After importing the authHandler and other required/helpful modules, a new function gets defined called protocolRouter. This could be expanded on much further, however here we’ll just use it as is. Its’ purpose serves as a simple router function that gets called whenever a new request is made to our app’s protocol. If you look inside the app.on('ready'… definition, you can see that we pass that router as an argument to the protocol.registerHttpProtocol function. This is an Electron API, and it essentially lets us define a callback (or in our case, a router) to be used with a given protocol scheme.

For example, let’s say we define a protocol as PROTOCOL_PREFIX = "my-electron-app" and then continued with our same setup protocol.registerHttpProtocol(PROTOCOL_PREFIX, protocolRouter). If you then type into a browser my-electron-app://something?param=1, the request would open the desktop app and call our protocolRouter, providing it the request object as a parameter.

Finally, as you can see, we setup the authHandler by passing it the ipcMain, BrowserWindow, and mainWindow as arguments. It will use all these in it’s set up (reference code above).

Step 3: Seeing How it Works

Now that everything is set up, for the most part, I’ll explain the final bits.

const protocolRouter = (request) => {
let url = parseUrl(request.url)
if (url.path.match(/auth\?code/)) {
// Get code from url query and send code to ipcMain event
authenticator.fetchToken(url.query.split('=')[1])
}
}

Looking back at the protocolRouter in the background.js file, we can see that it’s set up to match "auth?code” in the request path. This is flexible to be set when creating the authenticationUrl (set to redirect_uri: my-app://desktop.com/auth in this case), and Auth0 will add the query param code=<a code>. If a match is made, we get the code value and pass it to the fetchToken() method that was defined by the authHelper.

fetchToken (code) {
pkceAuthenticator.fetchAccessToken(code)
.then(authResult => {
// Alert main window of authentication success
mainWindow.webContents.send('auth-success', authResult)
authenticationWindow.close()
})
}

At this point, everything is pretty straight forward. fetchToken uses the fetchAccessToken to retrieve the API token, which then returns the authentication result. You’re then able to do whatever you want with the result. In this example, I’m sending it to my main browser window where an ipcRenderer is listening to “auth-success”.

fetchAccessToken (code) {
return requestPromise({
method: 'POST',
uri: `https://${domain}/oauth/token`,
body: {
redirect_uri: `my-app://desktop.com/auth`,
grant_type: 'authorization_code',
code_verifier: this.verifier,
client_id: clientId,
code: code
},
json: true
})
}
  • Note: FetchAccessToken sends a POST request to the https://<auth0-domain>/oauth/token endpoint, giving it a JSON load of the code with several other values. I used requestPromise to handle this, though you can use anything.

Conclusion

Hopefully, you found this helpful! If you have any questions, feel free to ask. Happy to hear any suggestions on how to improve this.

--

--

Sebastian Scholl
Sebastian Scholl

Written by Sebastian Scholl

Wrestling ideas, sharing experiences.

Responses (3)