Explorations and Ramblings in Functional Programming, Mobile and General Development in Typescript *
Deep-Linking in Electron
open in app ? Hell yeah
Deep-Linking in React Native
open in app ? Hell yeah (part 2)
Boosting Peformance in React Native
React Native isn't fast- said noone, ever
Turntable:Exploring OAuth in desktop applications
Research and lessons learned from implementing OAuth in an electron app
Turntable, an app designed to aid in the transfer of playlists and content from music streaming services was born out of a need to move my account data from spotify to youtube music. This is quite a popular problem but all the popular solutions coming with a subscription plan in tow. I don't see a point of this since It's something I'll do probably once at the most.
So, I did what every good developer does, and decided to build it myself, from scratch, and maybe better. I got to building and hit my first roadblock, which in hindsight, should have been very obvious, OAuth. OAuth involves using prexisting social/internet accounts to authenticate into other applications without directly sharing passwords/credentials. OAuth, in web/server hosted applications, is relatively easy, but for desktop applications, that can be a bit of a challenge.
I started looking into handling OAuth within Electron Desktop applications and I found 2 popular options and I'll be going a bit in-depth into each of them and exploring them here and which I ultimately picked (with my own twist)
<br/>Using this method in my opinion is quite crude and probably unsafe but mainly entails creating a new browser window and directing it to load your OAuth providers OAuth page and intercepting the outgoing oauth request to retrieve the OAuth code from the request url.
const authWindow=new BrowserWindow({
webPreferences:{
nodeIntegration:true
}
});
const url=new URL(THIRD_PARTY_LOGIN_URL);
url.searchParams.set(
"redirect_uri",
"https://my-redirect-uri:and_port/callback_url"
);
authWindow.loadUrl(url.toString());
const {session:{webRequest}}=authWindow.webContents;
const filter={urls:["https://my-redirect-uri:and_port/callback_url"]};
webRequest.onBeforeRequest(filter,async({url})=>{
const parsedUrl=new URL(url);
authWindow.close();
const code=parsedUrl.searchParams.get("code_name_from_third_party");
// carry out rest of authorization flow.
})
This method, while quite simple and straightforward, is subject to various edge cases ranging from malformed urls to improperly configured redirect urls and more. This won't do for our next big Email Client that will storm the productivity space.
So, where do we go from here ? Well, that's where the second pattern I discovered comes into play.
<br/>This method is quite simple broadly if an oauth redirect server is something you've implemented before in other contexts. but like I said earlier, I have my own twist on this method.
The default (and frankly, quite boring) way to do this, is the standard way of launching an express or in my case Hono server and having all that run within the main process of your application.
import {Hono} from "hono";
const app=new Hono();
app.get("/oauthcallbackurl",(ctx)=>{
const _url=context.req.url;
const url=new URL(`http://localhost:${PORT}/${_url}`);
const code=url.searchParams.get("oauth_code_params");
if(!code){
return c.text("Failed OAuth Attempt");
}
// go about handling the rest of your auth process
// here...
})
Now, like I said, this is quite boring but also quite unsafe (say it with me? as is all JS code). But I have a better way. I have personally devised a quite elaborate but in my opinion very safe and performant way to carry out this process that has worked for me.
This process, involves using Effect to do basically the same process, but in a separate process. We call these node workers and they come in handy to prevent devasting errors that can break the users experience as well as not bog down the main process.
creating the node worker is quite simple actually
import core from "@my/path/my_file?nodeWorker";
core({name:"my-core"}).postMessage({start:true})
// core.ts
import {parentPort} from "node:worker_threads";
if(!parentPort) throw new Error("COREMSG => Invalid Port");
Now that we have this done, our sub process starts once the core is loaded (ideally from our main.ts) and can launch our api server. Now, I won't go too deep into teaching Effect and it's patterns/ideosyncrasies in this post, but the code is simply verbose which is quite the paradox but looks a bit like this.
import {
HttpRouter,
HttpServer,
HttpServerRequest,
HttpServerResponse,
} from "@effect/platform";
const router=HttpRouter.empty.pipe(HttpRouter.get("/my_callback_url",Effect.gen(function*(){
// intercept the request
const request = yield* HttpServerRequest.HttpServerRequest;
// reconstruct the url
const url = new URL(`http://localhost:{PORT}/${request.url}`);
// retrieve the code from the search params
const code = url.searchParams.get("code");
if(!code){
return yield* HttpServerResponse.json({
successful:false
});
}
// continue the process
})));
Now, the code is quite similar to the previous process but with quite obvious differences in certain places, which I will in-depth in my Learning Effect "series ?" that will help you understand this pattern a lot more but it's largely similar and is my choosen process because error handling becomes a breeze with this
These, are the two main ways I've concluded are the smartest/best ways to go about this process and the second one (with the extra flavor though) is what I use in my personal projects.