Owning your app - Part 1: Authentication
A deep dive into the benefits and trade-offs of owning your app's authentication
July 19, 2024Recently, I discussed the importance of "owning your app" in the Javascript ecosystem and the problems that stem from relying too heavily on third-party services and dependencies. This post focuses specifically on authentication, if you want an overview of what this series is about, I suggest you read the other article first.
Authentication is a beast, arguably the most complex and error-prone part of setting up any app, and a critical aspect of truly owning your app. Fully comprehensive authentication solutions, offered by the likes of Clerk, Auth0, and Supabase, can take care of not just the actual authentication and session management but also provide social logins, password reset functionality (with emails), two-factor authentication (2FA), pre-built UI components and more. When setting up an app it can be temping to reach for one of these instead of handling everything manually in your code, but as your app grows, the real costs can start to surface.
Third-party authentication services
One of the biggest risks of coupling your app's authentication with a third-party service is vendor lock-in. Authentication is generally pretty central to how a web app works and if you build your base off the back of a vendor's specific API's and SDKs, it can be incredibly time-consuming, or in some cases practically impossible, to switch to a different provider or migrate to a self-hosted solution. You're essentially at the mercy of the vendor, subject to their pricing changes, feature limitations, and roadmap decisions.
If you're okay with the lock-in, you must also be okay with the pricing. Pretty much all authentication providers offer a free tier which, if you plan on making any money, you will eventually hit the ceiling at which point your free ride begins eating into your margins. This problem can be especially expensive if you have a free tier or if you plan on going B2C.
You're also completely exposed to the provider's maintenance and release cycles. By coupling your app's authentication to their service, you must update your app and its code as their APIs and SDKs develop, which can be pretty time-consuming and stressful if you have a relatively complex app. You're also exposing your users to the provider's their uptime, which in the case of authentication, can mean locking all logged-in users out of your app in the event of an outage.
All of these things combined can become tech debt and hugely hinder your ability to actually deliver features to your customers.
"Rolling your own auth"
Given these drawbacks, you might be wondering what the solution is. Enter the concept of "rolling your own auth" - a term that's often misunderstood and, in the JavaScript world, sometimes unfairly maligned.
The term "rolling your own auth" has traditionally been associated with implementing authentication algorithms from scratch in your code, which can be daunting and error-prone, and should be avoided. In modern web development, however, it's been redefined to mean "leveraging battle-tested authentication libraries and frameworks instead of relying on third-party services". This is a problem.
In traditional SSR frameworks like Laravel, Rails, and Django, "rolling your own auth" is not frowned upon and is actually the expected approach. Authentication in deeply integrated at the core of these frameworks and it's generally always the right move to choose these first-party, battle-tested libraries instead of reaching for a third-party service.
The JavaScript ecosystem at large, however, does not generally choose batteries-included frameworks, instead preferring a more modular approach. The result is the authentication libraries grow somewhat separately to the frameworks themselves, spawning the likes of Auth.js
(formerly NextAuth) and remix-auth
. Despite the existence of these battle-tested libraries, the concept of "rolling your own auth" in JavaScript still comes with a certain level of apprehension.
Let me be clear: it's not as daunting as you might think, and the benefits are substantial.
Firstly, using an off-the-shelf library allows you to own your users outright in your database. This means that you have total control over your user data and can easily migrate or switch your authentication library over time if you wanted. This is only possible with in-house authentication as the implementation and storage of your authentication are completely decoupled. If you used to use Passport.js
and now you want to use Auth.js
, it's possible to switch without any downtime. To go even further, you could even rewrite your app using a totally different stack and your users would be unaffected as their user information, password information and even their session information all exists in your database.
Owning the user information in your own database also completely removes the need to worry about Daily Active Users (DAU) or Monthly Active Users (MAU) costs as you are not bound by third-party provider fees. The cost of onboarding a new user to your app from an authentication perspective is, for all intents and purposes, zero.
Authentication libraries have also matured a lot over the years and have long moved past basic login and session management. Auth.js
and remix-auth
both support social logins (extendable to any OAuth provider), magic links, WebAuthn and 2FA. They also support handling sessions with HTTP-only cookies, rather than Javascript-readable JWTs, which is something that most of the third-party authentication services don't.
A quick note on JWTs in Localstorage
Javascript-readable JWTs, usually either in LocalStorage or non-HTTP-only cookies, are broadly deemed less secure than session cookies as they cannot be revoked individually without the use of a centralised table. This means if you need to quickly ban a malicious user, alter a user's permissions due to a role change, or invalidate a compromised token, you would need to wait for the token to expire naturally or force all users to log out and obtain new tokens. Managing sessions with cookies, and querying centralised sessions table on each request, nullifies all of these issues.
OAuth-only authentication
If you still don't fancy handling passwords, there's an even simpler approach: OAuth-only authentication. By only supporting OAuth, provided by the likes of Google, GitHub, and Facebook, you eliminate the need to store or manage passwords, removing a significant security risk, while maintaining full control over sessions and user data.
OAuth can also result in a simpler experience for both you (the developer) and your customers. You will end up writing less code as you no longer have to manage passwords and their adjacent functionalities (e.g. resets) and your customers won't have to remember what sign-in method they used when creating their account.
Risks
It wouldn't be fair to talk about in-house authentication without talking about the risks. While you aren't exposed to the risk of your third-party provider getting breached, you must still ensure it doesn't happen to you. The libraries I've mentioned will handle most of the application-level logic around implementing the cryptographic algorithms, but it's often on you to make sure you're using the library correctly and also to securely persist the data to your database. Thankfully, there are well-established rules to follow and there's plenty of guides around properly hashing and salting passwords, securely generating and storing encryption keys, and following best practices for database security.
Wrapping up
I'm hoping this article has helped dispel some of the myths around "rolling your own auth" and show you that it's not as scary as it may seem.
If you're looking for a starter kit for your next web app and you want first-party authentication, as well as emails, database integrations, payments, storage, and caching - all without any vendor lock-in - please check out Launchway.
Alternatively if you found this at all interesting, please consider signing up for the newsletter or simply following me on Twitter where I rant about things like this often.