AWS Cognito Authentication in Electron

The AWS Cognito authentication service as of this writing does not officially support the Electron platform. But there is a Javascript SDK for Cognito, as part of AWS Amplify. Others have tried using it on Electron but have run into issues. I ran into several more than what are described in that thread, so I’ll go over the stumbling blocks and how I managed to get around each one.

In my case I’m using Federated authentication (via Google) through Cognito User Pools. I’m calling Auth.federatedSignIn() from the preload section, but I think this would still work if you ran it with the other page code.

Making the auth happen in a different window

AWS sends you off to another webpage to log in, via window.open(<url>, "_self"). Depending on your electron configuration, this might actually open a new window, or it might try to open the auth page in the same window.

Opening in the same window is problematic. You can be redirected back to your electron page, but then you might be running a bunch of page/app initialization again. To solve this I monkey-patched window.open:

// Strips off the "_self" that was added by the AWS Amplify SDK, to open auth in a new window.
const oldWindowOpen = window.open;
window.open = <any>((url: string) => {
    return oldWindowOpen(url);
});

You might need to refine this to only mess with auth URLs if you have other bits of code that you expect to call window.open().

But after this change, the auth page gets opened in a new window.

Allowing the auth window to open

At this point it’s trying to open a new window, but my boilerplate Electron setup had a setWindowHandler() call that was blocking it. To fix I updated the code to allow when the URL was for my auth service:

this.MainWindow.webContents.setWindowOpenHandler(({ url }) => {
    if (url.startsWith('https://my-service.auth.us-east-2.amazoncognito.com/')) {
        return { action: 'allow' };
    } else {
        return { action: 'deny' };
    }
});

Detecting when authentication is finished

Normally when using this authentication library on a website it will redirect back to your website, where you can handle the code it’s given you on the URL. In my case I have redirectSignIn and redirectSignOut set to http://localhost:49391/ . I don’t actually have anything running on port 49391, but in our case we don’t need to! We can instead add an onBeforeRequest listener to intercept it and handle it. In our setup in the main process:

this.MainWindow.webContents.on("did-create-window", (window, details) => {
    if (details.url.includes("amazoncognito.com")) {
        this.authWindow = window;
    }

});

session.defaultSession.webRequest.onBeforeRequest(
    { urls: ["http://localhost:49391/*"] },
    (details: Electron.OnBeforeRequestListenerDetails, callback: (response: Electron.Response) => void) => {

        this.MainWindow.webContents.send("handleAuthResponse", details.url);
        this.authWindow?.close();

        callback({ cancel: true });
    });

We capture the auth window when it’s created, then when we see a request come in for http://localhost:49391 we intercept it, dispatch a message to our renderer process, close the auth window, then cancel the request. Cancelling the request using the callback makes sure that nobody else can set up a server on port 49391 and listen in for our auth key.

You will also need to update your app client in the AWS Cognito console:

Passing the success URL back to the Amplify library

We need to set up a listener in our render preload script to get the IPC message:

ipcRenderer.on("handleAuthResponse", (event, url) => {

    (<any>Auth)._handleAuthResponse(url);
});

_handleAuthResponse is a private method on the Auth object that we need to invoke and provide the success URL with the returned key. Obviously, there are major caveats with using a private method as it could break at any point, but it works as of aws-amplify 4.3.10. Should work if I don’t upgrade the package until they’ve added proper support, right? 🤞

In my case I’m running all my auth code inside the preload context so I directly call from there, but you can also send it back to the browser context if you’ve been doing auth calls there.

Patching up history.replaceState

The aws-amplify library tries to call history.replaceState at one point in _handleAuthResponse, to attempt to scrub the URL with the key from the browser back history. But this failed in Electron for me. To fix, I replaced that API with a stub:

Object.defineProperty(history, "replaceState", {
    configurable: true,
    value: () => {}
});

We don’t need to do this history scrub anyway because the window with the URL in it is getting closed.

I don’t need to call history.replaceState() normally in my app, but I would restore it back to its old value after if it became important.

Now you’re logged in!

At this point the federated authentication is complete and you can proceed as normal as the AWS Amplify docs direct. It should fire the “signIn” event on the Hub under the “auth” channel as normal. In my case for the User Pool I needed to call Auth.currentUserInfo() or Auth.currentUserPoolUser().

Update – October 2022

I tried updating Electron from 16 to 21 and this method stopped working. The AWS SDK now thinks that it’s in a Node environment and starts trying to call Node APIs. Which fail because nodeIntegration is disabled on that page (which it should be for security reasons). If I want to continue the hack route I’ll need some way to fool the AWS SDK into thinking its actually in a web page. Or switch to Auth0 which actually has a promising tutorial for integration with Electron.

Update – December 2022

I also found out that the AWS Amplify SDK doesn’t tree shake well and hauled in 1.64 MB of minified Javascript to my preload script, which was horribly slowing down cold startup time. I switched over to Auth0 and the startup time recovered. It’s a lot more expensive than Cognito but at least they support Electron. The Auth0 electron tutorial is excellent and only requires tiny dependencies like jwt-decode, so you can keep your code lean and mean.

Leave a Reply

Your email address will not be published. Required fields are marked *