About this documentation

This documentation is intended to give an overview of how the matrix-authentication-service works, both from an admin perspective and from a developer perspective.

The documentation itself is built using mdBook. A hosted version is available at https://matrix-org.github.io/matrix-authentication-service/.

  • Technical documentation for individual crates: rustdoc
  • UI components: storybook

Planning the installation

This part of the documentation goes through installing the service, the important parts of the configuration file, and how to run the service.

Before going through the installation, it is important to understand the different components of an OIDC-native Matrix homeserver, and how they interact with each other. It is meant to complement the homeserver, replacing the internal authentication mechanism with the authentication service.

Making a homeserver deployment OIDC-native radically shifts the authentication model: the homeserver is no longer responsible for managing user accounts and sessions. The authentication service becomes the source of truth for user accounts and access tokens, and the homeserver only verifies the validity of the tokens it receives through the service.

At time of writing, the authentication service is meant to be run on a standalone domain name (e.g. auth.example.com), and the homeserver on another (e.g. matrix.example.com). This domain will be user-facing as part of the authentication flow.

When a client initiates an authentication flow, it will discover the authentication service through the deployment .well-known/matrix/client endpoint. This file will refer to an issuer, which is the canonical name of the authentication service instance. Out of that issuer, it will discover the rest of the endpoints by calling the [issuer]/.well-known/openid-configuration endpoint. By default, the issuer will match the root domain where the service is deployed (e.g. https://auth.example.com/), but it can be configured to be different.

An example setup could look like this:

  • The deployment domain is example.com, so Matrix IDs look like @user:example.com

  • The issuer chosen is https://example.com/

  • The homeserver is deployed on matrix.example.com

  • The authentication service is deployed on auth.example.com

  • Calling https://example.com/.well-known/matrix/client returns the following JSON:

    {
      "m.homeserver": {
        "base_url": "https://matrix.example.com"
      },
      "org.matrix.msc2965.authentication": {
        "issuer": "https://example.com/",
        "account": "https://auth.example.com/account"
      }
    }
    
  • Calling https://example.com/.well-known/openid-configuration returns a JSON document similar to the following:

    {
        "issuer": "https://example.com/",
        "authorization_endpoint": "https://auth.example.com/authorize",
        "token_endpoint": "https://auth.example.com/oauth2/token",
        "jwks_uri": "https://auth.example.com/oauth2/keys.json",
        "registration_endpoint": "https://auth.example.com/oauth2/registration",
        "//": "..."
    }
    

With the installation planned, it is time to go through the installation and configuration process. The first section focuses on installing the service.

Installation

Pre-built binaries

Pre-built binaries can be found attached on each release, for Linux and macOS, on both x86_64 and aarch64 architectures.

Each archive contains:

  • the mas-cli binary
  • assets needed for running the service, including:
    • share/assets/: the built frontend assets
    • share/manifest.json: the manifest for the frontend assets
    • share/policy.wasm: the built OPA policies
    • share/templates/: the default templates
    • share/translations/: the default translations

The location of all these assets can be overridden in the configuration file.


Example shell commands to download and extract the mas-cli binary:

ARCH=x86_64 # or aarch64
OS=linux # or macos
VERSION=latest # or a specific version, like "v0.1.0"

# URL to the right archive
URL="https://github.com/matrix-org/matrix-authentication-service/releases/${VERSION}/download/mas-cli-${ARCH}-${OS}.tar.gz"

# Create a directory and extract the archive in it
mkdir -p /path/to/mas
curl -sL "$URL" | tar xzC /path/to/mas

# This should display the help message
/path/to/mas/mas-cli --help

Note for macOS users: the binaries are not signed, so if the archive is downloaded from a browser, it may be necessary to run xattr -d com.apple.quarantine mas-cli to remove the quarantine attribute on the binary.

Using the Docker image

A pre-built Docker image is available here: ghcr.io/matrix-org/matrix-authentication-service:main

The main tag is built from the main branch, and each commit on the main branch is also tagged with a stable sha-<commit sha> tag.

The image can also be built from the source:

  1. Get the source
    git clone https://github.com/matrix-org/matrix-authentication-service.git
    cd matrix-authentication-service
    
  2. Build the image
    docker build -t mas .
    

Building from the source

Building from the source requires:

  1. Get the source
    git clone https://github.com/matrix-org/matrix-authentication-service.git
    cd matrix-authentication-service
    
  2. Build the frontend
    cd frontend
    npm ci
    npm run build
    cd ..
    
    This will produce a frontend/dist directory containing the built frontend assets. This folder, along with the frontend/dist/manifest.json file, can be relocated, as long as the configuration file is updated accordingly.
  3. Build the Open Policy Agent policies
    cd policies
    make
    cd ..
    
    OR, if you don't have opa installed and want to build through the OPA docker image
    cd policies
    make DOCKER=1
    cd ..
    
    This will produce a policies/policy.wasm file containing the built OPA policies. This file can be relocated, as long as the configuration file is updated accordingly.
  4. Compile the CLI
    cargo build --release
    
  5. Grab the built binary
    cp ./target/release/mas-cli ~/.local/bin # Copy the binary somewhere in $PATH
    mas-cli --help # Should display the help message
    

Next steps

The service needs some configuration to work. This includes random, private keys and secrets. Follow the configuration guide to configure the service.

General configuration

Initial configuration generation

The service needs a few unique secrets and keys to work. It mainly includes:

  • the various signing keys referenced in the secrets.keys section
  • the encryption key used to encrypt fields in the database and cookies, set in the secrets.encryption section
  • a shared secret between the service and the homeserver, set in the matrix.secret section

Although it is possible to generate these secrets manually, it is strongly recommended to use the config generate command to generate a configuration file with unique secrets and keys.

mas-cli config generate > config.yaml

If you're using the docker container, the command mas-cli can be invoked with docker run:

docker run ghcr.io/matrix-org/matrix-authentication-service:main config generate > config.yaml

This applies to all of the mas-cli commands in this document.

Note: The generated configuration file is very extensive, and contains the default values for all the configuration options. This will be made easier to read in the future, but in the meantime, it is recommended to strip untouched options from the configuration file.

Using and inspecting the configuration file

When using the mas-cli, multiple configuration files can be loaded, with the following rule:

  1. If the --config option is specified, possibly multiple times, load the file at the specified path, relative to the current working directory
  2. If not, load the files specified in the MAS_CONFIG environment variable if set, separated by :, relative to the current working directory
  3. If not, load the file at config.yaml in the current working directory

The validity of the configuration file can be checked using the config check command:

# This will read both the `first.yaml` and `second.yaml` files
mas-cli config check --config=first.yaml --config=second.yaml

# This will also read both the `first.yaml` and `second.yaml` files
MAS_CONFIG=first.yaml:second.yaml mas-cli config check

# This will only read the `config.yaml` file
mas-cli config check

To help understand what the resulting configuration looks like after merging all the configuration files, the config dump command can be used:

mas-cli config dump

Configuration schema

The configuration file is validated against a JSON schema, which can be found here. Many tools in text editors can use this schema to provide autocompletion and validation.

Syncing the configuration file with the database

Some sections of the configuration file need to be synced every time the configuration file is updated. This includes the clients and upstream_oauth sections. The configuration is synced by default on startup, and can be manually synced using the config sync command.

By default, this will only add new clients and upstream OAuth providers and update existing ones, but will not remove entries that were removed from the configuration file. To do so, use the --prune option:

mas-cli config sync --prune

Next step

After generating the configuration file, the next step is to set up a database.

Database configuration

The service uses a PostgreSQL database to store all of its state. Although it may be possible to run with earlier versions, it is recommended to use PostgreSQL 13 or later. Connection to the database is configured in the database section of the configuration file.

Set up a database

You will need to create a dedicated PostgreSQL database for the service. The database can run on the same server as the service, or on a dedicated host. The recommended setup for this database is to create a dedicated role and database for the service.

Assuming your PostgreSQL database user is called postgres, first authenticate as the database user with:

su - postgres
# Or, if your system uses sudo to get administrative rights
sudo -u postgres bash

Then, create a postgres user and a database with:

# this will prompt for a password for the new user
createuser --pwprompt mas_user
createdb --owner=mas_user mas

The above will create a user called mas_user with a password of your choice, and a database called mas owned by the mas_user user.

Service configuration

Once the database is created, the service needs to be configured to connect to it. Edit the database section of the configuration file to match the database just created:

database:
  # Full connection string as per
  # https://www.postgresql.org/docs/13/libpq-connect.html#id-1.7.3.8.3.6
  uri: postgres://<user>:<password>@<host>/<database>

  # -- OR --
  # Separate parameters
  host: <host>
  port: 5432
  username: <user>
  password: <password>
  database: <database>

Database migrations

The service manages the database schema with embedded migrations. Those migrations are run automatically when the service starts, but it is also possible to run them manually. This is done using the database migrate command:

mas-cli database migrate

Next steps

Once the database is up, the remaining steps are to:

Homeserver configuration

The matrix-authentication-service is designed to be run alongside a Matrix homeserver. It currently only supports Synapse through the experimental OAuth delegation feature. The authentication service needs to be able to call the Synapse admin API to provision users through a shared secret, and Synapse needs to be able to call the service to verify access tokens using the OAuth 2.0 token introspection endpoint.

Provision a client for the Homeserver to use

In the clients section of the configuration file, add a new client with the following properties:

  • client_id: a unique identifier for the client. It must be a valid ULID, and it happens that 0000000000000000000SYNAPSE is a valid ULID.
  • client_auth_method: set to client_secret_basic. Other methods are possible, but this is the easiest to set up.
  • client_secret: a shared secret used for the homeserver to authenticate
clients:
  - client_id: 0000000000000000000SYNAPSE
    client_auth_method: client_secret_basic
    client_secret: "SomeRandomSecret"

Don't forget to sync the configuration file with the database after adding the client, using the config sync command.

Configure the connection to the homeserver

In the matrix section of the configuration file, add the following properties:

  • homeserver: corresponds to the server_name in the Synapse configuration file
  • secret: a shared secret the service will use to call the homeserver admin API
  • endpoint: the URL to which the homeserver is accessible from the service
matrix:
  homeserver: localhost:8008
  secret: "AnotherRandomSecret"
  endpoint: "http://localhost:8008"

Configure the homeserver to delegate authentication to the service

Set up the delegated authentication feature in the Synapse configuration in the experimental_features section:

experimental_features:
  msc3861:
    enabled: true

    # Synapse will call `{issuer}/.well-known/openid-configuration` to get the OIDC configuration
    issuer: http://localhost:8080/

    # Matches the `client_id` in the auth service config
    client_id: 0000000000000000000SYNAPSE
    # 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: "SomeRandomSecret"

    # Matches the `matrix.secret` in the auth service config
    admin_token: "AnotherRandomSecret"

    # URL to advertise to clients where users can self-manage their account
    account_management_url: "http://localhost:8080/account"

Set up the compatibility layer

The service exposes a compatibility layer to allow legacy clients to authenticate using the service. This works by exposing a few Matrix endpoints that should be proxied to the service.

The following Matrix Client-Server API endpoints need to be handled by the authentication service:

See the reverse proxy configuration guide for more information.

Configuring a reverse proxy

Although the service can be exposed directly to the internet, including handling the TLS termination, many deployments will want to run a reverse proxy in front of the service.

In those configuration, the service should be configured to listen on localhost or Unix domain socket.

Example configuration

http:
  public_base: https://auth.example.com/
  listeners:
    - name: web
      resources:
        - name: discovery
        - name: human
        - name: oauth
        - name: compat
        - name: graphql
        # Uncomment to serve the assets by the service
        #- name: assets
        #  path: ./share/assets/

      binds:
        # Bind on a local port
        - host: localhost
          port: 8080
          
        # OR bind on a Unix domain socket
        #- socket: /var/run/mas.sock

        # OR bind on a systemd socket
        #- fd: 0
        #  kind: tcp # or unix

      # Optional: use the PROXY protocol
      #proxy_protocol: true

Example nginx configuration

Note that the assets can be served directly by nginx, and the assets resource can be removed from the service configuration.

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name auth.example.com;

    ssl_certificate path/to/fullchain.pem;
    ssl_certificate_key path/to/privkey.pem;

    location / {
        proxy_pass http://localhost:8080;
        # OR via the Unix domain socket
        #proxy_pass http://unix:/var/run/mas.sock;
        
        proxy_http_version 1.1;

        # Optional: use the PROXY protocol
        #proxy_protocol on;
    }
    
    # Optional: serve the assets directly
    location /assets/ {
        root /path/to/share/assets/;
        
        # Serve pre-compressed assets
        gzip_static on;
        # With the ngx_brotli module installed
        # https://github.com/google/ngx_brotli
        #brotli_static on;
        
        # Cache assets for a year
        expires 365d;
    }
}

For the compatibility layer, the following endpoints need to be proxied to the service:

  • /_matrix/client/*/login
  • /_matrix/client/*/logout
  • /_matrix/client/*/refresh

For example, a nginx configuration could look like:

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    server_name matrix.example.com;
    
    # Forward to the auth service
    location ~ ^/_matrix/client/(.*)/(login|logout|refresh) {
        proxy_pass http://localhost:8080;
        # OR via the Unix domain socket
        #proxy_pass http://unix:/var/run/mas.sock;
        
        proxy_http_version 1.1;

        # Optional: use the PROXY protocol
        #proxy_protocol on;
    }

    # Forward to Synapse
    # as per https://matrix-org.github.io/synapse/latest/reverse_proxy.html#nginx
    location ~ ^(/_matrix|/_synapse/client) {
        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;

        client_max_body_size 50M;
        proxy_http_version 1.1;
    }
}

.well-known configuration

A .well-known/matrix/client file is required to be served to allow clients to discover the authentication service.

If no .well-known/matrix/client file is served currently then this will need to be enabled.

If the homeserver is Synapse and serving this file already then the correct values will already be included when the homeserver is configured to use MAS.

If the .well-known is hosted elsewhere then org.matrix.msc2965.authentication entries need to be included similar to the following:

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

For more context on what the correct values are, see here.

Configure an upstream SSO provider

The authentication service supports using an upstream OpenID Connect provider to authenticate its users. Multiple providers can be configured, and can be used in conjunction with the local password database authentication.

Any OIDC compliant provider should work with the service as long as it supports the authorization code flow.

Note that the service does not support other SSO protocols such as SAML, and there is no plan to support them in the future. A deployment which requires SAML or LDAP-based authentication should use a service like Dex to bridge between the SAML provider and the authentication service.

General configuration

Configuration of upstream providers is done in the upstream_oauth2 section of the configuration file, which has a providers list. Additions and changes to this sections are synced with the database on startup. Removals need to be applied using the mas-cli config sync --prune command.

An exhaustive list of all the parameters is available in the configuration file reference.

The general configuration usually goes as follows:

  • determine a unique id for the provider, which will be used as stable identifier between the configuration file and the database. This id must be a ULID, and can be generated using online tools like https://www.ulidtools.com
  • create an OAuth 2.0/OIDC client on the provider's side, using the following parameters:
    • redirect_uri: https://<auth-service-domain>/upstream/callback/<id>
    • response_type: code
    • response_mode: query
    • grant_type: authorization_code
  • fill the upstream_oauth2 section of the configuration file with the following parameters:
    • providers:
      • id: the previously generated ULID
      • client_id: the client ID of the OAuth 2.0/OIDC client given by the provider
      • client_secret: the client secret of the OAuth 2.0/OIDC client given by the provider
      • issuer: the issuer URL of the provider
      • scope: the scope to request from the provider. openid is usually required, and profile and email are recommended to import a few user attributes.
  • setup user attributes mapping to automatically fill the user profile with data from the provider. See the user attributes mapping section for more details.

User attributes mapping

The authentication service supports importing the following user attributes from the provider:

  • The localpart/username (e.g. @localpart:example.com)
  • The display name
  • An email address

For each of those attributes, administrators can configure a mapping using the claims provided by the upstream provider. They can also configure what should be done for each of those attributes. It can either:

  • ignore: ignore the attribute, and let the user fill it manually
  • suggest: suggest the attribute to the user, but let them opt-out of importing it
  • force: automatically import the attribute, but don't fail if it is not provided by the provider
  • require: automatically import the attribute, and fail if it is not provided by the provider

A Jinja2 template is used as mapping for each attribute. The template currently has one user variable, which is an object with the claims got through the id_token given by the provider. The following default templates are used:

  • localpart: {{ user.preferred_username }}
  • displayname: {{ user.name }}
  • email: {{ user.email }}

Multiple providers behaviour

Multiple authentication methods can be configured at the same time, in which case the authentication service will let the user choose which one to use. This is true if both the local password database and an upstream provider are configured, or if multiple upstream providers are configured. In such cases, the human_name parameter of the provider configuration is used to display a human-readable name for the provider, and the brand_name parameter is used to show a logo for well-known providers.

If there is only one upstream provider configured and the local password database is disabled (passwords.enabled is set to false), the authentication service will automatically trigger an authorization flow with this provider.

Sample configurations

This section contains sample configurations for popular OIDC providers.

Authentik

Authentik is an open-source IdP solution.

  1. Create a provider in Authentik, with type OAuth2/OpenID.
  2. The parameters are:
  • Client Type: Confidential
  • Redirect URIs: https://<auth-service-domain>/upstream/callback/<id>
  1. Create an application for the authentication service in Authentik and link it to the provider.
  2. Note the slug of your application, Client ID and Client Secret.

Authentication service configuration:

upstream_oauth2:
  providers:
    - id: 01HFRQFT5QFMJFGF01P7JAV2ME
      human_name: Authentik
      issuer: "https://<authentik-domain>/application/o/<app-slug>/" # TO BE FILLED
      client_id: "<client-id>" # TO BE FILLED
      client_secret: "<client-secret>" # TO BE FILLED
      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: always

Facebook

  1. You will need a Facebook developer account. You can register for one here.
  2. On the apps page of the developer console, "Create App", and choose "Allow people to log in with their Facebook account".
  3. Once the app is created, add "Facebook Login" and choose "Web". You don't need to go through the whole form here.
  4. In the left-hand menu, open "Use cases" > "Authentication and account creation" > "Customize" > "Settings"
    • Add https://<auth-service-domain>/upstream/callback/<id> as an OAuth Redirect URL.
  5. In the left-hand menu, open "App settings/Basic". Here you can copy the "App ID" and "App Secret" for use below.

Authentication service configuration:

upstream_oauth2:
  providers:
    - id: "01HFS3WM7KSWCEQVJTN0V9X1W6"
      issuer: "https://www.facebook.com"
      human_name: "Facebook"
      brand_name: "facebook"
      discovery_mode: disabled
      pkce_method: always
      authorization_endpoint: "https://facebook.com/v11.0/dialog/oauth/"
      token_endpoint: "https://graph.facebook.com/v11.0/oauth/access_token"
      jwks_uri: "https://www.facebook.com/.well-known/oauth/openid/jwks/"
      token_endpoint_auth_method: "client_secret_post"
      client_id: "<app-id>" # TO BE FILLED
      client_secret: "<app-secret>" # TO BE FILLED
      scope: "openid"
      claims_imports:
        localpart:
          action: ignore
        displayname:
          action: suggest
          template: "{{ user.name }}"
        email:
          action: suggest
          template: "{{ user.email }}"
          set_email_verification: always

GitLab

  1. Create a new application.
  2. Add the openid scope. Optionally add the profile and email scope if you want to import the user's name and email.
  3. Add this Callback URL: https://<auth-service-domain>/upstream/callback/<id>

Authentication service configuration:

upstream_oauth2:
  providers:
    - id: "01HFS67GJ145HCM9ZASYS9DC3J"
      issuer: "https://gitlab.com"
      human_name: "GitLab"
      brand_name: "gitlab"
      token_endpoint_auth_method: "client_secret_post"
      client_id: "<client-id>" # TO BE FILLED
      client_secret: "<client-secret>" # TO BE FILLED
      scope: "openid profile email"
      claims_imports:
        displayname:
          action: suggest
          template: "{{ user.name }}"
        localpart:
          action: ignore
        email:
          action: suggest
          template: "{{ user.email }}"

Google

  1. Set up a project in the Google API Console (see documentation)
  2. Add an "OAuth Client ID" for a Web Application under "Credentials"
  3. Add the following "Authorized redirect URI": https://<auth-service-domain>/upstream/callback/<id>

Authentication service configuration:

upstream_oauth2:
  providers:
    - id: 01HFS6S2SVAR7Y7QYMZJ53ZAGZ
      human_name: Google
      brand_name: "google"
      issuer: "https://accounts.google.com"
      client_id: "<client-id>" # TO BE FILLED
      client_secret: "<client-secret>" # TO BE FILLED
      scope: "openid profile email"
      claims_imports:
        localpart:
          action: ignore
        displayname:
          action: suggest
          template: "{{ user.name }}"
        email:
          action: suggest
          template: "{{ user.email }}"

Keycloak

Follow the Getting Started Guide to install Keycloak and set up a realm.

  1. Click Clients in the sidebar and click Create

  2. Fill in the fields as below:

    FieldValue
    Client IDmatrix-authentication-service
    Client Protocolopenid-connect
  3. Click Save

  4. Fill in the fields as below:

    FieldValue
    Client IDmatrix-authentication-service
    EnabledOn
    Client Protocolopenid-connect
    Access Typeconfidential
    Valid Redirect URIshttps://<auth-service-domain>/upstream/callback/<id>
  5. Click Save

  6. On the Credentials tab, update the fields:

    FieldValue
    Client AuthenticatorClient ID and Secret
  7. Click Regenerate Secret

  8. Copy Secret

upstream_oauth2:
  providers:
    - id: "01H8PKNWKKRPCBW4YGH1RWV279"
      issuer: "https://<keycloak>/realms/<realm>" # TO BE FILLED
      token_endpoint_auth_method: client_secret_basic
      client_id: "matrix-authentication-service"
      client_secret: "<client-secret>" # TO BE FILLED
      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: always

Microsoft Azure Active Directory

Azure AD can act as an OpenID Connect Provider. Register a new application under App registrations in the Azure AD management console. The RedirectURI for your application should point to your authentication service instance: https://<auth-service-domain>/upstream/callback/<id> where <id> is the same as in the config file.

Go to Certificates & secrets and register a new client secret. Make note of your Directory (tenant) ID as it will be used in the Azure links.

Authentication service configuration:

upstream_oauth2:
  providers:
    - id: "01HFRPWGR6BG9SAGAKDTQHG2R2"
      human_name: Microsoft Azure AD
      issuer: "https://login.microsoftonline.com/<tenant-id>/v2.0" # TO BE FILLED
      client_id: "<client-id>" # TO BE FILLED
      client_secret: "<client-secret>" # TO BE FILLED
      scope: "openid profile email"

      claims_imports:
        localpart:
          action: require
          template: "{{ (user.preferred_username | split('@'))[0] }}"
        displayname:
          action: suggest
          template: "{{ user.name }}"
        email:
          action: suggest
          template: "{{ user.email }}"
          set_email_verification: always

Running the service

To fully function, the service needs to run two main components:

  • An HTTP server
  • A background worker

By default, the mas-cli server command will start both components. It is possible to only run the HTTP server by setting the --no-worker option, and run a background worker with the mas-cli worker command.

Both components are stateless, and can be scaled horizontally by running multiple instances of each.

Runtime requirements

Other than the binary, the service needs a few files to run:

  • The templates, referenced by the templates.path configuration option
  • The compiled policy, referenced by the policy.path configuration option
  • The frontend assets, referenced by the path option of the assets resource in the http.listeners configuration section
  • The frontend manifest file, referenced by tge templates.assets_manifest configuration option

Be sure to check the installation instructions for more information on how to get these files, and make sure the configuration file is updated accordingly.

Configure the HTTP server

The service can be configured to have multiple HTTP listeners, serving different resources. See the http.listeners configuration section for more information.

The service needs to be aware of the public URL it is served on, regardless of the HTTP listeners configuration. This is done using the http.public_base configuration option. By default, the OIDC issuer advertised by the /.well-known/openid-configuration endpoint will be the same as the public_base URL, but can be configured to be different.

Tweak the remaining configuration

A few configuration sections might still require some tweaking, including:

  • telemetry: to setup metrics, tracing and Sentry crash reporting
  • email: to setup email sending
  • password: to enable/disable password authentication
  • upstream_oauth: to configure upstream OAuth providers

Run the service

Once the configuration is done, the service can be started with the mas-cli server command:

mas-cli server

It is advised to run the service as a non-root user, using a tool like systemd to manage the service lifecycle.

Troubleshoot common issues

Once the service is running, it is possible to check its configuration using the mas-cli doctor command. This should help diagnose common issues with the service configuration and deployment.

Migrating an existing homeserver

One of the design goals of MAS has been to allow it to be used to migrate an existing homeserver to an OIDC-based architecture.

Specifically without requiring users to re-authenticate and that non-OIDC clients continue to work.

Features that are provided to support this include:

  • Ability to import existing password hashes from Synapse
  • Ability to import existing sessions and devices
  • Ability to import existing access tokens linked to devices (ie not including short-lived admin puppeted access tokens)
  • Ability to import existing upstream IdP subject ID mappings
  • Provides a compatibility layer for legacy Matrix authentication

There will be tools to help with the migration process itself. But these aren't quite ready yet.

Preparing for the migration

The deployment is non-trivial so it is important to read through and understand the steps involved and make a plan before starting.

Run the migration advisor

You can use the advisor mode of the syn2mas tool to identify extra configuration steps or issues with the configuration of the homeserver.

syn2mas --command=advisor --synapseConfigFile=homeserver.yaml

This will output WARN entries for any identified actions and ERROR entries in the case of any issues that will prevent the migration from working.

Install and configure MAS alongside your existing homeserver

Follow the instructions in the installation guide to install MAS alongside your existing homeserver.

Map any upstream SSO providers

If you are using an upstream SSO provider then you will need to provision the upstream provide in MAS manually.

Each upstream provider will need to be given as an --upstreamProviderMapping command line option to the import tool.

Do a dry-run of the import to test

syn2mas --command migrate --synapseConfigFile homeserver.yaml --masConfigFile config.yaml --dryRun

If no errors are reported then you can proceed to the next step.

Doing the migration

Having done the preparation, you can now proceed with the actual migration. Note that this will require downtime for the homeserver and is not easily reversible.

Backup your data

As with any migration, it is important to backup your data before proceeding.

Shutdown the homeserver

This is to ensure that no new sessions are created whilst the migration is in progress.

Configure the homeserver

Follow the instructions in the homeserver configuration guide to configure the homeserver to use MAS.

Do the import

Run syn2mas in non-dry-run mode.

syn2mas --command migrate --synapseConfigFile homeserver.yaml --masConfigFile config.yaml --dryRun false

Start up the homeserver

Start up the homeserver again with the new configuration.

Update or serve the .well-known

The .well-known/matrix/client needs to be served as described here.

Configuration file reference

http

Controls the web server.

http:
  # Public URL base used when building absolute public URLs
  public_base: https://auth.example.com/

  # OIDC issuer advertised by the service. Defaults to `public_base`
  issuer: https://example.com/

  # List of HTTP listeners, see below
  listeners:
    # ...

http.listeners

Each listener can serve multiple resources, and listen on multiple TCP ports or UNIX sockets.

http:
  listeners:
    # The name of the listener, used in logs and metrics
    - name: web

      # List of resources to serve
      resources:
        # Serves the .well-known/openid-configuration document
        - name: discovery
        # Serves the human-facing pages, such as the login page
        - name: human
        # Serves the OAuth 2.0/OIDC endpoints
        - name: oauth
        # Serves the Matrix C-S API compatibility endpoints
        - name: compat
        # Serve the GraphQL API used by the frontend,
        # and optionally the GraphQL playground
        - name: graphql
          playground: true
        # Serve the given folder on the /assets/ path
        - name: assets
          path: ./share/assets/

      # List of addresses and ports to listen to
      binds:
        # First option: listen to the given address
        - address: "[::]:8080"

        # Second option: listen on the given host and port combination
        - host: localhost
          port: 8081

        # Third option: listen on the given UNIX socket
        - socket: /tmp/mas.sock

        # Fourth option: grab an already open file descriptor given by the parent process
        # This is useful when using systemd socket activation
        - fd: 1
          # Kind of socket that was passed, defaults to tcp
          kind: tcp # or unix

      # Whether to enable the PROXY protocol on the listener
      proxy_protocol: false

      # If set, makes the listener use TLS with the provided certificate and key
      tls:
        #certificate: <inline PEM>
        certificate_file: /path/to/cert.pem
        #key: <inline PEM>
        key_file: /path/to/key.pem
        #password: <password to decrypt the key>
        #password_file: /path/to/password.txt

The following additional resources are available, although it is recommended to serve them on a separate listener, not exposed to the public internet:

  • name: prometheus: serves a Prometheus-compatible metrics endpoint on /metrics, if the Prometheus exporter is enabled in telemetry.metrics.exporter.
  • name: health: serves the health check endpoint on /health.

database

Configure how to connect to the PostgreSQL database.

database:
  # Full connection string as per
  # https://www.postgresql.org/docs/13/libpq-connect.html#id-1.7.3.8.3.6
  uri: postgresql://user:password@hostname:5432/database?sslmode=require

  # -- OR --
  # Separate parameters
  host: hostname
  port: 5432
  #socket:
  username: user
  password: password
  database: database

  # Additional parameters for the connection pool
  min_connections: 0
  max_connections: 10
  connect_timeout: 30
  idle_timeout: 600
  max_lifetime: 1800

matrix

Settings related to the connection to the Matrix homeserver

matrix:
  # The homeserver name, as per the `server_name` in the Synapse configuration file
  homeserver: example.com

  # Shared secret used to authenticate the service to the homeserver
  # This must be of high entropy, because leaking this secret would allow anyone to perform admin actions on the homeserver
  secret: "SomeRandomSecret"

  # URL to which the homeserver is accessible from the service
  endpoint: "http://localhost:8008"

templates

Allows loading custom templates

templates:
  # From where to load the templates
  # This is relative to the current working directory, *not* the config file
  path: /to/templates

  # Path to the frontend assets manifest file
  assets_manifest: /to/manifest.json

clients

List of OAuth 2.0/OIDC clients and their keys/secrets. Each client_id must be a ULID.

clients:
  # Confidential client
  - client_id: 000000000000000000000FIRST
    client_auth_method: client_secret_post
    client_secret: secret
    # List of authorized redirect URIs
    redirect_uris:
      - http://localhost:1234/callback
  # Public client
  - client_id: 00000000000000000000SEC0ND
    client_auth_method: none

Note: any additions or modification in this list are synced with the database on server startup. Removed entries are only removed with the config sync --prune command.

secrets

Signing and encryption secrets

secrets:
  # Encryption secret (used for encrypting cookies and database fields)
  # This must be a 32-byte long hex-encoded key
  encryption: c7e42fb8baba8f228b2e169fdf4c8216dffd5d33ad18bafd8b928c09ca46c718

  # Signing keys
  keys:
    # It needs at least an RSA key to work properly
    - kid: "ahM2bien"
      key: |
        -----BEGIN RSA PRIVATE KEY-----
        MIIEowIBAAKCAQEAuf28zPUp574jDRdX6uN0d7niZCIUpACFo+Po/13FuIGsrpze
        yMX6CYWVPalgXW9FCrhxL+4toJRy5npjkgsLFsknL5/zXbWKFgt69cMwsWJ9Ra57
        bonSlI7SoCuHhtw7j+sAlHAlqTOCAVz6P039Y/AGvO6xbC7f+9XftWlbbDcjKFcb
        pQilkN9qtkdEH7TLayMAFOsgNvBlwF9+oj9w5PIk3veRTdBXI4GlHjhhzqGZKiRp
        oP9HnycHHveyT+C33vuhQso5a3wcUNuvDVOixSqR4kvSt4UVWNK/KmEQmlWU1/m9
        ClIwrs8Q79q0xkGaSa0iuG60nvm7tZez9TFkxwIDAQABAoIBAHA5YkppQ7fJSm0D
        wNDCHeyABNJWng23IuwZAOXVNxB1bjSOAv8yNgS4zaw/Hx5BnW8yi1lYZb+W0x2u
        i5X7g91j0nkyEi5g88kJdFAGTsM5ok0BUwkHsEBjTUPIACanjGjya48lfBP0OGWK
        LJU2Acbjda1aeUPFpPDXw/w6bieEthQwroq3DHCMnk6i9bsxgIOXeN04ij9XBmsH
        KPCP2hAUnZSlx5febYfHK7/W95aJp22qa//eHS8cKQZCJ0+dQuZwLhlGosTFqLUm
        qhPlt/b1EvPPY0cq5rtUc2W31L0YayVEHVOQx1fQIkH2VIUNbAS+bfVy+o6WCRk6
        s1XDhsECgYEA30tykVTN5LncY4eQIww2mW8v1j1EG6ngVShN3GuBTuXXaEOB8Duc
        yT7yJt1ZhmaJwMk4agmZ1/f/ZXBtfLREGVzVvuwqRZ+LHbqIyhi0wQJA0aezPote
        uTQnFn+IveHGtpQNDYGL/UgkexuCxbc2HOZG51JpunCK0TdtVfO/9OUCgYEA1TuS
        2WAXzNudRG3xd/4OgtkLD9AvfSvyjw2LkwqCMb3A5UEqw7vubk/xgnRvqrAgJRWo
        jndgRrRnikHCavDHBO0GAO/kzrFRfw+e+r4jcLl0Yadke8ndCc7VTnx4wQCrMi5H
        7HEeRwaZONoj5PAPyA5X+N/gT0NNDA7KoQT45DsCgYBt+QWa6A5jaNpPNpPZfwlg
        9e60cAYcLcUri6cVOOk9h1tYoW7cdy+XueWfGIMf+1460Z90MfhP8ncZaY6yzUGA
        0EUBO+Tx10q3wIfgKNzU9hwgZZyU4CUtx668mOEqy4iHoVDwZu4gNyiobPsyDzKa
        dxtSkDc8OHNV6RtzKpJOtQKBgFoRGcwbnLH5KYqX7eDDPRnj15pMU2LJx2DJVeU8
        ERY1kl7Dke6vWNzbg6WYzPoJ/unrJhFXNyFmXj213QsSvN3FyD1pFvp/R28mB/7d
        hVa93vzImdb3wxe7d7n5NYBAag9+IP8sIJ/bl6i9619uTxwvgtUqqzKPuOGY9dnh
        oce1AoGBAKZyZc/NVgqV2KgAnnYlcwNn7sRSkM8dcq0/gBMNuSZkfZSuEd4wwUzR
        iFlYp23O2nHWggTkzimuBPtD7Kq4jBey3ZkyGye+sAdmnKkOjNILNbpIZlT6gK3z
        fBaFmJGRJinKA+BJeH79WFpYN6SBZ/c3s5BusAbEU7kE5eInyazP
        -----END RSA PRIVATE KEY-----
    - kid: "iv1aShae"
      key: |
        -----BEGIN EC PRIVATE KEY-----
        MHQCAQEEIE8yeUh111Npqu2e5wXxjC/GA5lbGe0j0KVXqZP12vqioAcGBSuBBAAK
        oUQDQgAESKfUtKaLqCfhK+p3z870W59yOYvd+kjGWe+tK16SmWzZJbRCgdHakHE5
        MC6tJRnvedsYoKTrYoDv/XZIBI9zlA==
        -----END EC PRIVATE KEY-----

secrets.keys

The service can use a number of key types for signing. The following key types are supported:

  • RSA
  • ECDSA with the P-256 (prime256v1) curve
  • ECDSA with the P-384 (secp384r1) curve
  • ECDSA with the K-256 (secp256k1) curve

Each entry must have a unique (and arbitrary) kid, plus the key itself. The key can either be specified inline (with the key property), or loaded from a file (with the key_file property). The following key formats are supported:

  • PKCS#1 PEM or DER-encoded RSA private key
  • PKCS#8 PEM or DER-encoded RSA or ECDSA private key, encrypted or not
  • SEC1 PEM or DER-encoded ECDSA private key

For PKCS#8 encoded keys, the password or password_file properties can be used to decrypt the key.

passwords

Settings related to the local password database

passwords:
  # Whether to enable the password database.
  # If disabled, users will only be able to log in using upstream OIDC providers
  enabled: true

  # List of password hashing schemes being used
  # /!\ Only change this if you know what you're doing
  # TODO: document this section better
  schemes:
    - version: 1
      algorithm: argon2id

policy

Policy settings

policy:
  data:
    admin_users:
      - person1
      - person2

    # Dynamic Client Registration
    client_registration:
      # don't require URIs to be on the same host. default: false
      allow_host_mismatch: true
      # allow non-SSL and localhost URIs. default: false
      allow_insecure_uris: true

    # Registration using passwords
    passwords:
      # minimum length of a password. default: ?
      min_length: 8
      # require at least one lowercase character in a password. default: false
      require_lowercase: true
      # require at least one uppercase character in a password. default: false
      require_uppercase: true
      # require at least one number in a password. default: false
      require_number: true

telemetry

Settings related to metrics and traces

telemetry:
  tracing:
    # List of propagators to use for extracting and injecting trace contexts
    propagators:
      # Propagate according to the W3C Trace Context specification
      - tracecontext
      # Propagate according to the W3C Baggage specification
      - baggage
      # Propagate trace context with Jaeger compatible headers
      - jaeger

    # The default: don't export traces
    exporter: none

    # Export traces to an OTLP-compatible endpoint
    #exporter: otlp
    #endpoint: https://localhost:4318

  metrics:
    # The default: don't export metrics
    exporter: none

    # Export metrics to an OTLP-compatible endpoint
    #exporter: otlp
    #endpoint: https://localhost:4317

    # Export metrics by exposing a Prometheus endpoint
    # This requires mounting the `prometheus` resource to an HTTP listener
    #exporter: prometheus

  sentry:
    # DSN to use for sending errors and crashes to Sentry
    dsn: https://public@host:port/1

email

Settings related to sending emails

email:
  from: '"The almighty auth service" <auth@example.com>'
  reply_to: '"No reply" <no-reply@example.com>'

  # Default transport: don't send any emails
  transport: blackhole

  # Send emails using SMTP
  #transport: smtp
  #mode: plain | tls | starttls
  #hostname: localhost
  #port: 587
  #username: username
  #password: password

  # Send emails by calling a local sendmail binary
  #transport: sendmail
  #command: /usr/sbin/sendmail

  # Send emails through the AWS SESv2 API
  # This uses the AWS SDK, so the usual AWS environment variables are supported
  #transport: aws_ses

upstream_oauth2

Settings related to upstream OAuth 2.0/OIDC providers. Additions and modifications within this section are synced with the database on server startup. Removed entries are only removed with the config sync --prune command.

upstream_oauth2.providers

A list of upstream OAuth 2.0/OIDC providers to use to authenticate users.

Sample configurations for popular providers can be found in the upstream provider setup guide.

upstream_oauth2:
  providers:
    - # A unique identifier for the provider
      # Must be a valid ULID
      id: 01HFVBY12TMNTYTBV8W921M5FA

      # The issuer URL, which will be used to discover the provider's configuration.
      # If discovery is enabled, this *must* exactly match the `issuer` field
      # advertised in `<issuer>/.well-known/openid-configuration`.
      issuer: https://example.com/

      # A human-readable name for the provider,
      # which will be displayed on the login page
      #human_name: Example

      # A brand identifier for the provider, which will be used to display a logo
      # on the login page. Values supported by the default template are:
      #  - `apple`
      #  - `google`
      #  - `facebook`
      #  - `github`
      #  - `gitlab`
      #  - `twitter`
      #brand_name: google

      # The client ID to use to authenticate to the provider
      client_id: mas-fb3f0c09c4c23de4

      # The client secret to use to authenticate to the provider
      # This is only used by the `client_secret_post`, `client_secret_basic`
      # and `client_secret_jwk` authentication methods
      #client_secret: f4f6bb68a0269264877e9cb23b1856ab

      # Which authentication method to use to authenticate to the provider
      # Supported methods are:
      #   - `none`
      #   - `client_secret_basic`
      #   - `client_secret_post`
      #   - `client_secret_jwt`
      #   - `private_key_jwt` (using the keys defined in the `secrets.keys` section)
      token_endpoint_auth_method: client_secret_post

      # What signing algorithm to use to sign the authentication request when using 
      # the `private_key_jwt` or the `client_secret_jwt` authentication methods
      #token_endpoint_auth_signing_alg: RS256

      # The scopes to request from the provider
      # In most cases, it should always include `openid` scope
      scope: "openid email profile"

      # How the provider configuration and endpoints should be discovered
      # Possible values are:
      #  - `oidc`: discover the provider through OIDC discovery,
      #     with strict metadata validation (default)
      #  - `insecure`: discover through OIDC discovery, but skip metadata validation
      #  - `disabled`: don't discover the provider and use the endpoints below
      #discovery_mode: oidc

      # Whether PKCE should be used during the authorization code flow.
      # Possible values are:
      #  - `auto`: use PKCE if the provider supports it (default)
      #    Determined through discovery, and disabled if discovery is disabled
      #  - `always`: always use PKCE (with the S256 method)
      #  - `never`: never use PKCE
      #pkce_method: auto

      # The provider authorization endpoint
      # This takes precedence over the discovery mechanism
      #authorization_endpoint: https://example.com/oauth2/authorize

      # The provider token endpoint
      # This takes precedence over the discovery mechanism
      #token_endpoint: https://example.com/oauth2/token

      # The provider JWKS URI
      # This takes precedence over the discovery mechanism
      #jwks_uri: https://example.com/oauth2/keys

      # How user attributes should be mapped
      #
      # Most of those attributes have two main properties:
      #   - `action`: what to do with the attribute. Possible values are:
      #      - `ignore`: ignore the attribute
      #      - `suggest`: suggest the attribute to the user, but let them opt out
      #      - `force`: always import the attribute, and don't fail if it's missing
      #      - `require`: always import the attribute, and fail if it's missing
      #   - `template`: a Jinja2 template used to generate the value. In this template,
      #      the `user` variable is available, which contains the user's attributes 
      #      retrieved from the `id_token` given by the upstream provider.
      #
      # Each attribute has a default template which follows the well-known OIDC claims.
      #
      claims_imports:
        # The subject is an internal identifier used to link the
        # user's provider identity to local accounts.
        # By default it uses the `sub` claim as per the OIDC spec,
        # which should fit most use cases.
        subject:
          #template: "{{ user.sub }}"

        # The localpart is the local part of the user's Matrix ID.
        # For example, on the `example.com` server, if the localpart is `alice`,
        #  the user's Matrix ID will be `@alice:example.com`.
        localpart:
          #action: force
          #template: "{{ user.preferred_username }}"

        # The display name is the user's display name.
        displayname:
          #action: suggest
          #template: "{{ user.name }}"

        # An email address to import.
        email:
          #action: suggest
          #template: "{{ user.email }}"

          # Whether the email address must be marked as verified.
          # Possible values are:
          #  - `import`: mark the email address as verified if the upstream provider
          #     has marked it as verified, using the `email_verified` claim.
          #     This is the default.
          #   - `always`: mark the email address as verified
          #   - `never`: mark the email address as not verified
          #set_email_verification: import

Using the service

Running

Once the configuration is done, one should run the database migrations (database schema upgrade, also for setup) by running

mas-cli database migrate

The server can then be started by running

mas-cli server
Sep 24 14:42:42.743  INFO mas_cli::server: Starting task scheduler
Sep 24 14:42:42.743  INFO mas_core::templates: Loading builtin templates
Sep 24 14:42:42.752  INFO mas_cli::server: Listening on http://0.0.0.0:8080

The server should now be accessible through http://localhost:8080/.

Note: when running with Docker, the port used by the server should be exposed with the -p flag:

docker run --rm \
  -v `pwd`/config.yaml:/config.yaml \
  -p 8080:8080 \
  ghcr.io/matrix-org/matrix-authentication-service:main \
  server

Registering, logging in and out

Through the interface, users are able to create an account by clicking the Register button on the top right (or going to /register). They can then end their session by clicking the Sign out button and sign back in.

Playing around with the playground

The OpenID Foundation hosts a OpenID Connect Playground where one can test logging in through an OIDC provider: https://openidconnect.net/

Step 1: Add the client to the server config

Add the following section to the server configuration file config.yaml:

clients:
  # The client ID must be a valid ULID
  - client_id: 000000000000OIDCPLAYGROUND
    client_secret: verysecret
    redirect_uris:
      - "https://openidconnect.net/callback"

Sync the configuration with the database:

mas-cli config sync

Step 2: Change the playground configuration

  • Navigate to the playground
  • Click on "Configuration"
  • Server template: Custom
  • Paste the discovery document URL found on the service homepage (e.g. http://localhost:8080/.well-known/openid-configuration)
  • Click "Use discovery document" ; it should fill out the authorization, token and token keys endpoints
  • Set the OIDC Client ID to 000000000000OIDCPLAYGROUND and the Client Secret to verysecret (must match the ones in the configuration)
  • Click "Save"

Step 3: Run the OpenID Connect flow

Start the flow by clicking the "Start" button. It should redirect the browser to the authentication service. If a user is already logged in, it should redirect back to the playground immediately.

Follow the flow to see the code exchange happen. Note that the last step only works if the service is accessible through the internet.

Command line tool

The command line interface provides subcommands that helps running the service.

Logging

The overall log level of the CLI can be changed via the RUST_LOG environment variable. Default log level is info. Valid levels from least to most verbose are error, warn, info, debug and trace.

Global flags

--config

Sets the configuration file to load. It can be repeated multiple times to merge multiple files together.


Usage: mas-cli [OPTIONS] [COMMAND]

Commands:
  config     Configuration-related commands
  database   Manage the database
  server     Runs the web server
  worker     Run the worker
  manage     Manage the instance
  templates  Templates-related commands
  doctor     Run diagnostics on the deployment
  help       Print this message or the help of the given subcommand(s)

Options:
  -c, --config <CONFIG>  Path to the configuration file
  -h, --help             Print help

config

Helps to deal with the configuration

config check

Check the validity of configuration files.

$ mas-cli config check --config=config.yaml
INFO mas_cli::config: Configuration file looks good path=["config.yaml"]

config dump

Dump the merged configuration tree.

$ mas-cli config dump --config=first.yaml --config=second.yaml
---
clients:
  # ...

config generate

Generate a sample configuration file. It generates random signing keys (.secrets.keys) and the cookie encryption secret (.secrets.encryption).

$ mas-cli config generate > config.yaml
INFO generate: mas_config::oauth2: Generating keys...
INFO generate:rsa: mas_config::oauth2: Done generating RSA key
INFO generate:ecdsa: mas_config::oauth2: Done generating ECDSA key

config sync [--prune] [--dry-run]

Synchronize the configuration with the database. This will synchronize the clients and upstream_oauth sections of the configuration with the database. By default, it does not delete clients and upstreams that are not in the configuration anymore. Use the --prune option to do so. The --dry-run option will log the changes that would be made, without actually making them.

$ mas-cli config sync --prune --config=config.yaml
INFO cli.config.sync: Syncing providers and clients defined in config to database prune=true dry_run=false
INFO cli.config.sync: Updating provider provider.id=01H3FDH2XZJS8ADKRGWM84PZTY
INFO cli.config.sync: Adding provider provider.id=01H3FDH2XZJS8ADKRGWM84PZTF
INFO cli.config.sync: Deleting client client.id=01GFWRB9MYE0QYK60NZP2YF905
INFO cli.config.sync: Updating client client.id=01GFWRB9MYE0QYK60NZP2YF904

database

Run database-related operations

database migrate

Run the pending database migrations

$ mas-cli database migrate

manage

Includes admin-related subcommands.

manage verify-email <username> <email>

Mark a user email address as verified

server

Runs the authentication service.

$ mas-cli server
INFO mas_cli::server: Starting task scheduler
INFO mas_core::templates: Loading builtin templates
INFO mas_cli::server: Listening on http://0.0.0.0:8080

templates

Helps customizing templates.

templates save <path>

Save the builtin template in the specified folder.

$ mas-cli templates save ./templates
INFO mas_core::templates: Wrote template path="./templates/login.html"
INFO mas_core::templates: Wrote template path="./templates/register.html"
INFO mas_core::templates: Wrote template path="./templates/index.html"
INFO mas_core::templates: Wrote template path="./templates/reauth.html"
INFO mas_core::templates: Wrote template path="./templates/form_post.html"
INFO mas_core::templates: Wrote template path="./templates/error.html"
INFO mas_core::templates: Wrote template path="./templates/base.html"

By default this command won't overwrite existing files, but this behavior can be changed by adding the --overwrite flag.

templates check <path>

Check the validity of the templates in the specified folder. It compiles the templates and then renders them with different contexts.

$ mas-cli templates check ./templates
INFO mas_core::templates: Loading builtin templates
INFO mas_core::templates: Loading templates from filesystem path=./templates/**/*.{html,txt}
INFO mas_core::templates::check: Rendering template name="login.html" context={"csrf_token":"fake_csrf_token","form":{"fields_errors":{},"form_errors":[],"has_errors":false}}
INFO mas_core::templates::check: Rendering template name="register.html" context={"__UNUSED":null,"csrf_token":"fake_csrf_token"}
INFO mas_core::templates::check: Rendering template name="index.html" context={"csrf_token":"fake_csrf_token","current_session":{"active":true,"created_at":"2021-09-24T13:26:52.962135085Z","id":1,"last_authd_at":"2021-09-24T13:26:52.962135316Z","user_id":2,"username":"john"},"discovery_url":"https://example.com/.well-known/openid-configuration"}
...

Builtin templates are still loaded by default when running this command, but this can be skipped by adding the --skip-builtin flag.

doctor

Run diagnostics on the live deployment. This tool should help diagnose common issues with the service configuration and deployment.

When running this tool, make sure it runs from the same point-of-view as the service, with the same configuration file and environment variables.

$ mas-cli doctor

Contributing

This document aims to get you started with contributing to the Matrix Authentication Service!

1. Who can contribute to MAS?

Everyone is welcome to contribute code to matrix.org projects, provided that they are willing to license their contributions under the same license as the project itself. We follow a simple 'inbound=outbound' model for contributions: the act of submitting an 'inbound' contribution means that the contributor agrees to license the code under the same terms as the project's overall 'outbound' license - in our case, this is almost always Apache Software License v2 (see LICENSE).

2. What do I need?

To get MAS running locally from source you will need:

3. Get the source

  • Clone this repository

4. Build and run MAS

  • Build the frontend
    cd frontend
    npm ci
    npm run build
    cd ..
    
  • Build the Open Policy Agent policies
    cd policies
    make
    # OR, if you don't have `opa` installed and want to build through the OPA docker image
    make DOCKER=1
    cd ..
    
  • Generate the sample config via cargo run -- config generate > config.yaml
  • Run a PostgreSQL database locally
    docker run -p 5432:5432 -e 'POSTGRES_USER=postgres' -e 'POSTGRES_PASSWORD=postgres' -e 'POSTGRES_DATABASE=postgres' postgres
    
  • Update the database URI in config.yaml to postgresql://postgres:postgres@localhost/postgres
  • Run the database migrations via cargo run -- database migrate
  • Run the server via cargo run -- server -c config.yaml
  • Go to http://localhost:8080/

5. Learn about MAS

You can learn about the architecture and database of MAS here.

Architecture

The service is meant to be easily embeddable, with only a dependency to a database. It is also meant to stay lightweight in terms of resource usage and easily scalable horizontally.

Scope and goals

The Matrix Authentication Service has been created to support the migration of Matrix to an OpenID Connect (OIDC) based architecture as per MSC3861.

It is not intended to be a general purpose Identity Provider (IdP) and instead focuses on the specific needs of Matrix.

Furthermore, it is only intended that it would speak OIDC for authentication and not other protocols. Instead, if you want to connect to an upstream SAML, CAS or LDAP backend then you need to pair MAS with a separate service (such as Dex or Keycloak) which does that translation for you.

Whilst it only supports use with Synapse today, we hope that other homeservers will become supported in future.

If you need some other feature that MAS doesn't support (such as TOTP or WebAuthn), then you should consider pairing MAS with another IdP that does support the features you need.

Workspace and crate split

The whole repository is a Cargo Workspace that includes multiple crates under the /crates directory.

This includes:

  • mas-cli: Command line utility, main entry point
  • mas-config: Configuration parsing and loading
  • mas-data-model: Models of objects that live in the database, regardless of the storage backend
  • mas-email: High-level email sending abstraction
  • mas-handlers: Main HTTP application logic
  • mas-iana: Auto-generated enums from IANA registries
  • mas-iana-codegen: Code generator for the mas-iana crate
  • mas-jose: JWT/JWS/JWE/JWK abstraction
  • mas-static-files: Frontend static files (CSS/JS). Includes some frontend tooling
  • mas-storage: Abstraction of the storage backends
  • mas-storage-pg: Storage backend implementation for a PostgreSQL database
  • mas-tasks: Asynchronous task runner and scheduler
  • oauth2-types: Useful structures and types to deal with OAuth 2.0/OpenID Connect endpoints. This might end up published as a standalone library as it can be useful in other contexts.

Important crates

The project makes use of a few important crates.

Async runtime: tokio

Tokio is the async runtime used by the project. The choice of runtime does not have much impact on most of the code.

It has an impact when:

  • spawning asynchronous work (as in "not awaiting on it immediately")
  • running CPU-intensive tasks. They should be ran in a blocking context using tokio::task::spawn_blocking. This includes password hashing and other crypto operations.
  • when dealing with shared memory, e.g. mutexes, rwlocks, etc.

Logging: tracing

Logging is handled through the tracing crate. It provides a way to emit structured log messages at various levels.

#![allow(unused)]
fn main() {
use tracing::{info, debug};

info!("Logging some things");
debug!(user = "john", "Structured stuff");
}

tracing also provides ways to create spans to better understand where a logging message comes from. In the future, it will help building OpenTelemetry-compatible distributed traces to help with debugging.

tracing is becoming the standard to log things in Rust. By itself it will do nothing unless a subscriber is installed to -for example- log the events to the console.

The CLI installs tracing-subcriber on startup to log in the console. It looks for a RUST_LOG environment variable to determine what event should be logged.

Error management: thiserror / anyhow

thiserror helps defining custom error types. This is especially useful for errors that should be handled in a specific way, while being able to augment underlying errors with additional context.

anyhow helps dealing with chains of errors. It allows for quickly adding additional context around an error while it is being propagated.

Both crates work well together and complement each other.

Database interactions: sqlx

Interactions with the database are done through sqlx, an async, pure-Rust SQL library with compile-time check of queries. It also handles schema migrations.

Templates: tera

Tera was chosen as template engine for its simplicity as well as its ability to load templates at runtime. The builtin templates are embedded in the final binary through some macro magic.

The downside of Tera compared to compile-time template engines is the possibility of runtime crashes. This can however be somewhat mitigated with unit tests.

Crates from RustCrypto

The RustCrypto team offer high quality, independent crates for dealing with cryptography. The whole project is highly modular and APIs are coherent between crates.

Database

Interactions with the database goes through sqlx. It provides async database operations with connection pooling, migrations support and compile-time check of queries through macros.

Writing database interactions

All database interactions are done through repositoriy traits. Each repository trait usually manages one type of data, defined in the mas-data-model crate.

Defining a new data type and associated repository looks like this:

Some of those steps are documented in more details in the mas-storage and mas-storage-pg crates.

Compile-time check of queries

To be able to check queries, sqlx has to introspect the live database. Usually it does so by having the database available at compile time, but to avoid that we're using the offline feature of sqlx, which saves the introspection informatons as a flat file in the repository.

Preparing this flat file is done through sqlx-cli, and should be done everytime the database schema or the queries changed.

# Install the CLI
cargo install sqlx-cli --no-default-features --features postgres

cd crates/storage-pg/ # Must be in the mas-storage-pg crate folder
export DATABASE_URL=postgresql:///matrix_auth
cargo sqlx prepare

Migrations

Migration files live in the migrations folder in the mas-core crate.

cd crates/storage-pg/ # Again, in the mas-storage-pg crate folder
export DATABASE_URL=postgresql:///matrix_auth
cargo sqlx migrate run # Run pending migrations
cargo sqlx migrate add [description] # Add new migration files

Note that migrations are embedded in the final binary and can be run from the service CLI tool.

About Application Services login

Encrypted Application Services/Bridges currently leverage the m.login.application_service login type to create devices for users. This API is not available in the Matrix Authentication Service.

We're working on a solution to support this use case, but in the meantime, this means encrypted bridges will not work with the Matrix Authentication Service. A workaround is to disable E2EE support in your bridge setup.