Configuring Matrix Synapse to use Shibboleth SSO via PySAML2

Matrix, Synapse, Element

Matrix is a decentralised communications protocol that is essentially the modern equivalent of technologies like IRC and XMPP. You might consider using it as an open-source Slack/Microsoft Teams alternative which supports

The protocol itself has a concept of a “homeserver”, which houses users and chat rooms. Homeservers then form a federated network to provide for decentralised communications. No homeserver “owns” any given chat room, and users from one homeserver are able to connect to rooms on other homeservers (subject to IRC-style access control by the room owners). For instance, there is a homeserver running on matrix.org, and anyone may make an account there, but the protocol is really designed for each person or group to run its own server. User identifiers look like @nickhu:matrix.org with the part after the : signifying the homeserver to which the user belongs, and room identifiers look like #myroom:matrix.org. Room identifiers are not unique, in that there may be multiple identifiers spanning even distinct homeservers which refer to the same room (you get the view provided by the homeserver to which you are connected, and over time federation tries to ensure consistency). The homeserver software that is used in almost all cases is called synapse, or sometimes matrix-synapse.

With the current health pandemic, it's become evident that we will be more reliant on digital communications for the foreseeable future, so I thought it was the perfect time to introduce a synapse instance for our CS department. Installing the server itself is fairly straightforward, and the documentation is fairly sufficient. The only point to really pay attention to is choosing a server name, and setting up delegation because it's likely that you will want synapse to run on a different machine than the one pointed to by the root domain3. Federation makes this hard to change later on.

Getting people to use the service is the next barrier, and one thing that puts people off is having to make yet another messaging account. One way to sidestep this is to utilise the single sign-on (SSO) services implemented in many universities already. Documentation on this front is a lot more lacklustre however.

This article is my attempt to document this process, to help other departments and universities follow suit.


  1. at least in Element.io↩︎

  2. It feels like universities should have some sort of obligation to at least try to do this↩︎

  3. In my case, I wanted people to have usernames like @nick.hu:cs.ox.ac.uk rather than @nick.hu:matrix.cs.ox.ac.uk.↩︎

SAML and Shibboleth SSO

Many universities provide a single sign-on service, typically Shibboleth or Athens. Our university uses Shibboleth, which is effectively an implementation of a protocol called SAML. If SSO is the idea that you should have one identity and password, then SAML is the technical description of how that would work. Matrix has support for SAML-backed logins via pysaml2.

As of current, this is documented (sparsely) here, and also in comments in the saml2_config section of the homeserver.yaml configuration. My target machine was the server running synapse, on Ubuntu focal 20.04. To get this working with Shibboleth, here was my final configuration:

saml2_config:
  sp_config:
    metadata:
      remote:
        - url: http://mdq.ukfederation.org.uk/entities/https:%2F%2Fregistry.shibboleth.ox.ac.uk%2Fidp

    description: ["Matrix Synapse Server", "en"]
    name: ["Department of Computer Science Matrix Server", "en"]
    key_file: /etc/shibboleth/sp-key.pem
    cert_file: /etc/shibboleth/sp-cert.pem
    encryption_keypairs:
      - key_file: /etc/shibboleth/sp-key.pem
        cert_file: /etc/shibboleth/sp-cert.pem
    attribute_map_dir: /etc/matrix-synapse/saml2-attribute-maps

  user_mapping_provider:
    module: matrix_saml_strip_hostname.mapping_providers.StripHostnameSamlMappingProvider
    config:
      mxid_source_attribute: email

  #attribute_requirements: # could be used to restrict access to e.g. users from a specific department
  #  - attribute: userGroup
  #    value: "staff"
  #  - attribute: department
  #    value: "sales"

The sp_config field is basically configuration for pysaml2 in YAML form.

The first component of this is the metadata. SAML services have two main interacting components: an identity provider (IdP) and a service provider (SP). The IdP is the server run by the university that handles authentication. The SP in this instance is the synapse server that we are trying to set up. The metadata.remote YAML attribute describes to synapse the information on how the university's IdP can be accessed (in XML format). In addition to this, synapse will generate its own metadata XML which needs to be registered with the IdP. By default, this is exposed at https://matrix.example.com/_matrix/saml2/metadata.xml. In my case, this was handled by a member of IT services, and each university will have its own procedure for SP registration. Universities in the UK participate in a SAML federation, the UK Access Management Federation --- this is only important for the person who does the SP registration, but that is why the metadata remote URL of the IdP is hosted at mdq.ukfederation.org.uk.

The name and description fields are the strings presented to users when they try to login, and ultimately it's up to the IdP to present this (it doesn't appear to be used by my university's IdP, although it did show during the login flow when I was using the samltest.id IdP).

The certificate-key pair from /etc/shibboleth/sp-{cert,key}.pem was generated by sudo shib-keygen -h matrix.example.com (which is effectively an openssl wrapper included with shibboleth-sp-utils package). It's used to both sign and encrypt requests and responses; the fields key_file and cert_file at the top level handle the signing, and everything under encryption_keypairs handles encryption (without specifying encryption_keypairs requests and responses will be unencrypted).

What I have described so far is sufficient to provide a SSO login flow, and when a user logs in, synapse will receive a dictionary of attributes. For my university, the attributes are described at here, and I think most of them should be fairly uniform across other universities in the UK Federation. The problem is that synapse expects attributes uid, email, and displayName to exist, and these might be named differently in the response from the IdP. Plumbing from attributes returned by the IdP to what the SP (synapse) expects is done by an attribute map.

attribute_map_dir specifies a directory with a single file map.py with contents:

# These attributes come from https://help.it.ox.ac.uk/iam/federation/attributes
# e.g. eduPersonPrincipalName, which is identified by 'urn:oid:1.3.6.1.4.1.5923.1.1.1.6', maps onto uid

# NB: Only the SAML2 attributes work

# Here are the released attributes from Shibboleth:
#   - eduPersonPrincipalName
#   - mail
#   - sn
#   - eduPersonOrgUnitDN
#   - eduPersonScopedAffiliation
#   - givenName
#   - displayName
#   - eduPersonTargetedID
# 
# The attributes synapse expects to see are defined here:
#     https://github.com/matrix-org/synapse/blob/develop/synapse/handlers/saml_handler.py
# Currently (11/09/2020), it expects the following attributes:
#   - uid
#   - email
#   - displayName

MAP = {
    "identifier": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
    "fro": {
        'urn:oid:1.3.6.1.4.1.5923.1.1.1.6': 'uid',
        'urn:oid:0.9.2342.19200300.100.1.3': 'email',
        'urn:oid:2.16.840.1.113730.3.1.241': 'displayName',
    },
    "to": {
        'uid': 'urn:oid:1.3.6.1.4.1.5923.1.1.1.6',
        'email': 'urn:oid:0.9.2342.19200300.100.1.3',
        'displayName': 'urn:oid:2.16.840.1.113730.3.1.241',
    },
}

The response from the IdP will specify what kind name-format to use. to and fro are python dicts that map attributes returned from the IdP to uid, email, and displayName (note that they are inverses of each other --- the pysaml2 documentation says that only one needs to be provided, but synapse refused to start for me unless I specified both). uid should be a unique identifier, which corresponds to eduPersonPrincipalName (in my university, this is of the form blah1234@ox.ac.uk and uniquely identifies an individual).

The user_mapping_provider field specifies how these attributes are mapped onto matrix IDs (mxid, e.g. @user:matrix.example.com). By default, it takes as input the uid attribute, but this can be changed by setting user_mapping_provider.config.mxid_source_attribute. This is passed to the module in user_mapping_provider.module; the default one doesn't do much, but since I used email as the mxid_source_attribute, I used the module matrix-saml-strip-hostname so that the mxid is only derived from what precedes the @ in a user's email address. I installed synapse via the Ubuntu package repositories, and in order for this module to be picked up by the same python environment as synapse, the command to install is sudo /opt/venvs/matrix-synapse/bin/pip install matrix-saml-strip-hostname (you need the matrix-synapse-py3 package installed).

In the case where two people have the same string before the @ their email addresses, but perhaps they belong to different departments so refer to different people, e.g. john.smith@cs.example.com and john.smith@maths.example.com, the first one to register will get the handle @john.smith:example.com, and the second one will get @john.smith1:example.com (as described by failures in the SSO mapping provider documentation). This is disambiguated by the fact that they will have different uids. As of current, there is no way to delete matrix users or migrate them to different identifiers.

If restrictions on who can sign in (e.g. specific department only) are required, this can be specified in the attribute_requirements field.

To finish up the homeserver configuration, one might also wish to set

enable_registration: false

password_config:
  enabled: false

In particular, if passwords aren’t disabled some of the UI elements of Element are kind of buggy (e.g. setting up key backup prompts for a password, which never gets set and can't be changed).

Local instance of a client

It can be convenient to install a local instance of the Element client, so that users can simply be given a URL like https://chat.example.com and the right homeserver information is pre-filled out. This part is very simple, and can be done on another server. Take the latest release and untar it into the webroot (e.g. /var/www/html). Copy config.sample.json to config.json and make changes to set the homeserver URL to the homeserver that was set up previously.

It's also convenient to set

sso:
    client_whitelist:
      - https://app.element.io/
      - https://chat.example.com/

to prevent an extra page asking for confirmation after SSO login is completed.

I hope that this information is useful for someone, and if anyone needs help with any of the above then I can be contacted via matrix.

Back to archive