Back in July 2024, the customer success team came into our office.
Our new customers were ready to start using our application.
All that was left to do was to configure SSO (Single Sign-On) on both sides.
I paused.
We had never had SSO.
The sales team not only had sold our client on that, they literally had billed them for it.
The onboarding was in two weeks.
My understanding of SSO was blurry at best, since the occasion never presented itself to work on it.
That time was now.
I asked the other team leads to give me a couple of hours.
This way I could come back to them with a time budget in hand and a rough plan of how we were going to make this work.
I ~~grabbed~~ reached out to Gaétan, a friend of mine, and the then CTO of the company we were sharing the floor with.
Gaétan patiently outlined for me how the SSO authentication flow worked, and answered all my silly questions.
To whom it may be of use, I'm outlining here the SSO authentication flow.
Imagine an amusement park:
1. You try to get on a ride.
2. The ride attendant stops you. They don't know whether you're allowed on the ride. They send you back to the ticket booth.
3. The booth checks your ID, verifies you paid, and issues you a secured wristband.
4. You go back to the ride attendant.
5. They check your wristband.
6. If it's legit, they let you on the ride.
Now you can just replace:
- 'ride' with 'App'
- 'ride attendant' with 'ACS URL'
- 'ticket booth' with 'Identity Provider'
- 'wristband' with 'SAML Assertion'
You now know how SSO works.
Its technical implementation, however, can be quite time-consuming.
This is in part because there are many IDPs (Identity Providers) out there, each with its own quirks.
For this reason, if you can treat it as a black box, you should.
Smaller teams like mine definitely fall under that category.
We had implemented our own authentication flow, using a simple password / JWT exchange. So my first instinct was to keep our authentication system unified and move it to a provider who had SSO covered.
The first candidate I had in mind was [Auth0](https://auth0.com/). Unfortunately, (1) the documentation was unclear to me, (2) and I knew from experience Okta (who bought Auth0) doesn't speak to customers who spend under $100K annually with them.
Gaétan suggested the product they were using at the time, a French startup. The pricing felt reasonable, but was out of the budget I was managing nonetheless.
I had played around with [Supabase](https://supabase.com/) in the past. I knew they had a solid batteries-included authentication flow, great documentation, and had just announced GA (General Availability). They checked all the boxes.
Now came the question of migrating our user base to this new authentication system.
Let's start with the fact our user base was everything but tech-savvy.
We're talking about bakers and plumbers that get up at 4am.
They had better things to do than thinking about the fact you had to deal with implementing SSO for another client.
That's a you problem in their world, and rightfully so.
Given that we did not know their passwords (we obviously stored the passwords' hashes only), we could not move them to the new system overnight without them.
We could have easily invalidated all of the passwords at once and sent reset links to everyone.
However, our users call support before checking their email, let alone trying to reset their password on their own.
This would have unleashed havoc on our great but small support team, with all active users reaching out at the same time.
Additionally, if users get locked out just because they missed an email, our client companies lose money.
Our application is used to monitor and maintain business equipment. If a bakery's oven doesn't get fixed, the bread doesn't get baked. I think you'll agree you can hardly sell the air within the bread separately.
Invalidating passwords all at once being out of the picture, the next viable option was a silent migration, forwarding passwords to the new authentication system to be securely hashed and stored as users entered their passwords after being logged out.
However, (1) the tokens we issued had a long expiration. This was done at the request of our clients, so that end users wouldn't have to struggle with logging in so often. (2) Remember this was in July. Most people were on vacation.
Even if we had been proactive (so much for the silent migration), we could not have completed it in this timeframe. We'd have to revert to the global instant invalidation option anyway. We'd have to face the havoc.
I guess this is what the French call being stuck between two chairs and having to sit.
But who said you can't sit on the floor?
Going back to our constraints:
- we had two weeks (not talking about the other current & urgent projects)
- we couldn't change our whole authentication flow
- our client needed to connect with SSO using its IDP (Google)
The solution I saw emerging was to treat SSO just the way we treated our own authentication form, rather than putting it at the same level as our authentication flow.
Instead of typing their password, SSO users got routed to Supabase's authentication flow, which we treated as a blackbox.
Upon success, they'd knock on our backend's door showing a Supabase token. We could verify this token the same way we verified passwords, and issue them a token of our own for the rest of their session.
I learned later this is standard practice.
What is not standard, however, is splitting your user databases.
We still had to figure out how to keep our system and Supabase in sync.
Because we couldn't automatically pull the client's employee list from their IDP, their admins still had to manually create new users inside our application.
What's more, our system required specific permissions to be assigned upon user creation anyway.
Our app had to remain the source of truth, but Supabase needed to know who was allowed to connect using SSO.
>"How often have I said to you that when you have eliminated the impossible, whatever remains, however improbable, must be the truth?"
>*— Sherlock Holmes*
I can't say whether this is the truth, but the last trick I had left up my sleeve at this moment and in this situation was to dynamically keep the two user databases in sync using [MongoDB's change streams](https://www.mongodb.com/docs/manual/changeStreams/).
Finally, we crafted an integration guide for our client's technical team, to remove as much friction as possible from the process and ensure we'd be on time.
We hadn't prepared for the lack of responsiveness of the client's technical team, who took ages to share their SSO details with us.
Nonetheless, after a couple of calls in the right direction we finalized the integration 48h before the deadline.
Two years later, those two hours of planning have paid off.
We successfully onboarded that client, and many more with strict SSO-only policies since.
We had zero technical issues with our authentication flow.
We successfully passed two pentests.
Occasional challenges like that have a way of revealing simple solutions.
That was fun.
I love bricolage.