Migration from LDAP to OIDC: Matrix

This article is part of a series:
  1. Forgejo
  2. Nextcloud
  3. Matrix

In my hackerspace we operate various services for our members. Up until this month, most of these services used to do user authentication against a LDAP server. For a multitude of reasons, we replaced the LDAP server with an OpenID Connect SSO using a Keycloak server as the OIDC Identity Provider.

In this series of articles I summarize the efforts required to migrate each of the services from LDAP to OIDC authentication. This article covers the setup and migration in the Matrix homeserver Synapse.

OIDC Setup in Synapse

Adding an OIDC provider in Synapse is accomplished by adding its definition to the homeserver.yaml configuration file:

  - idp_id: "oidc"
    idp_name: "Human Readable Name"
    issuer: "https://sso.example.org/realms/example/"
    client_id: "oidc-client-id-matrix"
    client_secret: "oidc-client-secret-matrix"
    scopes: ["openid", "profile"]
    allow_existing_users: true
        localpart_template: "{{ user.preferred_username }}"
        confirm_localpart: true

The crucial setting here is allow_existing_users: true; this is what enables migration from LDAP with OIDC. Synapse stores all users in the same database, and providers such as LDAP or OIDC are primarily used for authentication and initial account creation. This makes the migration in Synapse pretty straight-forward: as long as the LDAP provider and OIDC provider maps the user to the same Matrix ID localpart (the s3lph in @s3lph:kabelsalat.ch), they map to the same user and can be used interchangeably.

How the localpart is derived from the OIDC id_token is configured in the localpart_template setting. This setting takes a Jinja2 template where the token claims are available in the user object. So the example above uses the preferred_username token claim.

Even though I have not tested this, I'm fairly confident that a migration should work even when the localpart_template does not match the existing users' localparts. However, then you'd have to fill the user_external_ids database table with the mappings between OIDC subject identifiers (the sub token claim) and the full Matrix ID (@localpart:example.org). This table is already populated each time a user signs in via OIDC:

synapse=# select * from user_external_ids;
 auth_provider |             external_id              |       user_id
 oidc          | ee5ff004-07c7-4c8f-b609-298aa1b5cd88 | @s3lph:kabelsalat.ch

You can also allow new users on your homeserver to choose a different username when they sign in via OIDC for the first time. This is controlled by the confirm_localpart setting in the example above. This too is controlled by entries in the user_external_ids table.

This architecture of only using LDAP and OIDC as an authentication providers and decoupling them from the actual users in this way, alongside with the allow_existing_users OIDC provider setting made Synapse the easiest service to migrate. If you can even call it "migration", that is - you simply add the OIDC provider and later remove the LDAP provider and you're done.


Unfortunately SSO handling in Matrix heavily depends on the specific clients your users are using, with greatly varying behaviors:

  • Some clients don't support SSO at all. Check the client feature matrix to see which clients support SSO login.
  • Some clients only support SSO if the default password authentication flow, m.login.password, is disabled. However, if you need password login for some local users, disabling it may not be an option for you.
  • However, most clients I tested properly implement the m.login.sso authentication flow and give the user the option to sign in via SSO.

You can disable the m.login.password flow if you don't need it by adding the following to your homeserver.yaml:

  enabled: false