{"id":174,"date":"2021-12-11T07:42:12","date_gmt":"2021-12-11T07:42:12","guid":{"rendered":"https:\/\/engy.us\/blog\/?p=174"},"modified":"2022-12-27T18:09:24","modified_gmt":"2022-12-27T18:09:24","slug":"aws-cognito-authentication-in-electron","status":"publish","type":"post","link":"https:\/\/engy.us\/blog\/2021\/12\/11\/aws-cognito-authentication-in-electron\/","title":{"rendered":"AWS Cognito Authentication in Electron"},"content":{"rendered":"\n<p>The AWS Cognito authentication service as of this writing does not officially support the Electron platform. But there is a <a href=\"https:\/\/docs.amplify.aws\/lib\/auth\/getting-started\/q\/platform\/js\/\">Javascript SDK for Cognito, as part of AWS Amplify<\/a>. Others have tried using it on Electron but have <a href=\"https:\/\/github.com\/aws-amplify\/amplify-js\/issues\/3321\">run into issues<\/a>. I ran into several more than what are described in that thread, so I&#8217;ll go over the stumbling blocks and how I managed to get around each one.<\/p>\n\n\n\n<p>In my case I&#8217;m using Federated authentication (via Google) through Cognito User Pools. I&#8217;m calling <code>Auth.federatedSignIn()<\/code> from the preload section, but I think this would still work if you ran it with the other page code.<\/p>\n\n\n\n<h2>Making the auth happen in a different window<\/h2>\n\n\n\n<p>AWS sends you off to another webpage to log in, via <code>window.open(&lt;url&gt;, \"_self\")<\/code>. 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.<\/p>\n\n\n\n<p>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:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Strips off the \"_self\" that was added by the AWS Amplify SDK, to open auth in a new window.\nconst oldWindowOpen = window.open;\nwindow.open = &lt;any&gt;((url: string) =&gt; {\n    return oldWindowOpen(url);\n});<\/code><\/pre>\n\n\n\n<p>You might need to refine this to only mess with auth URLs if you have other bits of code that you expect to call <code>window.open()<\/code>.<\/p>\n\n\n\n<p>But after this change, the auth page gets opened in a new window.<\/p>\n\n\n\n<h2>Allowing the auth window to open<\/h2>\n\n\n\n<p>At this point it&#8217;s trying to open a new window, but my boilerplate Electron setup had a <code>setWindowHandler()<\/code> call that was blocking it. To fix I updated the code to allow when the URL was for my auth service:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>this.MainWindow.webContents.setWindowOpenHandler(({ url }) =&gt; {\n    if (url.startsWith('https:\/\/my-service.auth.us-east-2.amazoncognito.com\/')) {\n        return { action: 'allow' };\n    } else {\n        return { action: 'deny' };\n    }\n});<\/code><\/pre>\n\n\n\n<h2>Detecting when authentication is finished<\/h2>\n\n\n\n<p>Normally when using this authentication library on a website it will redirect back to your website, where you can handle the code it&#8217;s given you on the URL. In my case I have <code>redirectSignIn<\/code> and <code>redirectSignOut<\/code> set to <code>http:\/\/localhost:49391\/<\/code> . I don&#8217;t actually have anything running on port 49391, but in our case we don&#8217;t need to! We can instead add an <a href=\"https:\/\/www.electronjs.org\/docs\/latest\/api\/web-request\">onBeforeRequest listener<\/a> to intercept it and handle it. In our setup in the main process:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>this.MainWindow.webContents.on(\"did-create-window\", (window, details) =&gt; {\n    if (details.url.includes(\"amazoncognito.com\")) {\n        this.authWindow = window;\n    }\n\n});\n\nsession.defaultSession.webRequest.onBeforeRequest(\n    { urls: &#91;\"http:\/\/localhost:49391\/*\"] },\n    (details: Electron.OnBeforeRequestListenerDetails, callback: (response: Electron.Response) =&gt; void) =&gt; {\n\n        this.MainWindow.webContents.send(\"handleAuthResponse\", details.url);\n        this.authWindow?.close();\n\n        callback({ cancel: true });\n    });<\/code><\/pre>\n\n\n\n<p>We capture the auth window when it&#8217;s created, then when we see a request come in for <code>http:\/\/localhost:49391<\/code> 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.<\/p>\n\n\n\n<p>You will also need to update your app client in the AWS Cognito console:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-attachment-id=\"180\" data-permalink=\"https:\/\/engy.us\/blog\/2021\/12\/11\/aws-cognito-authentication-in-electron\/image-4\/\" data-orig-file=\"https:\/\/i2.wp.com\/engy.us\/blog\/wp-content\/uploads\/2021\/12\/image.png?fit=1021%2C275&amp;ssl=1\" data-orig-size=\"1021,275\" data-comments-opened=\"1\" data-image-meta=\"{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}\" data-image-title=\"image\" data-image-description=\"\" data-medium-file=\"https:\/\/i2.wp.com\/engy.us\/blog\/wp-content\/uploads\/2021\/12\/image.png?fit=300%2C81&amp;ssl=1\" data-large-file=\"https:\/\/i2.wp.com\/engy.us\/blog\/wp-content\/uploads\/2021\/12\/image.png?fit=525%2C141&amp;ssl=1\" loading=\"lazy\" width=\"525\" height=\"141\" src=\"https:\/\/i2.wp.com\/engy.us\/blog\/wp-content\/uploads\/2021\/12\/image.png?resize=525%2C141&#038;ssl=1\" alt=\"\" class=\"wp-image-180\" srcset=\"https:\/\/i2.wp.com\/engy.us\/blog\/wp-content\/uploads\/2021\/12\/image.png?w=1021&amp;ssl=1 1021w, https:\/\/i2.wp.com\/engy.us\/blog\/wp-content\/uploads\/2021\/12\/image.png?resize=300%2C81&amp;ssl=1 300w, https:\/\/i2.wp.com\/engy.us\/blog\/wp-content\/uploads\/2021\/12\/image.png?resize=768%2C207&amp;ssl=1 768w\" sizes=\"(max-width: 767px) 89vw, (max-width: 1000px) 54vw, (max-width: 1071px) 543px, 580px\" data-recalc-dims=\"1\" \/><\/figure>\n\n\n\n<h2>Passing the success URL back to the Amplify library<\/h2>\n\n\n\n<p>We need to set up a listener in our render preload script to get the IPC message:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ipcRenderer.on(\"handleAuthResponse\", (event, url) =&gt; {\n\n    (&lt;any&gt;Auth)._handleAuthResponse(url);\n});<\/code><\/pre>\n\n\n\n<p><code>_handleAuthResponse<\/code> 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 <code>aws-amplify<\/code> 4.3.10. Should work if I don&#8217;t upgrade the package until they&#8217;ve added proper support, right? \ud83e\udd1e<\/p>\n\n\n\n<p>In my case I&#8217;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&#8217;ve been doing auth calls there.<\/p>\n\n\n\n<h2>Patching up history.replaceState<\/h2>\n\n\n\n<p>The <code>aws-amplify<\/code> library tries to call <code>history.replaceState<\/code> 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:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Object.defineProperty(history, \"replaceState\", {\n    configurable: true,\n    value: () =&gt; {}\n});<\/code><\/pre>\n\n\n\n<p>We don&#8217;t need to do this history scrub anyway because the window with the URL in it is getting closed.<\/p>\n\n\n\n<p>I don&#8217;t need to call <code>history.replaceState()<\/code> normally in my app, but I would restore it back to its old value after if it became important.<\/p>\n\n\n\n<h2>Now you&#8217;re logged in!<\/h2>\n\n\n\n<p>At this point the federated authentication is complete and you can proceed as normal as the AWS Amplify docs direct. It should fire the &#8220;signIn&#8221; event on the Hub under the &#8220;auth&#8221; channel as normal. In my case for the User Pool I needed to call <code>Auth.currentUserInfo()<\/code> or <code>Auth.currentUserPoolUser()<\/code>.<\/p>\n\n\n\n<h2>Update &#8211; October 2022<\/h2>\n\n\n\n<p>I tried updating Electron from 16 to 21 and this method stopped working. The AWS SDK now thinks that it&#8217;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&#8217;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.<\/p>\n\n\n\n<h2>Update &#8211; December 2022<\/h2>\n\n\n\n<p>I also found out that the AWS Amplify SDK doesn&#8217;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&#8217;s a lot more expensive than Cognito but at least they support Electron. The <a href=\"https:\/\/auth0.com\/blog\/securing-electron-applications-with-openid-connect-and-oauth-2\/\">Auth0 electron tutorial<\/a> is excellent and only requires tiny dependencies like jwt-decode, so you can keep your code lean and mean.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>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&#8217;ll &hellip; <\/p>\n<p class=\"link-more\"><a href=\"https:\/\/engy.us\/blog\/2021\/12\/11\/aws-cognito-authentication-in-electron\/\" class=\"more-link\">Continue reading<span class=\"screen-reader-text\"> &#8220;AWS Cognito Authentication in Electron&#8221;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"spay_email":"","jetpack_publicize_message":"","jetpack_is_tweetstorm":false},"categories":[1],"tags":[],"jetpack_featured_media_url":"","jetpack_publicize_connections":[],"jetpack_sharing_enabled":true,"jetpack_shortlink":"https:\/\/wp.me\/pahBcK-2O","jetpack-related-posts":[{"id":131,"url":"https:\/\/engy.us\/blog\/2020\/05\/21\/aws-appsync-resolvers-finding-what-youve-got-in-context\/","url_meta":{"origin":174,"position":0},"title":"AWS AppSync Resolvers - Finding what you've got in context","date":"May 21, 2020","format":false,"excerpt":"When you write your own Resolvers for AWS AppSync, you are given a $context object that contains a lot of helpful things. In my case I needed to grab a unique ID associated with the user from the Cognito User Pool I set up. But the $ctx.identity.cognitoIdentityId just wasn't there.\u2026","rel":"","context":"Similar post","img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":43,"url":"https:\/\/engy.us\/blog\/2010\/03\/08\/saving-window-size-and-location-in-wpf-and-winforms\/","url_meta":{"origin":174,"position":1},"title":"Saving window size and location in WPF and WinForms","date":"March 8, 2010","format":false,"excerpt":"A common user interface tweak is to save the size and location of a window and restore that state when the program starts up again. That way, the users don\u2019t need to re-do their work of resizing and moving the windows to where they want them. However I\u2019ve found that\u2026","rel":"","context":"With 21 comments","img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":79,"url":"https:\/\/engy.us\/blog\/2018\/10\/03\/wpf-databinding-using-dynamicdata\/","url_meta":{"origin":174,"position":2},"title":"WPF Databinding using DynamicData","date":"October 3, 2018","format":false,"excerpt":"My project is MVVM and I had been using ReactiveList<T> as my go-to observable collection for viewmodel properties. But ReactiveUI deprecated ReactiveList in 8.6.1. So I needed to get on to the new recommended library: DynamicData. But there is no direct drop-in replacement you can do like ObservableCollection<T> <-> ReactiveList<T>.\u2026","rel":"","context":"With 2 comments","img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":156,"url":"https:\/\/engy.us\/blog\/2021\/04\/07\/microsoft-visualstudio-componentmodelhost-dll-issues-in-visual-studio-extensions\/","url_meta":{"origin":174,"position":3},"title":"Microsoft.VisualStudio.ComponentModelHost.dll issues in Visual Studio extensions","date":"April 7, 2021","format":false,"excerpt":"I maintain a Visual Studio extension called Unit Test Boilerplate Generator. Recently I went to release a small fix for it but found that I could no longer build it since reformatting my PC. AppVeyor was also unable to build it after I upgraded it to a VS 2019 build\u2026","rel":"","context":"Similar post","img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":33,"url":"https:\/\/engy.us\/blog\/2010\/03\/31\/using-the-dispatcher-with-mvvm\/","url_meta":{"origin":174,"position":4},"title":"Using the Dispatcher with MVVM","date":"March 31, 2010","format":false,"excerpt":"When writing an MVVM application, you want to separate from the UI. However you also need to make sure that UI updates happen on the UI thread. Changes made through INotifyPropertyChanged get automatically marshaled to the UI thread, so in most cases you\u2019ll be fine. However, when using INotifyCollectionChanged (such\u2026","rel":"","context":"With 12 comments","img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":177,"url":"https:\/\/engy.us\/blog\/2021\/12\/14\/monorepo-with-npm-workspaces-and-typescript\/","url_meta":{"origin":174,"position":5},"title":"Monorepo with NPM Workspaces and Typescript","date":"December 14, 2021","format":false,"excerpt":"I recently needed to set up a project with a monorepo and was searching about for a good guide or example. Nothing I found was quite right. This repo had a rather extensive example but brought in lerna, which I wanted to avoid. This one was a bit simpler but\u2026","rel":"","context":"Similar post","img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]}],"_links":{"self":[{"href":"https:\/\/engy.us\/blog\/wp-json\/wp\/v2\/posts\/174"}],"collection":[{"href":"https:\/\/engy.us\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/engy.us\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/engy.us\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/engy.us\/blog\/wp-json\/wp\/v2\/comments?post=174"}],"version-history":[{"count":6,"href":"https:\/\/engy.us\/blog\/wp-json\/wp\/v2\/posts\/174\/revisions"}],"predecessor-version":[{"id":191,"href":"https:\/\/engy.us\/blog\/wp-json\/wp\/v2\/posts\/174\/revisions\/191"}],"wp:attachment":[{"href":"https:\/\/engy.us\/blog\/wp-json\/wp\/v2\/media?parent=174"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/engy.us\/blog\/wp-json\/wp\/v2\/categories?post=174"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/engy.us\/blog\/wp-json\/wp\/v2\/tags?post=174"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}