Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Nuss and IdP at Famedly

Our software has users, and we have to authenticate and authorize them before they can do anything. The standard solution to this problem is identity providers (hereafter referred to as IdP), software that has data about users, allows them to authenticate themselves with a consistent interface, and authorize software to perform actions on their behalf. Contemporary IdPs usually follow the OIDC and SCIM standards, although the latter less consistently and with some caveats.

We need our software to interoperate with such IdPs and provide some Famedly-specific functions for them. Currently Nuss is the piece of software that implements the most custom features related to IdPs, so this document is focused on it.

The goal of the initial version of this document was to reach a consistent approach to dealing with authentication and authorization within Famedly, but that turned out to not be worthwhile, so now it only pertains to features related to Arminius.

Decision

We will develop Nuss as an identity broker geared specifically towards working with Arminius. For now, Arminius will keep synchronizing with it using custom SCIM, however they might become more integrated in the future. Zitadel remains our main IdP solution at Famedly, with no further plans to homogenize our approach.

See the Solution section for more technical details.

Context

Original Problem

When developing the new homeserver the Arminius team stumbled into the problem of login session management. Matrix has long-lived sessions, created for every logged in device, which have to be managed, including the possibility of revocation without access to a device. Our current IdP solution, Zitadel, in no way supported that, so Nuss was created as an identity broker that adds this functionality. At this point Nuss also has other functionalities, but only session handling is absolutely necessary.

Current Design

We use Zitadel as the IdP for all our services. It provides user management functionalities, supports authentication through OIDC and user info fetching through SCIM. It doesn’t quite have all the features needed, so in many places we need various glue services, although only one is relevant to this document.

Nuss

Nuss is an identity broker with additional features that Arminius uses to solve the user management problem in a different way than Synapse – by assuming that it always has an accompanying IdP which keeps all the necessary user data and handles authentication and authorization. Nuss is necessary, because IdPs in general don’t support managing persistent sessions, which is a strict requirement for Matrix. Some technical specification of the interface Arminius expects, and Nuss provides, is present in the SCIM client component documentation.

Nuss interacts with Zitadel by fetching all the user data from it using SCIM and by being an OIDC proxy, capturing authentication requests to remember them as persistent sessions. It should also (although it is not currently properly implemented) handle authorization, both in the sense of ensuring that a given entity is allowed to have certain permissions, as well as requesting user consent to grant these permissions in specific cases.

Constraints

  1. We need persistent session data and Matrix-specific permissions in our IdP.
  2. Users might be moved between IdPs, so we have to support manually mapping Matrix accounts to users.
  3. We have to be able to associate users from the same upstream IdP to different homeservers, both for limiting logins to Matrix accounts, as well as administration rights.
  4. We have to support the MLS flow, i.e. users without an associated Matrix account and dynamically created pseudonymous users authenticated through client secrets associated with Matrix accounts.
  5. We have a planned release of Arminius in October, so it and all necessary side services have to be ready for then at least as a MVP.

Desiderata

  1. We would prefer to keep OIDC login session information for Arminius in a separate service than Arminius.
  2. The user should be able to grant different permissions to different Matrix devices, with explicit consent for them.
  3. Nuss should be able to run multi-tenant.
  4. Arminius should be able to long poll Nuss for user information, to make synchronization simpler and snappier.
  5. It should be possible to automatically map information acquired through SCIM to Matrix accounts, in particular (although not limited to):
    1. assign a user ID localpart based on upstream metadata
    2. Create a display name based on potentially several upstream profile fields.
  6. Arminius could be a general use Matrix server for self hosting.
  7. We would like to support migrating from Synapse to Arminius.

Solution

Proposed Design

Nuss becomes an identity broker geared specifically for Arminius, and always running next to every Arminius instance. It will:

  1. synchronize user data from upstream IdPs using SCIM, and proxy OIDC requests to them, injecting persistent session handling and Matrix-specific permission controls in the process
  2. map groups from upstream IdPs to available Matrix and Nuss administration permissions in a configurable manner, details in a follow-up ADR
  3. have its own frontend for letting the user consent to permissions and some administration tasks as needed
  4. support mapping users to Matrix accounts, with the details decided in a separate ADR, but supporting at least
    1. allowing users without associated Matrix accounts, for administration purposes
    2. multiple upstream IdPs
    3. automatically mapping fields from the upstream IdP to a displayname
  5. support creating local-only pseudonymous users through client registration, as required by the MLS flow

We are assuming the upstream IdP can use the client ID Nuss provides to limit which users will be able to login on a specific server, so we do not expect to have to implement that functionality on the Nuss side.

Arminius will fetch data from Nuss using SCIM, but there might be tighter integration of the two components in the future, see the Alternatives section for details.

Interaction Diagram

stateDiagram-v2
    nuss: Nuss
    upstreamIdP1: Upstream IdP
    upstreamIdP2: Upstream IdP
    mls: MLS
    arminius: Arminius

    nuss --> upstreamIdP1: SCIM fetch
    nuss --> upstreamIdP2: OIDC redirect

    mls --> nuss: creating pseudonymous users through SCIM
    arminius --> nuss: SCIM fetch, with sessions
    arminius --> nuss: OIDC authentication

The OIDC flow shown in the diagram is standard identity brokerage, only Nuss keeps the data about existing login sessions to facilitate session and device management, and injects consent requests for Matrix-specific permissions.

The two upstream IdPs in the diagram are only for illustration purposes, there can be arbitrarily many and all actions supported by them can be performed on all of them. There can, however, be only one Arminius instance.

Rationale

This approach satisfies all the hard requirements and has most of the desired qualities, while requiring much less development work than alternatives.

We end up with multiple copies of user data in our system, but most ways of dealing with this would require significant architectural changes which we don’t have the resources for currently.

Arminius should be usable as a self-hostable server, although with a relatively complex setup for such a service, requiring also managing Nuss and an upstream IdP.

We are dropping the idea to support multi-tenancy, because it makes little sense for a service that is so tightly connected to Arminius.

Consequences

Roadmap

We have the delivery deadline for Arminius in October, so we need a clear roadmap of what we want to do until then, and what can wait. We expect to be able to implement most of the features described in this document by then in a basic version, but full implementations will follow later.

October Deadline

By October Nuss should support:

  1. OIDC:
    1. At least the authorization code flow. [already done]
    2. Proxying the requests to upstream IdPs. [already done]
    3. Saving OIDC session information to allow session management. [already done]
    4. Granting the user only scopes they are authorized to.
    5. Requesting user consent for scopes. [mostly done, but requires human readable descriptions of scopes]
  2. Upstream SCIM:
    1. Synchronizing data from a single upstream. [done, although needs a rework to allow multiple replicas]
    2. Mapping some upstream information into standardized, Matrix specific information (user ID localpart, display name), details to be decided in a separate ADR.
  3. Downstream SCIM:
    1. Providing user data. [mostly done]
    2. Providing session data. [mostly done]
    3. Long polling of data changes. [already done]
    4. Session management. [already done, deletion only, but we don’t need anything more]
  4. Permission control management – we need to define groups in the upstream IdPs and their mapping to Matrix and Nuss permissions, details to be decided in a separate ADR.
  5. Remove the dev mode in Nuss and create a test setup with a mock upstream IdP.

In particular we still need two ADRs:

  1. Permissions in Arminius and Nuss.
  2. User mapping from the upstream IdP to Matrix accounts, with a particular focus on mapping of data.

Longer Term

Longer term we would like to implement the following:

  1. MLS:
    1. Users with no associated Matrix account. (with 2.2 likely being a prerequisite)
    2. Registering a client with an associated user authenticated through the client’s secret.
  2. User management:
    1. Allow multiple upstream IdPs.
    2. Manage users’ association with Matrix accounts.
  3. SCIM, either (the decision will require a separate ADR):
    1. implement filtering of requested resources, metadata endpoints, and all the other minor parts of the specification, documenting which optional parts we are not implementing and why, or
    2. integrate Nuss more tightly with Arminius, removing downstream SCIM altogether, as described in the Alternatives section.

Problems with This Approach

While Zitadel or other upstream IdPs remain the sources of truth for user data, we have multiple copies of this data in many different services, which will become very confusing on any desynchronization.

Arminius will only work if deployed together with Nuss, while Nuss pretends to be a separate component. The communication between these components uses a protocol based on SCIM, but with enough differences that the modularity is only a mirage.

We only support upstream IdPs that use OIDC and SCIM.

Alternatives

This section details two wholly different approaches to the overall design, the first of which we might want to adopt in the future. This is followed by two minor parts of this approach that were discarded as impractical.

Session Management in Arminius

We could implement session management in Arminius. Then Nuss would not be necessary and Arminius would connect to Zitadel, or another IdP supporting standard SCIM and OIDC directly.

Why Not

More unnecessary complexity in Arminius, although we could deploy it with any OIDC/SCIM upstream!

This would essentially mean merging current Nuss functionalities into Arminius, only removing the downstream SCIM protocol.

The Nuss-like component would have to be accessible on a different domain than the rest of Arminius for security reasons. This makes the setup more complex.

To support MLS Arminius would also need to support local, dynamically created users. This is beyond what one would expect from a Matrix server for general use.

There are also issues with populating users before they login. We would have to either mandate SCIM upstreams and proactively update our user list quite often (like Nuss does currently), or ensure users are pushed into Arminius (like with Synapse currently).

Why Maybe

In the future we might want to integrate Nuss and Arminius much more tightly, to remove at least one copy of user data from our system. This should only be done if it’s possible to keep all the user and session data in one copy within Arminius. In the current setup this would mean operating on the single Arminius DB directly, but we might want to rethink how that data is kept anyway.

If Nuss were to be integrated that way, this would also remove the not-quite-standard SCIM interface between Nuss and Arminius, replacing it with data flows through NATS, like with all other Arminius components.

When contemplating such a decision we should try to avoid a situation like with the Matrix Authentication Service, where it’s only partially integrated with Synapse, thus just adding to the confusion rather than clearing stuff up. This means such a change would require very careful consideration of the necessary architectural changes in Arminius.

The details of such a proposal are far beyond the scope of this ADR, and would require a separate one.

Nuss as a Full-Fledged IdP

Make Nuss into a full-fledged IdP, thus eliminating the need for external IdPs, completely centralizing where we keep user data, and perhaps even extending SCIM standards to include our custom modifications.

We could also centralize all frontend in Famedly Control, providing a unified user experience. This standardization would benefit all of Famedly.

Why Not

Supporting a full-fledged IdP is a lot of dev work, and very security sensitive work at that. If we were to do that, it would probably be better to fork an already established IdP, but maintaining a fork is its own can of worms.

At least until the standardization happened, Nuss would still be the only IdP Arminius would work with, making the modularity illusory.

We would still have to support external IdPs, since some customers would use them, so we would have to support identity brokerage anyway.

Also Include Synapse Compatibility

Synapse requires creating accounts on the server itself, in addition to having them in an IdP. Currently Simplified User Management Service plugs into Zitadel to do that, and we could replicate this functionality in Nuss to replace it.

Why Not

This kind of plugin is quite some effort to implement, and Synapse already works with our glue software fine.

This ADR does not mandate such compatibility, but if it turns out it’s for some reason needed and not that much effort no one will be stopped form implementing it somehow. However, it’s more likely that the alternative approach on this branch would be adopted in such a case.

Nuss Pushing Users to Arminius

Arminius is currently pulling users from Nuss to stay synchronized, and it uses the nonstandard long polling to make that reasonable. A natural alternative would be to have Nuss push any user updates to Arminius instead, preferably also through SCIM.

Why Not

Arminius would have to expose an interface for that, and securing that interface would be a nightmare, since Arminius has to be externally accessible at least for clients and often also for federation.