User Info Mapping in Nuss
Author[s]: timorl
Nuss is the identity broker for Arminius adding functionality on top of what upstream identity providers (IdPs) deliver. It’s supposed to map upstream IdP users into Matrix accounts. This mapping, however, is not trivial, as users might be allowed to only access specific Matrix accounts and user information might map nontrivially to Matrix account properties.
This ADR describes these problems in some details and defines a solution.
Decision
We will assume that several pieces of metadata in the upstream IdP define the necessary information that has to be mapped into Matrix account data. This mapping will happen in Nuss, and the exact way it happens will be configurable.
Whether a user can authenticate to a specific Matrix server will be configured completely on the IdP side, using the fact that a specific Nuss instance acts as a client for the IdP, thus login attempts from a specific server can be identified and rejected or accepted.
Details in the Solution section.
Context
Matrix accounts contain information about the user that is usually also present in identity providers. This information, however, is not structured in the same way in all cases, so we need to map it from the upstream IdP to Arminius. Of particular interest are the Matrix ID localpart, which is necessary for the functioning of the protocol, and display names which might need to be generated from several pieces of information that are separate in the upstream.
Synapse solved this problem with SSO mapping providers, which are Python classes you have to load for a specific deployment.
For localparts specifically, there are several MSCs (e.g. 1, 2), that propose redefining what the localparts represent. For now we assume the localparts will remain arbitrary, with the only restrictions defined in the appropriate part of the Matrix spec.
Other than that, Matrix profiles normally contain 3 fields – the display name, the avatar URL, and the time zone. These can usually be changed through the server API, but this can depend on the server. The avatar URL has to be a Matrix Content URI, so the avatar image itself has to be uploaded to the server somehow.
We are currently populating users in Arminius through a SCIM sync. We are doing this to be able to create and manipulate all user accounts before their first login (as would be the case if we relied on OIDC for this information). We are using SCIM in two places, once when Nuss gets data from the upstream IdP, and once when Arminius gets data from Nuss. The latter use might be replaced in the future, but the former should remain indefinitely.
Constraints
- We need to work with Zitadel as an upstream IdP.
- Arminius needs to populate enough Matrix account information to display users before they login for the first time.
- Nuss has to provide valid localparts to Arminius for users that are supposed to have a Matrix account there.
- Upstream users might have several associated Matrix accounts on different servers, Nuss has to provide the correct localpart to its associated server.
Desiderata
- We would like to use SCIM for fetching information from upstream IdPs.
- Nuss should be able to map arbitrary information from the upstream to a Matrix display name.
- Nuss should be able to forward the user’s timezone from the upstream IdP to Arminius.
- Nuss should be able to fetch and upload an avatar from the upstream IdP to Arminius, also setting the correct avatar URL for the downstream user.
- We would prefer that the upstream IdP were the sole authoritative source of user information.
- The localparts themselves should be pseudonymous, but easy to associate with upstream users with access to the upstream IdP.
- We might return login errors on upstream misconfiguration – e.g. two users with identical localparts.
- An upstream user might be allowed to login to the homeserver without a Matrix profile, e.g. for administration purposes.
Solution
Nuss will continue fetching user data from the upstream IdP through SCIM and provide it to the homeserver both through SCIM, as well as in the OIDC login token where applicable.
Upstream SCIM Data
The upstream has to provide several pieces of data to Nuss through SCIM:
- the localpart for the specific server
- the data required to construct a display name
- the user’s timezone
- the user’s avatar
We have to treat each of these slightly differently.
Localpart
The localparts of a specific upstream user will be returned as JSON objects in the entitlements field of the SCIM user struct, following the schema:
{
"type": "localpart-$DOMAIN",
"value": "$LOCALPART"
}
where $LOCALPART obviously refers to the literal localpart, and $DOMAIN is the domain of the homeserver this localpart is associated with. The upstream might return multiple such objects, and it’s the responsibility of Nuss to find and extract the appropriate one.
Repeated or invalid localparts should result in warnings in logs and the Nuss admin UI, and there should be an interface to view a list of them.
The responsibility of keeping the localparts pseudonymous rests on the upstream IdP data administrator. We might provide some automated tools facilitating this, but such software is out of scope for this document.
Display Name
The display name might need to be constructed out of several pieces of information from the upstream. The particularly relevant ones are:
displayName– this should be the default, and in initial implementations might be the only optionnickNametitle- all the subfields of
name
We might also want to support custom fields put in entitlements, although that would complicate the formatting logic somewhat.
The formatting logic will probably have to be implemented manually, because existing solutions are either too weak to support arbitrary subfields (e.g. new_string_template or are full, feature-rich template engines (e.g. Sailfish, or even minijinja).
The formatting will be configurable through a standard REST API, and might be different for different upstream IdPs. It will be represented by a JSON with two fields, idpId with the ID of the upstream IdP the formatter pertains to and “displayName” containing the formatting string itself. The detailed definition of the endpoint will be created in the course of implementation, as there is little that isn’t obvious.
Timezone
The timezone will be read from the timezone field.
Avatar
The avatar will be fetched from the element in photos that is of type "photo" and is primary. If there is no primary, then the first "photo" should be used. When there is no "photo" then the user will not have an avatar set.
Avatars will be fetched as a separate process from the main user info synchronization process. For what happens next see the appropriate subsection in the following section.
Forwarding the Data
Once the data is available, it has to be forwarded to the homeserver, which again might require special treatment.
Localpart
If the localpart exists, is valid and unique, then it gets forwarded as username in the SCIM interface. Note that this is different than the resource ID, which will be treated as the user ID and also returned in the OIDC token sub field.
If there is no localpart, then the user might still exist and log in, but they will not have any profile, thus they cannot be treated as a Matrix user. They might, however, perform administrative or moderation actions, if they have the appropriate permissions.
If the localpart is not a valid Matrix localpart, the user should be shown an error message telling them to contact their user data administrators. These users might still log in for administrative or moderation purposes, like in the previous paragraph.
If there are multiple users with the same localpart for the given server, then the first user that was synchronized from the upstream should be able to login and normally use their Matrix account. Any subsequent users should be treated as if their username was invalid like in the previous paragraph, but with a different error message mentioning the duplication.
Display Name
The display name will be forwarded in the displayName field.
Timezone
The timezone will be forwarded in the timezone field.
Avatar
Once an avatar is fetched, the same long running process will upload it to Arminius. The resulting Matrix Content URI will be saved into the database and this will be forwarded as the only value in the photos field, of type "photo" and set as primary.
This process should be observable through a Nuss API, so that it can be clear when at least the initial import is done.
Initial Implementation Notes
Due to time constraints we might want to limit the implementation effort. With that in mind, we might initially only support a simple forward of displayName, without configurable composition, and no support for avatars.
Rationale
This approach mostly uses widely adopted standards, with the only stretch being using entitlements for localparts.
Localparts are fully the responsibility of the upstream data administrator. This leaves them flexibility if they need them to have a specific shape. Any mistakes should be easily noticeable with the proposed error reporting.
Avatars are handled separately from other user information, as otherwise preloading all users would take too long. It should be better to have all users loaded quickly, with the avatars only appearing slowly, rather than being limited to waiting for the entire import. However, the other behaviour can also be replicated by observing the photo import process and waiting for it to finish before letting anyone login.
The REST API for display name formatting should be consistent with the one proposed for permission handling.
Consequences
Upstream IdP administrators will have to put all the profile information into the IdPs. We might want to implement helper software to make this simpler.
Only upstream IdP administrators will be able to change profile information of users, unless they explicitly grant the users permissions to change the appropriate fields.
Arminius will have to support avatar uploads with Nuss as a source.
Avatar change propagation might be lag, and will always require changing the upstream avatar URI.
Matrix endpoints for manipulating profile information will all be disabled, all operations will have to happen in the upstream.
Alternatives
Upstream Channels Other Than SCIM
Using Zitadel’s other endpoints, we might be able to get profile information of users in a more natural way, and with more flexibility than with SCIM. In particular we wouldn’t have to stretch the meaning of entitlements.
Why Not?
This would limit us to Zitadel as the only upstream, while using SCIM should make supporting other upstreams much simpler.
Forbid Anyone with Overlapping Localparts from Login
When a duplicated localpart is detected for different users we could stop all users with the localpart from logging in. This behaviour would be more consistent and predictable.
Why Not?
Adding a new user might prevent an old user from logging in. This is a much bigger problem than a new user being able to log in – some friction is expected when introducing new users, and fixing it then at least shouldn’t impact any ongoing conversations.
Use Matrix Endpoints for Changing Profile Information
Clients support Matrix endpoints, so changing information through them would be a free feature.
Why Not?
In the context Arminius will be used we don’t want users to be able to change their own profiles anyway.
We could support this as an interface for users with permissions to change their profiles, but this should be rare enough that the engineering effort is not worth it. This is a feature that might one day show up though.
Putting the Domain for the Localpart in the $ref Field
The $ref field usually contains URIs so it would be a relatively natural place to put the domain, rather than in type.
Why Not?
This domain doesn’t really represent a resource, so it doesn’t quite fit. The spec also discourages returning the same (type, value) pairs more than once, which might happen if we have several with the only difference being $ref.