Turning Complex Problems Into Confident Technology

Three identity providers, one flow, and the branch that was not allowed to exist

Trust and identity · Production for two providers, implemented for a third · Inside the Workshop

The problem

Adding a second sign-in provider is where verification systems usually go wrong. The first provider gets built, it works, and its assumptions quietly become the shape of the data. The second one is then recorded as if it were the first, and a moderator sees "unverified" next to someone who verified perfectly well.

Why it mattered

The failure mode is silent and it is the worst kind. Nothing throws. No log line says a provider was misrecorded. A real person completes a real sign-in, the system accepts it, and the badge does not appear. The referee has no way to know, and the only signal is a moderator noticing that someone who said they verified appears not to have.

What I looked at

Where the provider name actually appeared. Not just in the obvious places, but in column names, in conditionals, in the moderation projection, and in the assumption that one confirmation per testimonial was all that needed representing.

The revealing question was not "how do I add a provider" but "what in this code would be wrong if a provider I have not heard of were added by someone else". Everywhere the answer was "this branch", the branch was the bug.

The options

  • Add the second provider carefully. Follow the first one's pattern, write the extra branches, be thorough. Cheapest now. Rejected because it makes the third provider cost the same as the second, and the fourth the same again, while the chance of missing a branch rises each time.
  • Abstract the provider, keep the storage. A shared interface over provider-specific columns. Tempting, because the interface is the visible part. Rejected because the storage is where the misrecording actually happens: a tidy interface writing into a column named after one provider is still wrong.
  • Change both: an interface and storage that can represent any provider. More work, and it needs a migration before the code that reads it can ship.

What I chose, and why

Both. A provider is metadata plus a code exchange, resolved through a registry, and the shared flow routes on the provider name with no provider-specific branching anywhere in it. Adding a provider later is a registration and a class, not a copy of the flow. Storage records one row per testimonial and provider, so two confirmations by different providers are two facts rather than one overwriting the other.

The part I would defend hardest is the startup guard. If more than one provider is configured and the environment has not declared that its verification storage is ready, the host refuses to start. That is deliberate and it is not defensive coding: the previous failure mode was silent and wrong, and loud and stopped is strictly better. An environment that cannot represent two providers should not be allowed to accept two.

An unconfigured provider returns a 404 and its routes report that the option is not enabled, so a half-configured environment fails visibly rather than half-working.

Where it got to

Two providers configured and running in production, a third implemented and deliberately left unconfigured until it is wanted. The badge, duplicate detection and the moderation view all read from the same per-provider records, so there is no second source of truth to drift.

The verification design this sits inside is written up as proving a testimonial without asking anyone to trust me.

What I would do differently

I would have treated "one provider" as a temporary state from the first commit. The cost of the registry at the beginning would have been an hour; the cost of retrofitting it was a migration, a guard, a dual-write period and a set of checks to prove nothing had been silently misrecorded in the meantime.

I would also not repeat the assumption that provider subjects behave alike. They do not. Some are unique per application, so the same person yields different identifiers in different environments; at least one is a global account identifier that is identical everywhere regardless of how many separate applications you register. Treating a subject as environment-scoped without checking which provider it belongs to is a trap I walked into, and the only reason it did not bite was that each environment has its own database.

Related

If this is your problem too

"It worked until we added the second one" is one of the most common shapes of integration problem there is. An integration review is the fixed-scope version of this investigation, and maintainability covers the underlying question of what makes a change safe.