Skip to content

Commit 4adf403

Browse files
committed
Port to the new Synapse module API
1 parent e178353 commit 4adf403

File tree

4 files changed

+163
-56
lines changed

4 files changed

+163
-56
lines changed

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Version 2
2+
3+
This is a major release, which changes a few things.
4+
5+
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`**.
6+
7+
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.
8+
9+
Steps to upgrade:
10+
11+
- ensure you have Synapse v1.46.0+
12+
- install the new module (an updated `shared_secret_authenticator.py` file)
13+
- 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).
14+
- (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.
15+
16+
17+
# Version 1
18+
19+
Initial release.

README.md

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ Shared Secret Authenticator is a password provider module that plugs into your [
44

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

7-
This is useful when you want to manage the state of your Matrix server (and its users) from an external system.
7+
This is useful when you want to:
8+
9+
- use a bridge to another chat network which does double-puppeting and may need to impersonate your users from time to time
10+
- 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))
811

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

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

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

28-
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`.
31+
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`.
2932

3033
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`.
3134

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

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

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

4144
```yaml
42-
password_providers:
43-
- module: "shared_secret_authenticator.SharedSecretAuthenticator"
44-
config:
45-
sharedSecret: "YOUR SHARED SECRET GOES HERE"
45+
modules:
46+
- module: shared_secret_authenticator.SharedSecretAuthProvider
47+
config:
48+
shared_secret: "YOUR SHARED SECRET GOES HERE"
49+
# By default, only login requests of type `com.devture.shared_secret_auth` are supported.
50+
# Below, we explicitly enable support for the old `m.login.password` login type,
51+
# which was used in v1 of matrix-synapse-shared-secret-auth and still widely supported by external software.
52+
# If you don't need such legacy support, consider setting this to `false` or omitting it entirely.
53+
m_login_password_support_enabled: true
4654
```
4755
56+
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).
57+
58+
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).
59+
In **v2** we don't
60+
4861
For additional logging information, you might want to edit Matrix Synapse's `.log.config` file as well, adding a new logger:
4962

5063
```
@@ -71,17 +84,32 @@ import hashlib
7184
import requests
7285
7386
74-
def obtain_access_token(user_id, homeserver_api_url, shared_secret):
87+
def obtain_access_token(full_user_id, homeserver_api_url, shared_secret):
7588
login_api_url = homeserver_api_url + '/_matrix/client/r0/login'
7689
77-
password = hmac.new(shared_secret.encode('utf-8'), user_id.encode('utf-8'), hashlib.sha512).hexdigest()
90+
token = hmac.new(shared_secret.encode('utf-8'), full_user_id.encode('utf-8'), hashlib.sha512).hexdigest()
7891
7992
payload = {
80-
'type': 'm.login.password',
81-
'user': user_id,
82-
'password': password,
93+
'type': 'com.devture.shared_secret_auth',
94+
'identifier': {
95+
'type': 'm.id.user',
96+
'user': full_user_id,
97+
},
98+
'token': token,
8399
}
84100
101+
# If `m_login_password_support_enabled`, you can use `m.login.password`.
102+
# The token goes into the `password` field for this login type, not the `token` field.
103+
#
104+
# payload = {
105+
# 'type': 'm.login.password',
106+
# 'identifier': {
107+
# 'type': 'm.id.user',
108+
# 'user': full_user_id,
109+
# },
110+
# 'password': token,
111+
# }
112+
85113
response = requests.post(login_api_url, data=json.dumps(payload))
86114

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

110-
This module merely provides an alternate way that a user (or rather, some system on behalf of the user) could log in.
138+
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`).
139+
140+
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.
111141

112142

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

115145
Yes.
116146

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

120-
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.
150+
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.
151+
152+
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.
121153

122154

123155
### This feels like an evil backdoor. Why would you do it?

setup.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,20 @@
22

33
setup(
44
name="shared_secret_authenticator",
5-
version="1.0.2",
5+
version="2.0.0",
66
py_modules=['shared_secret_authenticator'],
77
description="Shared Secret Authenticator password provider module for Matrix Synapse",
88
include_package_data=True,
99
zip_safe=True,
10-
install_requires=['Twisted'],
10+
install_requires=['matrix-synapse>=1.46'],
11+
python_requires="~=3.6",
12+
classifiers=[
13+
"Development Status :: 5 - Production/Stable",
14+
"Programming Language :: Python :: 3 :: Only",
15+
"Programming Language :: Python :: 3.6",
16+
"Programming Language :: Python :: 3.7",
17+
"Programming Language :: Python :: 3.8",
18+
"Programming Language :: Python :: 3.9",
19+
],
1120
)
1221

shared_secret_authenticator.py

Lines changed: 86 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# Shared Secret Authenticator module for Matrix Synapse
44
# Copyright (C) 2018 Slavi Pantaleev
55
#
6-
# http://devture.com/
6+
# https://devture.com/
77
#
88
# This program is free software: you can redistribute it and/or modify
99
# it under the terms of the GNU Affero General Public License as
@@ -18,53 +18,100 @@
1818
# You should have received a copy of the GNU Affero General Public License
1919
# along with this program. If not, see <https://www.gnu.org/licenses/>.
2020
#
21+
from typing import Awaitable, Callable, Optional, Tuple
2122

22-
import logging
2323
import hashlib
2424
import hmac
25-
from twisted.internet import defer
25+
import logging
26+
27+
import synapse
28+
from synapse import module_api
2629

2730
logger = logging.getLogger(__name__)
2831

29-
class SharedSecretAuthenticator(object):
32+
class SharedSecretAuthProvider:
33+
def __init__(self, config: dict, api: module_api):
34+
for k in ('shared_secret',):
35+
if k not in config:
36+
raise Error('Required `{0}` configuration key not found'.format(k))
37+
38+
m_login_password_support_enabled = bool(config['m_login_password_support_enabled']) if 'm_login_password_support_enabled' in config else False
39+
40+
self.api = api
41+
self.shared_secret = config['shared_secret']
42+
43+
auth_checkers: Optional[Dict[Tuple[str, Tuple], CHECK_AUTH_CALLBACK]] = {}
44+
auth_checkers[("com.devture.shared_secret_auth", ("token",))] = self.check_com_devture_shared_secret_auth
45+
if m_login_password_support_enabled:
46+
auth_checkers[("m.login.password", ("password",))] = self.check_m_login_password
47+
48+
enabled_login_types = [k[0] for k in auth_checkers]
49+
logger.info('Enabled login types: %s', enabled_login_types)
3050

31-
def __init__(self, config, account_handler):
32-
self.account_handler = account_handler
33-
self.sharedSecret = config['sharedSecret']
51+
api.register_password_auth_provider_callbacks(
52+
auth_checkers=auth_checkers,
53+
)
3454

35-
@defer.inlineCallbacks
36-
def check_password(self, user_id, password):
37-
# The password is supposed to be an HMAC of the user id, keyed with the shared secret.
38-
# It's not really a password in this case.
39-
given_hmac = password.encode('utf-8')
55+
async def check_com_devture_shared_secret_auth(
56+
self,
57+
username: str,
58+
login_type: str,
59+
login_dict: "synapse.module_api.JsonDict",
60+
) -> Optional[
61+
Tuple[
62+
str,
63+
Optional[Callable[["synapse.module_api.LoginResponse"], Awaitable[None]]],
64+
]
65+
]:
66+
if login_type != "com.devture.shared_secret_auth":
67+
return None
68+
return await self._log_in_username_with_token("com.devture.shared_secret_auth", username, login_dict.get("token"))
4069

41-
logger.info('Authenticating user: %s', user_id)
70+
async def check_m_login_password(
71+
self,
72+
username: str,
73+
login_type: str,
74+
login_dict: "synapse.module_api.JsonDict",
75+
) -> Optional[
76+
Tuple[
77+
str,
78+
Optional[Callable[["synapse.module_api.LoginResponse"], Awaitable[None]]],
79+
]
80+
]:
81+
if login_type != "m.login.password":
82+
return None
83+
return await self._log_in_username_with_token("m.login.password", username, login_dict.get("password"))
4284

43-
h = hmac.new(self.sharedSecret.encode('utf-8'), user_id.encode('utf-8'), hashlib.sha512)
85+
async def _log_in_username_with_token(
86+
self,
87+
login_type: str,
88+
username: str,
89+
token: str,
90+
) -> Optional[
91+
Tuple[
92+
str,
93+
Optional[Callable[["synapse.module_api.LoginResponse"], Awaitable[None]]],
94+
]
95+
]:
96+
logger.info('Authenticating user `%s` with login type `%s`', username, login_type)
97+
98+
full_user_id = self.api.get_qualified_user_id(username)
99+
100+
# The password (token) is supposed to be an HMAC of the full user id, keyed with the shared secret.
101+
given_hmac = token.encode('utf-8')
102+
103+
h = hmac.new(self.shared_secret.encode('utf-8'), full_user_id.encode('utf-8'), hashlib.sha512)
44104
computed_hmac = h.hexdigest().encode('utf-8')
45105

46-
try:
47-
is_identical = hmac.compare_digest(computed_hmac, given_hmac)
48-
except AttributeError:
49-
# `hmac.compare_digest` is only available on Python >= 2.7.7
50-
# Fall back to being somewhat insecure on older versions.
51-
is_identical = (computed_hmac == given_hmac)
52-
53-
if not is_identical:
54-
logger.info('Bad hmac value for user: %s', user_id)
55-
defer.returnValue(False)
56-
return
57-
58-
if not (yield self.account_handler.check_user_exists(user_id)):
59-
logger.info('Refusing to authenticate missing user: %s', user_id)
60-
defer.returnValue(False)
61-
return
62-
63-
logger.info('Authenticated user: %s', user_id)
64-
defer.returnValue(True)
65-
66-
@staticmethod
67-
def parse_config(config):
68-
if 'sharedSecret' not in config:
69-
raise Exception('Missing sharedSecret parameter for SharedSecretAuthenticator')
70-
return config
106+
if not hmac.compare_digest(computed_hmac, given_hmac):
107+
logger.info('Bad hmac value for user: %s', full_user_id)
108+
return None
109+
110+
user_info = await self.api.get_userinfo_by_id(full_user_id)
111+
if user_info is None:
112+
logger.info('Refusing to authenticate missing user: %s', full_user_id)
113+
return None
114+
115+
logger.info('Authenticated user: %s', full_user_id)
116+
117+
return full_user_id, None

0 commit comments

Comments
 (0)