Low-Cost Auth, Identity, and Single Sign-On

Providing auth doesn’t have to break the bank

Christopher Sung
Better Programming

--

Photo by Brett Jordan on Unsplash

Auth, in general, presents an interesting use case in the never-ending struggle between build vs. buy. Several premium options are available from providers like Okta, Auth0 (owned by Okta), CloudJump, OneLogin, and others if you’re leaning toward the buy side. These options allow you to integrate on the frontend, the backend, or both, as warranted by your needs.

And yes, it’s true — a lot of engineering hours can be saved upfront by using an out-of-the-box solution. However, even from the outset, the convenience of ramping up will cost you. And on a monthly recurring basis, that cost will quickly grow as your user base expands throughout the lifetime of that integration.

The simple truth is that auth services can be expensive. Once a certain threshold of users has been reached, the costs start to pile up staggeringly, cutting into profitability at the very core of our products and services.

But building and maintaining our own system for authentication, identity, and authorization seems like a fool’s errand. The last thing we want is to be responsible for protecting sensitive credentials from all that lurks among the publicly facing internet. This is why we pay third-party vendors to house that information so that they assume the risk instead of us.

Perhaps there’s a tradeoff to be made somewhere in our development process, whether we’re starting greenfield in a new proof of concept or MVP or migrating away from a costly initial solution in an existing implementation.

Our sample use case

Let’s look at an example scenario.

We’re running a large e-commerce site that resells products from major brands. Analysts from these brands want insight into how their products are selling and how conversion rates can be optimized in our storefront. Naturally, we create a business intelligence portal for them, which requires an invite to onboard and access.

Similarly, these brands want levers they can pull from a promotional standpoint, which results in the introduction of native product ads and digital coupons in our web and mobile apps. This necessitates the addition of a marketing portal that some of these same brands will utilize.

We can envision some overlap in the users from a particular brand who use both portals, and hey, wouldn’t it be great if those users just needed one login to access both? A closer peek at the product requirements for such a system might look something like the following:

  • secure storage of user credentials
  • single sign-on (SSO) from multiple sources / platforms
  • onboarding by invite or by self-serve
  • password reset
  • option of social login (Google, Facebook, et al.)
  • option of multi-factor authentication (MFA)

How to approach development

Moving all our chips into the ‘buy’ column will likely prove to be cost-prohibitive. However, as discussed before, we don’t want to handle everything ourselves. So, if we examine what needs to be stood up to achieve this from an engineering standpoint, it comes down to two aspects:

  • create, read, update, delete (CRUD) of a user’s credentials, like email & password combo
  • CRUD of a user’s identities for those credentials depending on scope or context

Armed with these two capabilities, we can:

  • register a new user
  • login an existing user
  • reset a user’s password

OK, that’s all very well and good, but where are we actually going to store said credentials and identity info to satisfy these requirements?

If you’re already using a cloud provider like AWS or GCP, then you know part of their offerings include infrastructure in this area: Cognito & Amplify on AWS, and GCP… Firebase.

I pause on Firebase because anyone who knows me also knows that I have a very soft spot in my heart for Firebase. It is literally the Swiss Army knife of cloud computing. And as it relates to our current situation, it happens to already offer services that can achieve the two aspects outlined above:

And if we look at the pricing for these services, assuming the pay-as-you-go Blaze plan:

  • Firebase Auth: free up to 50K monthly active users
  • Firebase Realtime Database: free up to 1 GB storage

Firebase also allows us to offer auth-as-a-service in our platform, as it has both a Client SDK (for login) and an Admin SDK (for user management) that can be accessed from the backend. Suppose we create a generic interface for our service's main actions (user registration, login, password reset, etc.). In that case, we can keep it vendor-agnostic under the hood if we want to swap it out for another provider. If we decide we’re fine with using some of the client-facing UX that Firebase provides (similar to Amplify and other premium options), that works too, but this solution can run headless.

Finally, regarding auth strategies, Firebase has support for phone, social login, MFA, and more, depending upon your specific needs.

Developing for our use case

In the scenario outlined above, we have two platforms: business intelligence (BI) and marketing. For each of these, there’s a known platform ID (platformId) which provides the context of a specific user’s identity in that platform and the user’s unique ID within that platform (userId).

Let’s assume that the product team has decided on email and password as the authentication strategy. When a platform collects that information for onboarding, they create the new user in their system, which generates the userId, and then send platformId, userId, email, password, and anything related to authorization (like role) to the auth service to register them, either as a new user or as an existing user with a new identity related to that platform.

Similarly, for a login of an existing user, the platform can send platformId, email, and password to the auth service, which first attempts to sign in the user with the given email and password combo and, if successful, looks up the identity related to their email in the specified platform. Remember that since we provide single sign-on (SSO), an email could have multiple identities attached to it — User A has editor privileges in the business intelligence platform but only viewer privileges in the marketing portal.

In each case, upon success, the auth service can take the identity information, encode it in a JWT, and then return it to the platform to use in subsequent API calls on their end. The actual information in an identity, and thus the JWT, depends upon the needs of the participating platforms, but certainly, authorization-related properties (what is this user allowed to do?) are the bare minimum. And because that information is in the JWT, the backend systems of our platforms can then require that in all requests, decoding as needed to determine the permissions or role of the caller.

As far as identities, since we are using Firebase Realtime Database for that persistence, it should be noted that the data structure itself is one large JSON tree. If we need to identify a record uniquely, we create a unique path in the tree to our node of interest. In our use case, the operative identifier for a user is email, and since emails may contain characters ill-suited for keys in our JSON structure, we can hash them to bypass this issue.

So, let’s take a closer look at how to actually implement these two use cases in our auth service.

User registration

First up on the list is the ability to register a new user with a given identity specific to the platform upon which they’re onboarding.

Oy, I can’t believe we’re already this far into the article, and there hasn’t been a single diagram or code snippet yet. Let’s fix that.

The processing flow for a user registration

The above diagram shows the flow for registering a new user from an external request. Let’s break down the steps of how this happens:

  • a POST request is made to the auth service
  • We use the Firebase Admin SDK to create a new user in Firebase Auth with the given email and password
  • We then update the identity info for that email in Firebase Realtime Database. If an entry for that email already exists, then we add it as another platform available for the user. If not, we create a new node for the email with the new identity.
  • Upon success, we then encode the identity info in a JWT and return it to the caller.

Below are some snippets that capture those actions in two helper classes, one for authentication (Firebase Auth) and one for identity (Firebase Realtime Database). Note that these are just guide posts for the calls to be made and their associated logic. In practice, you’ll want to hone the various properties of your authentication and identity types as needed and add validation, error handling, logging, and the like.

In our authentication class, we use the Firebase Admin SDK to create the new user in Firebase Auth:

And in our identity class, we provide a method to either create the user’s identity for the specified platform in our realtime database or add that new identity inside the existing node for that email. We’ll need to define functions that generate those keys (paths) for both to ensure that our users and their associated identities stay within their proper namespaces. Example templates for both might be:

user path: users/<emailHash>
identity path: platform-<platformId>

which in the JSON tree for a new user in our identity system and a platform ID of 5efe0a58fe6c601cb0e79e50 would look like this:

{
"users": {
"9bb42c9007354b4be131cbc242a3d511eaa7d635": {
"platform-5efe0a58fe6c601cb0e79e50": {
"email": "newuser@example.com",
"userId": "5ea727ff3d36c028edbd2039",
"role": "editor"
}
}
}
}

And if that same user should onboard onto another platform with an ID of 606f71ae0de8b9961ca4bfa5, then their identity node would look like this:

{
"users": {
"9bb42c9007354b4be131cbc242a3d511eaa7d635": {
"platform-5efe0a58fe6c601cb0e79e50": {
"email": "newuser@example.com",
"userId": "5ea727ff3d36c028edbd2039",
"role": "editor"
},
"platform-606f71ae0de8b9961ca4bfa5": {
"email": "newuser@example.com",
"userId": "6084023ebbfbdde37335a051",
"role": "viewer"
}
}
}
}

Thus, we’re simply keeping a dictionary of identities within the users namespace based on their hashed email. The code to handle new and existing users looks something like the following:

User login

For login, things are slightly different. The auth service needs to sign in to the given platform on behalf of our user, which means we need to leverage the Firebase Client SDK for this functionality instead of the Firebase Admin SDK used so far.

The processing flow for a user login

The above diagram shows the flow to log in an existing user from an external request. Let’s break down the steps of how this happens:

  • a POST request is made to the auth service
  • We use the Firebase Client SDK to sign in as that user in Firebase Auth with the given email and password
  • We then look up that email's identity info and the specified platform in Firebase Realtime Database.
  • Upon success, we encode the identity info in a JWT and return it to the caller.

In our authentication class, we now use the Firebase Client SDK to sign in:

And in our identity class, we obtain the current dictionary of identities for the user’s email and grab the one associated with the platformId in the request.

One interesting aspect of this design is that none of the platforms using our auth service are actually exposed in these flows. All authentication and identity info are persisted in a separate environment (Firebase), which means in these platforms, we can make it a requirement for a user to be authenticated before they ever make any sensitive request.

The bottom line

Circling back to our main concern, cost, we can take a closer look at what this auth service might run in terms of vendor spend.

Our identity database based upon Firebase Realtime Database is free up to 1GB, and 1GB can hold a ton of identity info, maybe 3 million entries, depending on how many fields we happen to store in each one. But even if we need several orders of magnitude more, say 100GB, that’s still only ~$500 USD / month.

And our auth system allows for 50K active users for free in any given month. Remember that these are active users in a particular month — the number of actual accounts is unlimited. Should these services go viral and result in a million active users per month, the cost is still less than $2500 USD / month.

And with that kind of traffic, my guess is that the monetization of those one million active monthly users will more than make up for that extra $3000 a month. If not, it may be time to have a potentially awkward conversation with the product team. 😬

--

--

Head of Engineering @ Sagespot, creator of BeatCam https://www.instagram.com/beatcam/, @wholenoteguitar, @activebass; @ITP_NYU prof; jazz guitarist in prev life