Making Bubbles: Three Stages of Identity
One of the key aspects to the bubbles model for federated identity systems is the fact that within the bubble, the account for each user is fully authoritative for that space. But since bubbles don’t exist in a vacuum, that same person probably has accounts that exist in other bubbles. In fact, the attributes in their account probably came from somewhere else to begin with. And of course, our bubble can in turn act as a source for another system downstream.
With that model in mind, from the perspective of our bubble, we’ve got three distinct identity processing systems that all need to come together to make things work: the local identity management system for our bubble, something to process inbound accounts, and something to package accounts up for outbound transmission to somewhere else.
The Local Bubble
Within the bubble itself, we are using a cohesive IdAM system and are almost certainly using federation technology to connect out to a set of RP’s within the bubble. All of these systems can look towards one authoritative IdP within the bubble for the source of all account information.
Inside the bubble, we have tons of freedom for how we want to connect our users to our systems. While we probably want to use current best-of-class technologies like OpenID Connect and passkeys, we only really need to be compatible internally, using whatever makes the most sense for our environment.
The important thing here is that each user has an account that is accessible within the bubble at all times, and is not dependent on reaching out to anything outside the bubble for local authentication.
Inbound Processing
Most of the users in a bubble probably came from somewhere. If we onboard an account from an external system, it means that we’re creating an account based on a set of attributes from a known source. These attributes can come in with an assertion, credential, certificate, API call, or some other technology. The important thing, for us, is that we can now tie these attributes to a known account, and we can cache the attributes as we received them. A lot of these are going to be immensely useful — we won’t have to have every user type in all their attributes every time they connect into a new bubble.
But it’s not enough that we’re just making a cached copy. In many cases, we’ll want to override or update these attributes locally, but we don’t necessarily want to lose the data from the source when we do that override. After all, we don’t control the data source, and we want to know where all of our information came from.
We can use an overlay style data structure that lets us keep both updated data and the source data at the same time. Let’s say, for instance, that Patty O’Sullivan gets an account onboarded into the system, but it turns out that everyone inside the bubble just calls her Sully. We can create a local value that overrides the official value, but the official value doesn’t go away: it’s still sitting in its own structure. If we don’t have an override, when we look up an attribute we can follow a pointer to an upstream source and get it directly without having to copy it.
The approach also allows us to very efficiently take care of cases where we don’t have a need for referencing an attribute that was handed to us, or that we need to create a brand new attribute that doesn’t exist at the source. And in fact, this pattern can be applied up the chain, since our source might have gotten its information from somewhere else in the first place.
And we can just keep copying this pattern, even pointing at multiple sources at the same time. We can optimize this graph structure for both storage size and lookup efficiency, but more importantly it allows us to keep the data sources separate from each other in a meaningful fashion. We can tell where we’re getting each attribute value from, and we can differentiate between local updates and data copied from elsewhere.
This also means that we can put restrictions on data from different layers. For example, maybe we want a policy that needs an update on a cached value every so often. Or if I’m doing a local override of an important attribute, like one that gets used in security decision making, then I need to check that the override is still valid after a certain timeout. This can avoid a class of configuration errors that we see in the field, where something gets changed in order to solve an immediate problem, but never gets changed back when things de-escalate.
Outbound Packaging
And of course, we also want our bubble to be able to act as the source for some downstream receivers as well. In order to do that, we need to be able to package up our accounts and assert them outbound.
But wait a moment — isn’t that the same exact thing we’re doing inside of the bubble for our apps? Aren’t we already going through a federation process to connect on the inside? Shouldn’t we just use that same IdP again, since it’s already set up and has all the same accounts?
While it would be possible to re-use the same component, it makes more sense to have a dedicated IdP that only speaks to external receivers. This separation allows us to deliberately control which information we share and with whom, and without it being conflated with local policy, changes, overrides, and other concerns. When we’re talking to an external receiver, we likely want to give a very specific view of an account in this context, especially considering that we want to minimize the transmission of sensitive data across boundaries.
Stacking the Three Pieces Together
Each identity system we’ve talked about here has a distinct role to play. In this way, the three parts of a bubble system — inbound, local, and outbound — can work together to create a cohesive path for an account, its attributes, and the person who’s using it.