← Writing

One person, several accounts

Adding single sign-on looks like a contained piece of work. Wire up a provider, exchange a token, find the user, issue a session. Two providers and you are done.

The assumption hiding in that sentence is find the user — singular. It is the assumption that breaks, and everything interesting follows from it breaking.

The identity is not the account

On a white-label platform, one person can legitimately exist several times. They work for a business that is a customer of a reseller. They also consult for another business on the same platform under a different brand. Somebody set them up twice years ago with the same work email. They are a partner administrator as well as a user of their own system.

None of these are data quality problems to be cleaned up. They are the product working correctly. So "the user with this email address" is not a lookup that returns one row, and a login flow built on the assumption that it does will either pick one arbitrarily or fail.

Both failure modes are bad in a specific way: the user has already authenticated successfully with their identity provider. From their point of view they logged in and your product broke.

The awkward middle state

Handling this means the login response cannot always be a session. Sometimes it has to be "you are authenticated, and there are four of you — which one?".

That is a state most auth flows do not have a shape for. The user has proved who they are but has no session yet, because a session belongs to an account and the account is not yet chosen. You need something scoped enough to let them choose, and nothing more — it must not be usable as a session, and it must expire quickly.

Getting there meant a new version of the login command specifically to carry a multi-account response, rather than bending the existing single-account one. That was the right call. The old shape said "here is your session"; the new shape says "here is what happened, which might be a session and might be a choice". Those are different contracts, and versioning was cheaper than making one response mean two things.

It also has to work on clients you do not control the release cycle of, which is why the request carries a version — the server needs to know whether the caller can cope with being handed a choice, and fall back to the old behaviour when it cannot.

Every provider is different in a small way

We support more than one identity provider, and the sense in which they are "the same protocol" is thinner than the specifications suggest.

They differ in what they will tell you about a person and when. One gives you an email on every sign-in; another may not, depending on choices the user made the first time they ever used it, possibly years ago on a different device. They differ in how they represent a stable identifier, which matters enormously because that identifier is what you key on — and keying on email is the mistake that seems fine until somebody changes their name.

They differ most in how they fail. One returns a structured error; another returns something generic that could mean several things. Error handling written carefully for one provider does not transfer, and the second integration is not half the work of the first — it is most of the work again, in a different shape.

They also come with platform obligations. Adding one meant declaring keychain entitlements; another required a privacy manifest under new store rules. Those are not auth problems, but they are in the critical path of shipping auth.

Second factors belong to the organisation

Two-factor authentication starts as a user preference and does not stay one. Businesses want it mandatory, which means it is an account-level policy that overrides individual choice, administered by someone who is not the user.

That inverts the model. It is no longer "does this user have 2FA enabled" but "does this user have it, or is it required of them" — and the enforcement has to apply to people who already have sessions issued under the old policy, which is a migration rather than a flag.

Then there is the part nobody puts in a design document: verifying a code takes a second or two, during which the interface must show something, and switching between code types has to not look broken. A meaningful share of the work on 2FA was loading states and transitions, because a login screen that appears frozen is a login screen people abandon.

And "remember me" is a decision about how long you trade convenience against risk. We landed on short-lived sessions at registration and a fortnight for established ones — numbers that are defensible rather than derived, which is true of most such numbers.

What I took from it

Authentication and account selection are different steps. Most systems can conflate them because the mapping is one-to-one. As soon as it is not, you need an intermediate state — authenticated, unresolved — and retrofitting that is harder than designing it in.

Version the response, not just the endpoint. When a response can now mean something structurally new, adding a field to the old shape asks every existing client to understand something it was not written for. A new command with an explicit version is more work up front and far less coordination later.

Never key identity on email. It is the obvious join column and it is mutable, reusable, and sometimes absent. Every provider offers a stable identifier precisely because email is not one.

Security controls become organisational policy. Anything you build as a user preference in an enterprise product will eventually need to be enforceable by an administrator over that user's objection. Designing it as a preference with an override bolted on later is measurably worse than assuming policy from the start.

Also in this series