Securing Electron App with Clerk
Hello, this is a serial blogpost about Electron architecture.
I will write about the electron architecture that I choose, but before that, I’d tell you the gist, since it is required to understand why I implement this authentication mechanism.
First, as we know it, Electron has two processes, Main
and Renderer
. If you’re not familiar with it, I recommend you to read the official documentation.
Essentially, Main
is process that running NodeJS in the OS that installed our electron app. Where as Renderer
is a process that runs on browser.
Renderer
process responsible mainly to handle user interface.
Main
and Renderer
can communicate to each other using “Inter-Process Communication” a.k.a “IPC”.
It’s not a simple thing to do, so to make the codebase provide maximum productivity, I decided to use TRPC
as a layer on top of native Electron IPC.
To do that I use electron-trpc
package.
Okay, from this point, you should have everything you should know to follow this writing.
To give a simple visualization, this is how electron processes communicate:
MAIN PROCESS <---> IPC (TRPC) <----> RENDERER PROCESS
And the overal electron architecture looks like this:
As you can see, the only way to make requests to internet is in the Main
process.
So if I secure my API (which again, using TRPC protocol) using JWT token, I need to have JWT token in the Main
process.
That is not a limitation from Electron, it’s a deliberate decision from me, because I want to handle everything in the Main
process, the state, the logic, saving to database, connecting to internet, etc.
That way, I think my app will be easier to reason about, and Main
process become single source of truth, where the Renderer
process only responsible for displaying data and capture user interaction.
(But that might change if I find it inflexible after sometime using this current architecture decision)
Now back to authentication part, since I’m using Clerk
in the Renderer
process, I could just get it from the Renderer
process, which means I can send token to Main
process and let it save it somewhere (in the Machine
ideally).
Okay, my first intuition is that, it’s simple, I just need to fetch JWT token
from Clerk
everytime a new Renderer
process is created (When new window is created). This looks like this:
The problem with this is that, JWT token ideally (and by default on Clerk) has lifetime of 60seconds. Which means, If the user is opening the window and using it more that 60seconds (1min), and then perform something that make internet authenticated request to my server, the JWT will be expired already.
I do not want to poll every 60s just to get latest token (especially if not needed), and I also do not want to sacrifice the security by increasing token expiration time.
Now, this is clear that I can’t implement above mechanism.
Ok, if the token will expire in 60s, then I guess I can just call the getToken
right before I want to make an authentiated request.
And I do it in the Main
process. From the Renderer
I just need to send sessionId
instead of JWT token
, then in Main
I use that sessionId
to fetch the latest JWT token
. Like this:
But, I realize that when using this mechanism, it means I have to use @clerk/clerk-sdk-node
which means I have to supply SECRET_KEY
somewhere in the codebase.
Electron app is not the best place to save such sensitive information, eventhough it’s like NodeJS, but it runs on user OS not on our server where we can just use environment
variable to store sensitive data.
So, again, this mechanism is not possible.
After some testing, finally, I come to the mechanism that fits my need perfectly.
It’s almost similar to the first mechanism, but instead of fetching JWT Token
once, it will fetch it everytime Renderer
process wants to call Trpc Main
procedure.
And it should happend automatically, such that there’s no difference between calling public procedure and authenticated procedure.
As a developer, I didn’t have to manually fetch the token the supply it as input / argument to the TRPC procedure.
I want the Trpc Client handle that. Like this:
This works really well.
But to make it works, first I have to modify the ipcLink
from electron-trpc
package, as well as the createIPCHandler
.
Those modification is required to be able to “automatically” fetch the JWT Token
from Clerk Server
then sends it to Main
process.
Which then the Main
process will store it, and then the TRPC Internet Client
will be able to get latest token
right before making request to internet server.
With all those sets, I can now call TRPC
procedure just like before (seems no change), but under the hood, the request will know if I’m authenticated or not as a long as the ClerkProvider
is set up in the React app.
And that’s it.
That’s how I secure my Electron app with Clerk
.
And you might be wondering, what’s the server I’m using? Well any server that Clerk
supports can handle that.
In my case, I’m using Cloudflare worker. I will make another post about this later.