Matrix server with Nextcloud login

This is a pretty geeky description of my matrix homeserver setup using Matrix-authentication-service and Nextcloud as the authentication source. It might be useful to others or future me. If not interested in technical details, please skip this post. A minimal version highlighting just the necessary configuration can be found in this TL;DR post. This is the slightly extended version explaining my setup.

Setting up matrix with experimental OIDC auth is still pretty … experimental … and seems difficult as there are no easy guides yet. Here, I describe my setup, using Nextcloud as an upstream authentication provider. This way, I can e.g. enforce 2FA authentication.
I don’t claim it is the most elegant of all setups but it seems to work fine for now.

My server is basically a single-user setup and I infrequently change users. Adding another user is as easy as creating a new nextcloud user and putting them into the “matrix” group in my cloud setup. Yay.

I started using MAS (Matrix-authentication-service) using a local password setup and that worked fine, there is no Nextcloud needed. So if you have a small server, consider using just that and save yourself a bit of complexity. I did want to use 2FA and I did want to play with stuff, so that is why I went fuly down the rabbit hole.

I do use containers, but I like to run things on bare metal too when it is easy. So I set up everything on a Debian server. Using docker or somesuch to install the components is certainly possible, but you will need to make the networking work yourself.

If you have feedback or improvement suggestions, let me know by responding via ActivityPub to my toot or ping me via matrix.

Setup

Domains

I deploy on my domain sspaeth.de with user names @user:sspaeth.de. The matrix server runs on the subdomain matrix.sspaeth.de and the MAS server runs on the domain auth.sspaeth.de. My nextcloud runs on cloud.sspaeth.de. Everything is presented to the world through a nginx proxy which also provides the necessary TLS certificates.

Components

Ignoring the sliding sync proxy (which is being integrated into synapse), my setup consists out of four components:

  1. some (static) .well-known files
  2. Synapse homeserver
  3. Matrix authentication service (MAS)
  4. Nextcloud as upstream auth provider
  5. A nginx proxy to really tie the room together

well-known files

To make my server setup discoverable, I use some simple well known files on MAIN_DOMAIN (it is the @user:MAIN_DOMAIN part). These are actually just static files on my cheap shared webhoster.

https://sspaeth.de/.well-known/matrix/server contains:

{ "m.server": "matrix.sspaeth.de:443" }

https://sspaeth.de/.well-known/matrix/client contains:

{
  "m.homeserver": {
    "base_url": "https://matrix.sspaeth.de"
  },
 "org.matrix.msc2965.authentication": {
    "issuer": "https://auth.sspaeth.de/",
    "account": "https://auth.sspaeth.de/account/"
  }
}

(I left out the sliding sync proxy in the example above, to make things simpler to understand).

When I try to login via Element Web, I select ‘sspaeth.de’ as my home server and login looks kind of like this:

Synapse

I use the prebuild binary package provided through the matrix.org repository to install synapse (instructions here). You might use a different method, it should not really matter. I will also not explain here how to configure Synapse in general.

Suffice to say, that my synapse runs locally via http on port 8008, so homeserver.yaml contains this:

  - type: http
    port: 8008
    tls: false
    type: http
    x_forwarded: true
    bind_addresses: ['::1', '127.0.0.1']
    resources:
      - names: [client, federation, metrics]
        compress: false

Also, I set the following to disable plain old synapse-internal passwords (probably not all of them needed, but this is how it evolved over time. Let me know if I can ditch some):

enable_registration: false
password_config:
   enabled: false

Now, the crucial part, and the only real change that I needed to do in Synapse is to set up MAS as the OIDC server. I put the below in a separate OIDC.yaml file in the conf.d subdirectory of my synapse config:

experimental_features:
  msc3861:
    enabled: true
    issuer: https://auth.sspaeth.de/
    # Synapse will call `{issuer}/.well-known/openid-configuration` to get the OIDC configuration

    # Matches the `client_id` in the auth service config
    client_id: 00000000000000000SYNAPSE00
    # Matches the `client_auth_method` in the auth service config
    client_auth_method: client_secret_basic
    # Matches the `client_secret` in the auth service config
    client_secret: 1234CLIENTSECRETHERE56789

    # Matches the `matrix.secret` in the auth service config
    admin_token: 0x97531ADMINTOKENHERE13579

That was really all there is to do in synapse. (I will post my nginx config file later)

OK, so far, so good, now when I try to login to synapse, synapse will call {issuer}/.well-known/openid-configuration to get the OIDC configuration and use the endpoints specified there. (that openid-configuration URI is autogenerated by MAS, by the way)

MAS

I first describe the installation of the Matrix-authentication-service (MAS) (because perhaps not everyone wants to install it as weirdly as I did), followed by my configuration. MAS runs locally on port 8080.

Installation

I downloaded the latest release from https://github.com/matrix-org/matrix-authentication-service (0.10) and uncompressed it. The main mas-cli binary went into /usr/local/bin, the “shared” folder went into /usr/local/share/mas-cli/*, my created config.yaml file went into /etc/mas/ and a crude systemd service file autostarts it. The service file looks like this:

[Unit]
Description=MAS (Matrix OIDC Auth)
After=network.target
After=postgresql.service

[Service]
ExecReload=/bin/kill $MAINPID
# Environment="RUST_LOG=warn" # use one of debug,info,warn,error

User=www-data
Group=nogroup

WorkingDirectory=/etc/mas
ExecStart=/usr/local/bin/mas-cli server
Restart=on-failure

# Optional hardening to improve security
PrivateDevices=yes
PrivateTmp=yes

[Install]
WantedBy=multi-user.target

Yes, the hardening part could be made a lot better, I am sure… but that is for another day.

Now, let us have a look at the MAS configuration. This visualization illustrates how MAS sits between Synapse and the upstream IdP (Nextcloud):

Graphical overview of how Synapse-MAS and Upstream IdPs work together.

Configuration

A few snippets that I either added to or where I modified the defaults in config.yaml. The following sets up the client talking to synapse and MAS in general.

# Config client to talk to synapse
clients:
  - client_id: 00000000000000000SYNAPSE00
    client_auth_method: client_secret_basic
    client_secret: 1234CLIENTSECRETHERE56789
    redirect_uris:
      - https://openidconnect.net/callback

This is the main MAS component listening on port 8080:

http:
  listeners:
  - name: web
    resources:
    - name: discovery
    - name: human
    - name: oauth
    - name: compat
    - name: assets
      # NOTE: This is the custom path in my local installation!!!
      path: /usr/local/share/mas-cli/assets/
    binds:
    - address: '[::1]:8080'
    proxy_protocol: false
  trusted_proxies:
  - 192.128.0.0/16
  - 172.16.0.0/12
  - 10.0.0.0/10
  - 127.0.0.1/8
  - fd00::/8
  - ::1/128
  public_base: https://auth.sspaeth.de/
  issuer: https://auth.sspaeth.de/ 

I’ll skip the database config here, but note, I also changed the template directory to suit my local installation:

templates:
  path: /usr/local/share/mas-cli/templates/
  assets_manifest: /usr/local/share/mas-cli/manifest.json
  translations_path: /usr/local/share/mas-cli/translations/

I also tell my MAS about the matrix server and how to talk locally to it:

matrix:
  homeserver: sspaeth.de
  secret: 0x97531ADMINTOKENHERE13579
  endpoint: http://localhost:8008/

This should be OK for local password auth through MAS, but the next section is what I added to configure Nextcloud as an upstream OIDC provider:

upstream_oauth2:
  providers:
  - id: 01B2BSNY1QVVS9ZG3JTVDHNYYE
    # Note, above value is used in the Nextcloud config in the Redirection URI
    human_name: Nextcloud
    issuer: "https://cloud.sspaeth.de"
    client_id: "THISISMYLONGANDSECRETNEXTCLOUDCLIENTID" # needs to be configured in the Nextcloud OIDC settings
    client_secret: "THISISMYLONGANDSECRETNEXTCLOUDCLIENSECRET" # needs to be configured in the Nextcloud OIDC settings
    token_endpoint_auth_method: "client_secret_post"
    scope: "openid profile email"
    claims_imports:
        localpart:
          action: require
          template: "{{ user.preferred_username }}"
        displayname:
          action: suggest
          template: "{{ user.name }}"
        email:
          action: suggest
          template: "{{ user.email }}"
          set_email_verification: import

Ahh, last but not least you can allow MAS to use local passwords and upstream Nextcloud in parallel. If you want to only allow Nextcloud, you also need to disable local passwords. In this case, you will directly be forwarded to the Nextcloud login page instead of the auth.sspaeth.de/login one first.

passwords:
  enabled: false
  schemes:
  - version: 1
    algorithm: argon2id

Nextcloud

The OpenID Connect app/plugin adds the central config option OpenID Connect clients. In this I have set:

Name 	auth.sspaeth.de
# Note the Redirect URI below uses the
# upstream_oauth2->providers->id value from my MAS configuration. (see above)
Redirection URI: https://auth.sspaeth.de/upstream/callback/01B2BSNY1QVVS9ZG3JTVDHNYYE
Client Identifier: THISISMYLONGANDSECRETNEXTCLOUDCLIENTID
Secret: THISISMYLONGANDSECRETNEXTCLOUDCLIENTSECRET
Signing Algorithm: RS256
Type: confidential
Flows: Code & Implicit Authorization Flow
Limited to Groups: matrix # my choice to only allow specific users to log in.

Note, that it uses three values that I had configured in my MAS upstream_oauth2 part.

NGINX

My nginx server serves 1) matrix.sspaeth.de, these are noteworthy locations:

    # Only if you need Sliding Sync which runs on port 8009 on my box.
    # Not covered in this setup description.
    location ~ ^/(client/|_matrix/client/unstable/org.matrix.msc3575/sync) {
      proxy_pass http://127.0.0.1:8009$request_uri;
      proxy_set_header Host $http_host;
      proxy_buffering off;
      proxy_set_header X-Forwarded-For $remote_addr;
      proxy_set_header X-Forwarded-Proto $scheme;
    }       

These legacy matrix auth endpoints at matrix.sspaeth.de, need to be redirected to my MAS instance running on port 8080:

    # Forward legacy auth to the matrix-auth service
    location ~ ^/_matrix/client/(.*)/(login|logout|refresh) {
        proxy_pass http://localhost:8080;
        proxy_http_version 1.1;
    }

Plus all the “normal” matrix communication that should go to synapse:

    # Synapse
    location ~ ^(/_matrix|/_synapse/client) {
      # note: do not add a path (even a single /) after the port in `proxy_pass`,
      # otherwise nginx will canonicalise the URI and cause signature verification
      # errors.
      proxy_pass http://localhost:8008;
      proxy_set_header X-Forwarded-For $remote_addr;
      proxy_set_header X-Forwarded-Proto $scheme;
      proxy_set_header Host $host;

      # Nginx by default only allows file uploads up to 1M in size
      # Increase client_max_body_size to match max_upload_size defined in homeserver.yaml
      client_max_body_size 50M;

      # Synapse responses may be chunked, which is an HTTP/1.1 feature.
      proxy_http_version 1.1;
    }

Then we have 2) the domain auth.sspaeth.de which is where MAS does its magic. I pretty much just forward all traffic to the MAS server, and serve the static assets directly. The two relevant locations in nginx are:

    location / {
        proxy_pass http://[::1]:8080;        
        proxy_http_version 1.1;
    }

    # Optional: serve the assets directly via nginx
    location /assets/ {
        root /usr/local/share/mas-cli/;
        
        # Serve pre-compressed assets
        gzip_static on;
        # With the ngx_brotli module installed:
        #brotli_static on;
        # Cache assets for a year
        expires 365d;
    }

That was simple wasn’t it?

I don’t explain the nginx setup of Nextcloud, you should have one running beforehand if you are interested in this.

Now, if I want to login, I select “sspaeth.de” as a server in e.g. Element Web, click login and get presented with the regular Nextcloud login page.

That is all, folks.