Skip to content

Commit

Permalink
Port to the new Synapse module API
Browse files Browse the repository at this point in the history
  • Loading branch information
spantaleev committed Nov 3, 2021
1 parent e178353 commit 4adf403
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 56 deletions.
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Version 2

This is a major release, which changes a few things.

It **requires [Synapse v1.46.0+](https://github.com/matrix-org/synapse/releases/tag/v1.46.0)**, which introduced support for password provider modules (Synapse previously had a `password_providers` configuration key for password providers, but switched to its new `module` system). You **need to add this module to the `modules` configuration key in `homeserver.yaml`**.

We now **recommend that you use a custom login type (`com.devture.shared_secret_auth`)** for Synapse's [`POST /_matrix/client/r0/login` API](https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-login), **instead of the `m.login.password` login type used in version 1**. Using a special login type means that regular password requests (which use the `m.login.password` login type) do not go through this module needlessly. By default, we don't enable support for `m.login.password` requests, but we let you turn on backward compatibility with a `m_login_password_support_enabled` setting.

Steps to upgrade:

- ensure you have Synapse v1.46.0+
- install the new module (an updated `shared_secret_authenticator.py` file)
- update your `homeserver.yaml` Synapse configuration to move the module from the `password_providers` configuration key to the new `modules` configuration key. Some configuration keys have also changed. See the [README](./README.md#configuring).
- (optionally) update your other software to newer versions which send `com.devture.shared_secret_auth` login requests, not `m.login.password`. Once everything has been upgraded, you can remove the `m_login_password_support_enabled` backward compatibility configuration option.


# Version 1

Initial release.
62 changes: 47 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ Shared Secret Authenticator is a password provider module that plugs into your [

The goal is to allow an external system to send a specially-crafted login request to Matrix Synapse and be able to obtain login credentials for any user on the homeserver.

This is useful when you want to manage the state of your Matrix server (and its users) from an external system.
This is useful when you want to:

- use a bridge to another chat network which does double-puppeting and may need to impersonate your users from time to time
- manage the state of your Matrix server (and its users) from an external system (your own custom code or via a tool like [matrix-corporal](https://github.com/devture/matrix-corporal))

Example: you want your external system to auto-join a given user (`@user:example.com`) to some room. To do this, you need `@system:example.com` to invite `@user:example.com` to `!room:example.com` and then for the user to accept the invitation.

Expand All @@ -25,7 +28,7 @@ On [Archlinux](https://www.archlinux.org/), you can install one of these [AUR](h

To install and configure this manually, make sure `shared_secret_authenticator.py` is on the Python path, somewhere where the Matrix Synapse server can find it.

The easiest way is `pip install git+https://github.com/devture/matrix-synapse-shared-secret-auth` but you can also manually download `shared_secret_authenticator.py` from this repo to a path like `/usr/local/lib/python3.7/site-packages/shared_secret_authenticator.py`.
The easiest way is `pip install git+https://github.com/devture/matrix-synapse-shared-secret-auth` but you can also manually download `shared_secret_authenticator.py` from this repo to a path like `/usr/local/lib/python3.8/site-packages/shared_secret_authenticator.py`.

Some distribution packages (such as the Debian packages from `matrix.org`) may use an isolated virtual environment, so you will need to install the library there. Any environments should be referenced in your init system - for example, the `matrix.org` Debian package creates a systemd init file at `/lib/systemd/system/matrix-synapse.service` that executes python from `/opt/venvs/matrix-synapse`.

Expand All @@ -36,15 +39,25 @@ As the name suggests, you need a "shared secret" (between this Matrix Synapse mo

You can generate a secure one with a command like this: `pwgen -s 128 1`.

You then need to edit Matrix Synapse's configuration (`homeserver.yaml` file) and enable the new password provider:
You then need to edit Matrix Synapse's configuration (`homeserver.yaml` file) and enable the module:

```yaml
password_providers:
- module: "shared_secret_authenticator.SharedSecretAuthenticator"
config:
sharedSecret: "YOUR SHARED SECRET GOES HERE"
modules:
- module: shared_secret_authenticator.SharedSecretAuthProvider
config:
shared_secret: "YOUR SHARED SECRET GOES HERE"
# By default, only login requests of type `com.devture.shared_secret_auth` are supported.
# Below, we explicitly enable support for the old `m.login.password` login type,
# which was used in v1 of matrix-synapse-shared-secret-auth and still widely supported by external software.
# If you don't need such legacy support, consider setting this to `false` or omitting it entirely.
m_login_password_support_enabled: true
```
This uses the new **module** API (and `module` configuration key in `homeserver.yaml`), which added support for "password providers" in [Synapse v1.46.0](https://github.com/matrix-org/synapse/releases/tag/v1.46.0) (released on 2021-11-02). If you're running an older version of Synapse or need to use the old `password_providers` API, install an older version of matrix-synapse-sshared-secret-auth (`1.*` or the `v1-stable` branch).

The `m_login_password_support_enabled` configuration key enables support for the [`m.login.password`](https://matrix.org/docs/spec/client_server/r0.6.1#password-based) authentication type (the default that we used in **v1** of matrix-synapse-sshared-secret-auth).
In **v2** we don't

For additional logging information, you might want to edit Matrix Synapse's `.log.config` file as well, adding a new logger:

```
Expand All @@ -71,17 +84,32 @@ import hashlib
import requests
def obtain_access_token(user_id, homeserver_api_url, shared_secret):
def obtain_access_token(full_user_id, homeserver_api_url, shared_secret):
login_api_url = homeserver_api_url + '/_matrix/client/r0/login'
password = hmac.new(shared_secret.encode('utf-8'), user_id.encode('utf-8'), hashlib.sha512).hexdigest()
token = hmac.new(shared_secret.encode('utf-8'), full_user_id.encode('utf-8'), hashlib.sha512).hexdigest()
payload = {
'type': 'm.login.password',
'user': user_id,
'password': password,
'type': 'com.devture.shared_secret_auth',
'identifier': {
'type': 'm.id.user',
'user': full_user_id,
},
'token': token,
}
# If `m_login_password_support_enabled`, you can use `m.login.password`.
# The token goes into the `password` field for this login type, not the `token` field.
#
# payload = {
# 'type': 'm.login.password',
# 'identifier': {
# 'type': 'm.id.user',
# 'user': full_user_id,
# },
# 'password': token,
# }

response = requests.post(login_api_url, data=json.dumps(payload))

return response.json()['access_token']
Expand All @@ -107,17 +135,21 @@ Yes.
This doesn't change the way normal log in happens.
Users would normally be authenticated by Matrix Synapse's database and the password stored in there.

This module merely provides an alternate way that a user (or rather, some system on behalf of the user) could log in.
This module merely provides an alternate way (a new `com.devture.shared_secret_auth` login type) that a user (or rather, some system on behalf of the user) could use to log in. It's completely separate from the other login flows (like `m.login.password`).

If you've enabled the old `m.login.password` login type via the `m_login_password_support_enabled` configuration setting (defaults to `false`, disabled) then this login type also gets handled. All regular password logins pass through this authentication module, and should they fail to complete, continue on their way to Synapse.


### Can this be used in conjunction with other password providers?

Yes.

Matrix Synapse will go through the list of `password_providers` and try each one in turn.
Matrix Synapse will go through the list of password provider modules and try each matching one in turn.
It will stop only when it finds a password provider that successfully authenticates the user.

Because this password provider only does things locally and upon a direct "password" hit and other password providers (like the [HTTP JSON REST Authenticator](https://github.com/kamax-io/matrix-synapse-rest-auth)) may perform additional (and slower) tasks, for performance reasons it's better to put this one first in the `password_providers` list.
Because this password provider only does things locally and upon a direct "password" hit and other password providers (like the [HTTP JSON REST Authenticator](https://github.com/kamax-io/matrix-synapse-rest-auth)) may perform additional (and slower) tasks, for performance reasons it's better to put this one first in the `modules` list.

If you don't require backward compatibility (`m.login.password` support), we also suggest not enabling support for this login type (set `m_login_password_support_enabled` to `false` or skip this configuration option), which will improve performance.


### This feels like an evil backdoor. Why would you do it?
Expand Down
13 changes: 11 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,20 @@

setup(
name="shared_secret_authenticator",
version="1.0.2",
version="2.0.0",
py_modules=['shared_secret_authenticator'],
description="Shared Secret Authenticator password provider module for Matrix Synapse",
include_package_data=True,
zip_safe=True,
install_requires=['Twisted'],
install_requires=['matrix-synapse>=1.46'],
python_requires="~=3.6",
classifiers=[
"Development Status :: 5 - Production/Stable",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
],
)

125 changes: 86 additions & 39 deletions shared_secret_authenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# Shared Secret Authenticator module for Matrix Synapse
# Copyright (C) 2018 Slavi Pantaleev
#
# http://devture.com/
# https://devture.com/
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
Expand All @@ -18,53 +18,100 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
from typing import Awaitable, Callable, Optional, Tuple

import logging
import hashlib
import hmac
from twisted.internet import defer
import logging

import synapse
from synapse import module_api

logger = logging.getLogger(__name__)

class SharedSecretAuthenticator(object):
class SharedSecretAuthProvider:
def __init__(self, config: dict, api: module_api):
for k in ('shared_secret',):
if k not in config:
raise Error('Required `{0}` configuration key not found'.format(k))

m_login_password_support_enabled = bool(config['m_login_password_support_enabled']) if 'm_login_password_support_enabled' in config else False

self.api = api
self.shared_secret = config['shared_secret']

auth_checkers: Optional[Dict[Tuple[str, Tuple], CHECK_AUTH_CALLBACK]] = {}
auth_checkers[("com.devture.shared_secret_auth", ("token",))] = self.check_com_devture_shared_secret_auth
if m_login_password_support_enabled:
auth_checkers[("m.login.password", ("password",))] = self.check_m_login_password

enabled_login_types = [k[0] for k in auth_checkers]
logger.info('Enabled login types: %s', enabled_login_types)

def __init__(self, config, account_handler):
self.account_handler = account_handler
self.sharedSecret = config['sharedSecret']
api.register_password_auth_provider_callbacks(
auth_checkers=auth_checkers,
)

@defer.inlineCallbacks
def check_password(self, user_id, password):
# The password is supposed to be an HMAC of the user id, keyed with the shared secret.
# It's not really a password in this case.
given_hmac = password.encode('utf-8')
async def check_com_devture_shared_secret_auth(
self,
username: str,
login_type: str,
login_dict: "synapse.module_api.JsonDict",
) -> Optional[
Tuple[
str,
Optional[Callable[["synapse.module_api.LoginResponse"], Awaitable[None]]],
]
]:
if login_type != "com.devture.shared_secret_auth":
return None
return await self._log_in_username_with_token("com.devture.shared_secret_auth", username, login_dict.get("token"))

logger.info('Authenticating user: %s', user_id)
async def check_m_login_password(
self,
username: str,
login_type: str,
login_dict: "synapse.module_api.JsonDict",
) -> Optional[
Tuple[
str,
Optional[Callable[["synapse.module_api.LoginResponse"], Awaitable[None]]],
]
]:
if login_type != "m.login.password":
return None
return await self._log_in_username_with_token("m.login.password", username, login_dict.get("password"))

h = hmac.new(self.sharedSecret.encode('utf-8'), user_id.encode('utf-8'), hashlib.sha512)
async def _log_in_username_with_token(
self,
login_type: str,
username: str,
token: str,
) -> Optional[
Tuple[
str,
Optional[Callable[["synapse.module_api.LoginResponse"], Awaitable[None]]],
]
]:
logger.info('Authenticating user `%s` with login type `%s`', username, login_type)

full_user_id = self.api.get_qualified_user_id(username)

# The password (token) is supposed to be an HMAC of the full user id, keyed with the shared secret.
given_hmac = token.encode('utf-8')

h = hmac.new(self.shared_secret.encode('utf-8'), full_user_id.encode('utf-8'), hashlib.sha512)
computed_hmac = h.hexdigest().encode('utf-8')

try:
is_identical = hmac.compare_digest(computed_hmac, given_hmac)
except AttributeError:
# `hmac.compare_digest` is only available on Python >= 2.7.7
# Fall back to being somewhat insecure on older versions.
is_identical = (computed_hmac == given_hmac)

if not is_identical:
logger.info('Bad hmac value for user: %s', user_id)
defer.returnValue(False)
return

if not (yield self.account_handler.check_user_exists(user_id)):
logger.info('Refusing to authenticate missing user: %s', user_id)
defer.returnValue(False)
return

logger.info('Authenticated user: %s', user_id)
defer.returnValue(True)

@staticmethod
def parse_config(config):
if 'sharedSecret' not in config:
raise Exception('Missing sharedSecret parameter for SharedSecretAuthenticator')
return config
if not hmac.compare_digest(computed_hmac, given_hmac):
logger.info('Bad hmac value for user: %s', full_user_id)
return None

user_info = await self.api.get_userinfo_by_id(full_user_id)
if user_info is None:
logger.info('Refusing to authenticate missing user: %s', full_user_id)
return None

logger.info('Authenticated user: %s', full_user_id)

return full_user_id, None

0 comments on commit 4adf403

Please sign in to comment.