About this documentation

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

MAS is an OAuth 2.0 and OpenID Provider server for Matrix. It has been created to support the migration of Matrix to an OpenID Connect (OIDC) based authentication layer as per MSC3861.

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

How the documentation is organized

This documentation has four main sections:

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 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
VERSION=latest # or a specific version, like "v0.1.0"

# URL to the right archive
URL="https://github.com/element-hq/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

Using the Docker image

A pre-built Docker image is available here: ghcr.io/element-hq/matrix-authentication-service:latest

The latest tag is built using the latest release. 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/element-hq/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/element-hq/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/element-hq/matrix-authentication-service 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
        - name: 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

Base nginx configuration

A basic configuration for nginx, which proxies traffic to the service would look like this:

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_http_version 1.1;
        proxy_pass http://localhost:8080;
        # OR via the Unix domain socket
        #proxy_pass http://unix:/var/run/mas.sock;

        # Forward the client IP address
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        # or, using the PROXY protocol
        #proxy_protocol on;
    }
}

Compatibility layer

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_http_version 1.1;
        proxy_pass http://localhost:8080;
        # OR via the Unix domain socket
        #proxy_pass http://unix:/var/run/mas.sock;

        # Forward the client IP address
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        # or, using the PROXY protocol
        #proxy_protocol on;
    }

    # Forward to Synapse
    # as per https://element-hq.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;
    }
}

Preserve the client IP

For rate-limiting and logging purposes, MAS needs to know the client IP address, which can be lost when using a reverse proxy. There are two ways to preserve the client IP address

X-Forwarded-For header

MAS can infer the client IP address from the X-Forwarded-For header. It will trust the value for this header only if the request comes from a trusted reverse proxy.

The range of IPs that can be trusted is configured using the trusted_proxies configuration option, which has the default private IP ranges.

http:
  trusted_proxies:
  - 192.168.0.0/16
  - 172.16.0.0/12
  - 10.0.0.0/10
  - 127.0.0.1/8
  - fd00::/8
  - ::1/128

With nginx, this can be achieved by setting the proxy_set_header directive to X-Forwarded-For $proxy_add_x_forwarded_for.

Proxy protocol

MAS supports the PROXY protocol to preserve the client IP address. To enable it, enable the proxy_protocol option on the listener:

http:
  listeners:
    - name: web
      resources:
        - name: discovery
        - name: human
        - name: oauth
        - name: compat
        - name: graphql
        - name: assets
      binds:
        - address: "[::]:8080"
      proxy_protocol: true

With nginx, this can be achieved by setting the proxy_protocol directive to on in the location block.

Serve assets directly

To avoid unnecessary round-trips, the assets can be served directly by nginx, and the assets resource can be removed from the service configuration.

http:
  listeners:
    - name: web
      resources:
        - name: discovery
        - name: human
        - name: oauth
        - name: compat
        - name: graphql
        # MAS doesn't need to serve the assets anymore
        #- name: assets
      binds:
        - address: "[::]:8080"
      proxy_protocol: true

Make sure the assets directory served by nginx is up to date.

server {
    # --- SNIP ---

    location / {
        # --- SNIP ---
    }

    # Make nginx 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;
    }
}

.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.

If you are using the docker image, everything is already included in the image at the right place, so in most cases you don't need to do anything.

If you are using the pre-built binaries, those files are shipped alongside them in the share directory. The default configuration will look for them from the current working directory, meaning that you don't have to adjust the paths, as long as you are running the service from the parent directory of the share directory.

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
  • account: to configure what account management features are enabled
  • 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.

Get syn2mas

The easiest way to get syn2mas is through npm:

npm install -g @vector-im/syn2mas

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.

Prepare the MAS database

Once the database is created, it still needs to have its schema created and synced with the configuration. This can be done with the following command:

mas-cli config sync

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.

Policy engine

A set of actions are controlled by a generic policy engine. A decision of the policy engine is deterministically made based on three components:

  • The policy itself
  • A static configuration
  • The action to be performed

The policy is a Open Policy Agent (OPA) policy compiled into WebAssembly. Matrix Authentication Service ships with a default policy which should be sufficient for most deployments. It can be replaced with a custom policy if needed, which can be useful to implement custom authorization logic without recompiling the service.

Actions

The policy engine mainly restricts three operations:

  • User attributes, which includes user registration, user profile updates, and user password changes.
  • Client registration, when an OAuth 2.0 dynamic client registration is requested.
  • Authorization requests, when a client requests an access token.

Policies are only evaluated in user-facing contexts, and not in administrative contexts. As such, they usually can be bypassed through the admin API or the CLI if needed.

User attributes

The policy is evaluated in three different scenarios:

  • register.rego: During user registration, either with password credentials or with an upstream OAuth 2.0 provider. This calls the email.rego and password.rego policies as well.
  • email.rego: When a user adds a new email address to their account.
  • password.rego: When a user changes their password.

Client registration

The policy (client_registration.rego) is evaluated when a client sends their metadata through the OAuth 2.0 dynamic client registration API. By default, it enforces a set of strict rules to make sure clients provide enough information about themselves, with coherent URLs. This is useful in production environments, but can be relaxed in development environments.

Authorization requests

The policy (authorization_grant.rego) is evaluated when a client requests an access token. This only covers OAuth 2.0 sessions, not compatibility sessions. It is evaluated for the authorization code grant, the client credentials grant and the device authorization grant.

This is probably the most interesting policy, as it defines which scope can be granted to which user and which client.

On evaluation, three main entities are available:

  • details about the grant, such as the type of grant and the requested scopes
  • the client making the request
  • the user with their attributes (only for the authorization code grant and the device authorization grant)

The policy evaluation cannot modify the grant, only allow or deny it. Therefore the client must know in advance which scope they want to request.

This is an important concept to understand: what access a token has is stored in the session itself, therefore access to privileged scopes is only based on policy evaluation, not on user attributes.

If we take the Synapse admin API access as an example, the fact that an access token has admin API access doesn't depend on attributes on the user directly. Instead, it is during the creation of the session that:

  • the client asks for the corresponding scope (e.g. urn:synapse:admin:*)
  • the policy engine decides whether to grant it or not

The default policy shipped with the service does gate access to this scope based on a user attributes (can_request_admin), but this is not a requirement.

It does make reasoning about admin access more complicated compared to a simple boolean flag on the user like what Synapse does, but it also allows for more complex authorization logic. This is especially important as in the future it will make it possible to implement a more granular role-based access control system to fit more complex use cases.

To understand the authorization process and how sessions are created, refer to the authorization and sessions section.

Authorization and sessions

The main job of the authentication service is to grant access to resources to clients, and to let resources know who is accessing them. In less abstract terms, this means that the service is responsible for issuing access tokens and letting the homeserver (and other services) introspect those access tokens.

How access tokens work

In MAS, the access token is an opaque string for which the service has metadata associated with it. An access token has:

  • a subject, which is the user the token is issued for
  • a list of scopes
  • a client for which the token is issued
  • a timeframe for which the token is valid

On a single token, metadata is immutable: it doesn't change over time. One exception is the validity of the token: the service may revoke a token before its expiration date.

A typical client will get a short-lived access token (valid 5 minutes) along with a refresh token. The refresh token can then be used to get a new access token without the user having to re-authenticate.

How Synapse behaves

When an incoming request is made to Synapse, it will introspect the access token through the Matrix Authentication Service. This is using a standard OAuth 2.0 introspection request (RFC 7662).

Out of this request, Synapse will care about the following:

  • the active field, which tells if the token is valid or not
  • the sub field, which tells which user the token is issued for. This is an opaque string, and Synapse saves the mapping between the Matrix user ID and the subject of the token in its own database
  • in case Synapse doesn't know the presented subject, it will look at the username field, which it will use as the localpart for the user as fallback
  • the scope field, which tells which scopes are granted to the token. More specifically, it will look for the following scopes:

It's important to understand that when Synapse delegates authentication to MAS, Synapse no longer manages many user attributes. This includes the user admin, locked, and deactivated status.

Compatibility sessions

In addition to OAuth 2.0 sessions, for which we'll go into more details later, MAS also supports the legacy /_matrix/client/v3/login API. This exists as a compatibility layer for clients that don't yet support OAuth 2.0, but has some restrictions compared to the way those sessions behaved in Synapse.

When a client presents a compatibility access token to Synapse, MAS will make it look like to Synapse as if the token had the following scopes:

Which corresponds to the broad access to the Matrix C-S API and the device ID of the client, as one would expect from the legacy login API. One important missing scope is urn:synapse:admin:*, which means that the client won't have access to the Synapse admin API.

This is the case even if the user has the can_request_admin attribute set to true, and this is by design: the legacy login API doesn't have a way to request specific scopes, and we don't want to grant admin access to all clients that have a compatibility session. This was the case in the past with Synapse, as the admin status was set on the user itself, but this is not the case anymore with MAS.

OAuth 2.0 sessions

Modern clients are expected to use OAuth 2.0 to authenticate with the homeserver. In OAuth 2.0/OIDC, there are multiple ways to start an OAuth 2.0 session called grants.

An OAuth 2.0 session has three important properties:

  • the client, which is the application accessing the resource
  • the user, which is the user for which the client is accessing the resource
  • a set of scopes, which are the permission granted to the client

There are two main ways to create a client in MAS:

Authorized as a user or authorized as a client

OAuth 2.0 has an interesting concept where a session can be authorized not just as a user, but also as a client. This means an OAuth 2.0 session can be created without a user, and only with a client. It is useful for automated machine-to-machine communication, and is often referred to as "service accounts".

Synapse doesn't yet support this concept, and as such requesting any Synapse API, even the admin API, requires a user attached to the session.

This isn't the case with MAS' GraphQL API, which can be accessed with a client-only session: the API can be requested by a session which has the urn:mas:graphql:* and the urn:mas:admin scope without being backed by a user.

Supported authorization grants

MAS supports a few different authorization grants for OAuth 2.0 sessions. Whilst this section won't go into the technical details of how those grants work, it's important to understand what they are and what they are used for.

Grant typeEntityUser interactionMatrix C-S APISynapse Admin APIMAS Admin APIMAS Internal GraphQL API
Authorization codeUserSame deviceYesYesYesYes
Device authorizationUserOther deviceYesYesYesYes
Client credentialsClientNoneNoNo1YesYes
1

The Synapse admin API doesn't strictly require a user, but Synapse doesn't support client-only sessions yet. In the future, it will be possible to leverage the client credentials grant to access the Synapse admin API.

Authorization code grant

The authorization code grant (RFC 6749 section 4.1) is used to interactively log in the user on the same device as the client. This is the most common grant for most Matrix clients and is targeted at human end users.

The general idea is that the client (after registering itself) crafts an authorization URL that the user will visit in their web browser. The authentication service does whatever it needs to do to authenticate the user, and once the user is authenticated and consented to the access request, the service redirects the user back to the client with an authorization code. The client then exchanges this authorization code for an access token and a refresh token.

This grant is not meant for automation: it requires user interaction on the same device as where the client lives.

Device authorization grant

The device authorization grant (RFC 8628) is similar to the authorization code grant, but separates the user interaction from where the client lives.

A classic example of this grant is when a client is on a TV or a game console, where the user wouldn't want to enter their credentials on the device itself. Instead, the user is shown a code on the device, which they then enter on a different device (like a phone or a computer) to authenticate.

For Matrix, it has two main use cases:

  • for CLI tools (or other constrained clients) which can't open a web browser or can't catch a redirect
  • for a "login from another existing device" feature, like the "login via QR code" described in MSC4108

This grant isn't meant for automation either, as it still requires user interaction.

Client credentials grant

The client credentials grant (RFC 6749 section 4.4) is a bit special, as it lets a client authenticate as itself, without a user.

This has no meaning yet in the Matrix C-S API, but is useful for other APIs like the MAS GraphQL API. It may also be used in the future as a foundation for a new Application Service API, replacing the current hs_token/as_token mechanism.

This works by presenting the client credentials to get back an access token. The simplest type of client credentials is a client ID and client secret pair, but MAS also supports client authentication with a JWT (RFC 7523), which is a robust way to authenticate clients without a shared secret.

Admin API

MAS provides a REST-like API for administrators to manage the service. This API is intended to build tools on top of MAS, and is only available to administrators.

Note: This Admin API is now the correct way for external tools to interact with MAS. External access to the Internal GraphQL API is deprecated and will be removed in a future release.

Enabling the API

The API isn't exposed by default, and must be added to either a public or a private HTTP listener. It is considered safe to expose the API to the public, as access to it is gated by the urn:mas:admin scope.

To enable the API, tweak the http.listeners configuration section to add the adminapi resource:

http:
  listeners:
    - name: web
      resources:
        # Other public resources
        - name: discovery
        # …
        - name: adminapi
      binds:
        - address: "[::]:8080"
    # or to a separate, internal listener:
    - name: internal
      resources:
        # Other internal resources
        - name: health
        - name: prometheus
        # …
        - name: adminapi
      binds:
        - host: localhost
          port: 8081

Reference documentation

The API is documented using the OpenAPI specification. The API schema is available here. This schema can be viewed in tools like Swagger UI, available here.

If admin API is enabled, MAS will also serve the specification at /api/spec.json, with a Swagger UI available at /api/doc/.

Authentication

All requests to the admin API are gated using access tokens obtained using OAuth 2.0 grants. They must have the urn:mas:admin scope.

User-interactive tools

If the intent is to build admin tools where the administrator logs in themselves, interactive grants like the authorization code grant or the device authorization grant should be used.

In this case, whether the user can request admin access or not is defined by the can_request_admin attribute of the user.

To try it out in Swagger UI, a client can be defined statically in the configuration file like this:

clients:
  - client_id: 01J44Q10GR4AMTFZEEF936DTCM
    # For the authorization_code grant, Swagger UI uses the client_secret_post authentication method
    client_auth_method: client_secret_post
    client_secret: wie9oh2EekeeDeithei9Eipaeh2sohte
    redirect_uris:
      # The Swagger UI callback in the hosted documentation
      - https://element-hq.github.io/matrix-authentication-service/api/oauth2-redirect.html
      # The Swagger UI callback hosted by the service
      - https://mas.example.com/api/doc/oauth2-redirect

Then, in Swagger UI, click on the "Authorize" button. In the modal, enter the client ID and client secret in the authorizationCode section, select the urn:mas:admin scope and click on the "Authorize" button.

Automated tools

If the intent is to build tools that are not meant to be used by humans, the client credentials grant should be used.

In this case, the client must be listed in the policy.data.admin_clients configuration option.

policy:
  data:
    admin_clients:
      - 01J44QC8BCY7FCFM7WGHQGKMTJ

To try it out in Swagger UI, a client can be defined statically in the configuration file like this:

clients:
  - client_id: 01J44QC8BCY7FCFM7WGHQGKMTJ
    # For the client_credentials grant, Swagger UI uses the client_secret_basic authentication method
    client_auth_method: client_secret_basic
    client_secret: eequie6Oth4Ip2InahT5zuQu8OuPohLi

Then, in Swagger UI, click on the "Authorize" button. In the modal, enter the client ID and client secret in the clientCredentials section, select the urn:mas:admin scope and click on the "Authorize" button.

General API shape

The API takes inspiration from the JSON API specification for its request and response shapes.

Single resource

When querying a single resource, the response is generally shaped like this:

{
  "data": {
    "type": "type-of-the-resource",
    "id": "unique-id-for-the-resource",
    "attributes": {
      "some-attribute": "some-value"
    },
    "links": {
      "self": "/api/admin/v1/type-of-the-resource/unique-id-for-the-resource"
    }
  },
  "links": {
    "self": "/api/admin/v1/type-of-the-resource/unique-id-for-the-resource"
  }
}

List of resources

When querying a list of resources, the response is generally shaped like this:

{
  "meta": {
    "count": 42
  },
  "data": [
    {
      "type": "type-of-the-resource",
      "id": "unique-id-for-the-resource",
      "attributes": {
        "some-attribute": "some-value"
      },
      "links": {
        "self": "/api/admin/v1/type-of-the-resource/unique-id-for-the-resource"
      }
    },
    { "...": "..." },
    { "...": "..." }
  ],
  "links": {
    "self": "/api/admin/v1/type-of-the-resource?page[first]=10&page[after]=some-id",
    "first": "/api/admin/v1/type-of-the-resource?page[first]=10",
    "last": "/api/admin/v1/type-of-the-resource?page[last]=10",
    "next": "/api/admin/v1/type-of-the-resource?page[first]=10&page[after]=some-id",
    "prev": "/api/admin/v1/type-of-the-resource?page[last]=10&page[before]=some-id"
  }
}

The meta will have the total number of items in it, and the links object contains the links to the next and previous pages, if any.

Pagination is cursor-based, where the ID of items is used as the cursor. Resources can be paginated forwards using the page[after] and page[first] parameters, and backwards using the page[before] and page[last] parameters.

Error responses

Error responses will use a 4xx or 5xx status code, with the following shape:

{
  "errors": [
    {
      "title": "Error title"
    }
  ]
}

Well-known error codes are not yet specified.

Example

With the following configuration:

clients:
  - client_id: 01J44RKQYM4G3TNVANTMTDYTX6
    client_auth_method: client_secret_basic
    client_secret: phoo8ahneir3ohY2eigh4xuu6Oodaewi

policy:
  data:
    admin_clients:
      - 01J44RKQYM4G3TNVANTMTDYTX6

curl example to list the users that are not locked and have the can_request_admin flag set to true:

CLIENT_ID=01J44RKQYM4G3TNVANTMTDYTX6
CLIENT_SECRET=phoo8ahneir3ohY2eigh4xuu6Oodaewi

# Get an access token
curl \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d "grant_type=client_credentials&scope=urn:mas:admin" \
  https://mas.example.com/oauth2/token \
  | jq -r '.access_token' \
  | read -r ACCESS_TOKEN

# List users (The -g flag prevents curl from interpreting the brackets in the URL)
curl \
  -g \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  'https://mas.example.com/api/admin/v1/users?filter[can_request_admin]=true&filter[status]=active&page[first]=100' \
  | jq
Sample output
{
  "meta": {
    "count": 2
  },
  "data": [
    {
      "type": "user",
      "id": "01J2KDPHTZYW3TAT1SKVAD63SQ",
      "attributes": {
        "username": "kilgore-trout",
        "created_at": "2024-07-12T12:11:46.911578Z",
        "locked_at": null,
        "can_request_admin": true
      },
      "links": {
        "self": "/api/admin/v1/users/01J2KDPHTZYW3TAT1SKVAD63SQ"
      }
    },
    {
      "type": "user",
      "id": "01J3G5W8MRMBJ93ZYEGX2BN6NK",
      "attributes": {
        "username": "quentin",
        "created_at": "2024-07-23T16:13:04.024378Z",
        "locked_at": null,
        "can_request_admin": true
      },
      "links": {
        "self": "/api/admin/v1/users/01J3G5W8MRMBJ93ZYEGX2BN6NK"
      }
    }
  ],
  "links": {
    "self": "/api/admin/v1/users?filter[can_request_admin]=true&filter[status]=active&page[first]=100",
    "first": "/api/admin/v1/users?filter[can_request_admin]=true&filter[status]=active&page[first]=100",
    "last": "/api/admin/v1/users?filter[can_request_admin]=true&filter[status]=active&page[last]=100"
  }
}

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/
        # Serve the admin API on the /api/admin/v1/ path. Disabled by default
        #- name: adminapi

      # 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

  # Whether to use SSL to connect to the database
  ssl_mode: require # or disable, prefer, verify-ca, verify-full
  #ssl_ca: # PEM-encoded certificate
  ssl_ca_file: /path/to/ca.pem # Path to the root certificate file

  # Client certificate to present to the server when SSL is enabled
  #ssl_certificate: # PEM-encoded certificate
  ssl_certificate_file: /path/to/cert.pem # Path to the certificate file
  #ssl_key: # PEM-encoded key
  ssl_key_file: /path/to/key.pem # Path to the key file

  # 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 modifications 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

  # Minimum complexity required for passwords, estimated by the zxcvbn algorithm
  # Must be between 0 and 4, default is 3
  # See https://github.com/dropbox/zxcvbn#usage for more information
  minimum_complexity: 3

  # 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

account

Configuration related to account management

account:
  # Whether users are allowed to change their email addresses.
  #
  # Defaults to `true`.
  email_change_allowed: true

  # Whether users are allowed to change their display names
  #
  # Defaults to `true`.
  # This should be in sync with the policy in the homeserver configuration.
  displayname_change_allowed: true

  # Whether to enable self-service password registration
  #
  # Defaults to `false`.
  # This has no effect if password login is disabled.
  password_registration_enabled: false

  # Whether users are allowed to change their passwords
  #
  # Defaults to `true`.
  # This has no effect if password login is disabled.
  password_change_allowed: true

  # Whether email-based password recovery is enabled
  #
  # Defaults to `false`.
  # This has no effect if password login is disabled.
  password_recovery_enabled: false

captcha

Settings related to CAPTCHA protection

captcha:
    # Which service to use for CAPTCHA protection. Set to `null` (or `~`) to disable CAPTCHA protection
    service: ~

    # Use Google reCAPTCHA v2
    #service: recaptcha_v2
    #site_key: "6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI"
    #secret_key: "6LeIxAcTAAAAAGG"-vFI1TnRWxMZNFuojJ4WifJWe

    # Use Cloudflare Turnstile
    #service: cloudflare_turnstile
    #site_key: "1x00000000000000000000AA"
    #secret_key: "1x0000000000000000000000000000000AA"

    # Use hCaptcha
    #service: hcaptcha
    #site_key: "10000000-ffff-ffff-ffff-000000000001"
    #secret_key: "0x0000000000000000000000000000000000000000"

policy

Policy settings

policy:
  # Path to the WASM module
  # Default in Docker distribution: `/usr/local/share/mas-cli/policy.wasm`
  # Default in pre-built binaries: `./share/policy.wasm`
  # Default in locally-built binaries: `./policies/policy.wasm`
  wasm_module: ./policies/policy.wasm
  # Entrypoint to use when evaluating client registrations
  client_registration_entrypoint: client_registration/violation
  # Entrypoint to use when evaluating user registrations
  register_entrypoint: register/violation
  # Entrypoint to use when evaluating authorization grants
  authorization_grant_entrypoint: authorization_grant/violation
  # Entrypoint to use when changing password
  password_entrypoint: password/violation
  # Entrypoint to use when adding an email address
  email_entrypoint: email/violation

  # This data is being passed to the policy
  data:
    # Users which are allowed to ask for admin access. If possible, use the
    # can_request_admin flag on users instead.
    admin_users:
      - person1
      - person2

    # Client IDs which are allowed to ask for admin access with a
    # client_credentials grant
    admin_clients:
      - 01H8PKNWKKRPCBW4YGH1RWV279
      - 01HWQCPA5KF10FNCETY9402WGF

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

    # Restrict emails on registration to a specific domain
    # Items in this array are evaluated as a glob
    allowed_domains:
      - *.example.com
    # Ban specific domains from registration
    banned_domains:
      - *.banned.example.com

rate_limiting

Settings for limiting the rate of user actions to prevent abuse.

Each rate limiter consists of two options:

  • burst: a base amount of how many actions are allowed in one go.
  • per_second: how many units of the allowance replenish per second.
rate_limiting:
  # Limits how many account recovery attempts are allowed.
  # These limits can protect against e-mail spam.
  #
  # Note: these limit also apply to recovery e-mail re-sends.
  account_recovery:
    # Controls how many account recovery attempts are permitted
    # based on source IP address.
    per_ip:
      burst: 3
      per_second: 0.0008

    # Controls how many account recovery attempts are permitted
    # based on the e-mail address that is being used for recovery.
    per_address:
      burst: 3
      per_second: 0.0002

  # Limits how many login attempts are allowed.
  #
  # Note: these limit also applies to password checks when a user attempts to
  # change their own password.
  login:
    # Controls how many login attempts are permitted
    # based on source IP address.
    # This can protect against brute force login attempts.
    per_ip:
      burst: 3
      per_second: 0.05

    # Controls how many login attempts are permitted
    # based on the account that is being attempted to be logged into.
    # This can protect against a distributed brute force attack
    # but should be set high enough to prevent someone's account being
    # casually locked out.
    per_account:
      burst: 1800
      per_second: 0.5

  # Limits how many registrations attempts are allowed,
  # based on source IP address.
  # This limit can protect against e-mail spam and against people registering too many accounts.
  registration:
    burst: 3
    per_second: 0.0008

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

      # Which 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

experimental

Settings that may change or be removed in future versions. Some of which are in this section because they don't have a stable place in the configuration yet.

experimental:
  # Time-to-live of OAuth 2.0 access tokens in seconds. Defaults to 300, 5 minutes.
  #access_token_ttl: 300

  # Time-to-live of compatibility access tokens in seconds, when refresh tokens are supported. Defaults to 300, 5 minutes.
  #compat_token_ttl: 300
API documentation

OAuth 2.0 scopes

The default policy shipped with MAS supports the following scopes:

OpenID Connect scopes

MAS supports the following standard OpenID Connect scopes, as defined in OpenID Connect Core 1.0:

openid

The openid scope is a special scope that indicates that the client is requesting an OpenID Connect id_token. The userinfo endpoint as described by the same specification requires this scope to be present in the request.

The default policy allows any client and any user to request this scope.

email

Requires the openid scope to be present in the request. It adds the user's email address to the id_token and to the claims returned by the userinfo endpoint.

The default policy allows any client and any user to request this scope.

Those scopes are specific to the Matrix protocol and are part of MSC2967.

urn:matrix:org.matrix.msc2967.client:api:*

This scope grants access to the full Matrix client-server API.

The default policy allows any client and any user to request this scope.

urn:matrix:org.matrix.msc2967.client:device:[device id]

This scope sets the device ID of the session, where [device id] is the device ID of the session. Currently, MAS only allows the following characters in the device ID: a-z, A-Z, 0-9 and -. It also needs to be at least 10 characters long.

There can only be one device ID in the scope list of a session.

The default policy allows any client and any user to request this scope.

urn:matrix:org.matrix.msc2967.client:guest

This scope grants access to a restricted set of endpoints that are available to guest users. It is mutually exclusive with the urn:matrix:org.matrix.msc2967.client:api:* scope.

Note that MAS doesn't yet implement any special semantic around guest users, but this scope is reserved for future use.

The default policy allows any client and any user to request this scope.

Synapse-specific scopes

MAS also supports one Synapse-specific scope, which aren't formally defined in any specification.

urn:synapse:admin:*

This scope grants access to the Synapse admin API.

Because of how Synapse works for now, this scope by itself isn't sufficient to access the admin API. A session wanting to access the admin API also needs to have the urn:matrix:org.matrix.msc2967.client:api:* scope.

The default policy doesn't allow everyone to request this scope. It allows:

  • users with the can_request_admin attribute set to true in the database
  • users listed in the policy.data.admin_users configuration option

MAS-specific scopes

MAS also has a few scopes that are specific to the MAS implementation.

urn:mas:admin

This scope grants full access to the MAS Admin API.

The default policy doesn't allow everyone to request this scope. It allows:

urn:mas:graphql:*

This scope grants access to the whole MAS Internal GraphQL API. What permission the session has on the API is determined by the entity that the session is authorized as. When authorized as a user (and without the mas:urn:admin scope), this will usually allow querying and mutating the user's own data.

The default policy allows any client and any user to request this scope.

However, as noted in the Internal GraphQL API documentation, access to the Internal GraphQL API from outside of MAS itself is deprecated in favour of the Admin API.

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

templates check

Check the validity of the templates loaded by the config. It compiles the templates and then renders them with different contexts.

$ mas-cli templates check
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"}
...

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?

We ask that everybody who contributes to this project signs off their contributions, as explained below.

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).

In order to have a concrete record that your contribution is intentional and you agree to license it under the same terms as the project's license, we've adopted the same lightweight approach used by the Linux Kernel, Docker, and many other projects: the Developer Certificate of Origin (DCO). This is a simple declaration that you wrote the contribution or otherwise have the right to contribute it to Matrix:

Developer Certificate of Origin
Version 1.1

Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA

Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.

Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I
    have the right to submit it under the open source license
    indicated in the file; or

(b) The contribution is based upon previous work that, to the best
    of my knowledge, is covered under an appropriate open source
    license and I have the right under that license to submit that
    work with modifications, whether created in whole or in part
    by me, under the same open source license (unless I am
    permitted to submit under a different license), as indicated
    in the file; or

(c) The contribution was provided directly to me by some other
    person who certified (a), (b) or (c) and I have not modified
    it.

(d) I understand and agree that this project and the contribution
    are public and that a record of the contribution (including all
    personal information I submit with it, including my sign-off) is
    maintained indefinitely and may be redistributed consistent with
    this project or the open source license(s) involved.

If you agree to this for your contribution, then all that's needed is to include the line in your commit or pull request comment:

Signed-off-by: Your Name <your@email.example.org>

Git allows you to add this signoff automatically when using the -s flag to git commit, which uses the name and email set in your user.name and user.email git configs.

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.

Internal GraphQL API

Note: This API used to be the way for external tools to interact with MAS. However, external usage is now deprecated in favour of the REST based Admin API. External access to this API will be removed in a future release.

MAS uses an internal GraphQL API which is used by the self-service user interface (usually accessible on /account/), for users to manage their own account.

The endpoint for this API can be discovered through the OpenID Connect discovery document, under the org.matrix.matrix-authentication-service.graphql_endpoint key. Though it is usually hosted at https://<mas-host>/graphql.

GraphQL uses a self-describing schema, which means that the API can be explored in tools like the GraphQL Playground. If enabled, MAS hosts an instance of the playground at https://<mas-host>/graphql/playground.

Authorization

There are two ways to authorize a request to the GraphQL API:

  • if you are requesting from the self-service user interface (or the MAS-hosted GraphQL Playground), it will use the session cookies to authorize as the current user. This mode only allows the user to access their own data, and will never provide admin access.
  • else you will need to provide an OAuth 2.0 access token in the Authorization header, with the Bearer scheme.

The access token must have the urn:mas:graphql:* scope to be able to access the GraphQL API. With only this scope, the session will be authorized as the user who owns the access token, and will only be able to access their own data.

To get full access to the GraphQL API, the access token must have the urn:mas:admin scope in addition to the urn:mas:graphql:* scope.

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.