Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 7c9b917

Browse files
authored
Consolidate logic to check for deactivated users. (#15634)
This moves the deactivated user check to the method which all login types call. Additionally updates the application service tests to be more realistic by removing invalid tests and fixing server names.
1 parent 1df0221 commit 7c9b917

File tree

7 files changed

+55
-67
lines changed

7 files changed

+55
-67
lines changed

changelog.d/15634.bugfix

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a long-standing bug where deactivated users were able to login in uncommon situations.

docs/modules/password_auth_provider_callbacks.md

+3
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ instead.
4646

4747
If the authentication is unsuccessful, the module must return `None`.
4848

49+
Note that the user is not automatically registered, the `register_user(..)` method of
50+
the [module API](writing_a_module.html) can be used to lazily create users.
51+
4952
If multiple modules register an auth checker for the same login type but with different
5053
fields, Synapse will refuse to start.
5154

synapse/appservice/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ def __init__(
8686
url.rstrip("/") if isinstance(url, str) else None
8787
) # url must not end with a slash
8888
self.hs_token = hs_token
89+
# The full Matrix ID for this application service's sender.
8990
self.sender = sender
9091
self.namespaces = self._check_namespaces(namespaces)
9192
self.id = id
@@ -212,7 +213,7 @@ def is_interested_in_user(
212213
True if the application service is interested in the user, False if not.
213214
"""
214215
return (
215-
# User is the appservice's sender_localpart user
216+
# User is the appservice's configured sender_localpart user
216217
user_id == self.sender
217218
# User is in the appservice's user namespace
218219
or self.is_user_in_namespace(user_id)

synapse/handlers/auth.py

+5-9
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@
5252
NotFoundError,
5353
StoreError,
5454
SynapseError,
55-
UserDeactivatedError,
5655
)
5756
from synapse.api.ratelimiting import Ratelimiter
5857
from synapse.handlers.ui_auth import (
@@ -1419,12 +1418,6 @@ async def _check_local_password(self, user_id: str, password: str) -> Optional[s
14191418
return None
14201419
(user_id, password_hash) = lookupres
14211420

1422-
# If the password hash is None, the account has likely been deactivated
1423-
if not password_hash:
1424-
deactivated = await self.store.get_user_deactivated_status(user_id)
1425-
if deactivated:
1426-
raise UserDeactivatedError("This account has been deactivated")
1427-
14281421
result = await self.validate_hash(password, password_hash)
14291422
if not result:
14301423
logger.warning("Failed password login for user %s", user_id)
@@ -1749,8 +1742,11 @@ async def complete_sso_login(
17491742
registered.
17501743
auth_provider_session_id: The session ID from the SSO IdP received during login.
17511744
"""
1752-
# If the account has been deactivated, do not proceed with the login
1753-
# flow.
1745+
# If the account has been deactivated, do not proceed with the login.
1746+
#
1747+
# This gets checked again when the token is submitted but this lets us
1748+
# provide an HTML error page to the user (instead of issuing a token and
1749+
# having it error later).
17541750
deactivated = await self.store.get_user_deactivated_status(registered_user_id)
17551751
if deactivated:
17561752
respond_with_html(request, 403, self._sso_account_deactivated_template)

synapse/handlers/jwt.py

+3-16
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from authlib.jose import JsonWebToken, JWTClaims
1717
from authlib.jose.errors import BadSignatureError, InvalidClaimError, JoseError
1818

19-
from synapse.api.errors import Codes, LoginError, StoreError, UserDeactivatedError
19+
from synapse.api.errors import Codes, LoginError
2020
from synapse.types import JsonDict, UserID
2121

2222
if TYPE_CHECKING:
@@ -26,15 +26,14 @@
2626
class JwtHandler:
2727
def __init__(self, hs: "HomeServer"):
2828
self.hs = hs
29-
self._main_store = hs.get_datastores().main
3029

3130
self.jwt_secret = hs.config.jwt.jwt_secret
3231
self.jwt_subject_claim = hs.config.jwt.jwt_subject_claim
3332
self.jwt_algorithm = hs.config.jwt.jwt_algorithm
3433
self.jwt_issuer = hs.config.jwt.jwt_issuer
3534
self.jwt_audiences = hs.config.jwt.jwt_audiences
3635

37-
async def validate_login(self, login_submission: JsonDict) -> str:
36+
def validate_login(self, login_submission: JsonDict) -> str:
3837
"""
3938
Authenticates the user for the /login API
4039
@@ -103,16 +102,4 @@ async def validate_login(self, login_submission: JsonDict) -> str:
103102
if user is None:
104103
raise LoginError(403, "Invalid JWT", errcode=Codes.FORBIDDEN)
105104

106-
user_id = UserID(user, self.hs.hostname).to_string()
107-
108-
# If the account has been deactivated, do not proceed with the login
109-
# flow.
110-
try:
111-
deactivated = await self._main_store.get_user_deactivated_status(user_id)
112-
except StoreError:
113-
# JWT lazily creates users, so they may not exist in the database yet.
114-
deactivated = False
115-
if deactivated:
116-
raise UserDeactivatedError("This account has been deactivated")
117-
118-
return user_id
105+
return UserID(user, self.hs.hostname).to_string()

synapse/rest/client/login.py

+20-3
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
LoginError,
3636
NotApprovedError,
3737
SynapseError,
38+
UserDeactivatedError,
3839
)
3940
from synapse.api.ratelimiting import Ratelimiter
4041
from synapse.api.urls import CLIENT_API_PREFIX
@@ -84,6 +85,7 @@ class LoginRestServlet(RestServlet):
8485
def __init__(self, hs: "HomeServer"):
8586
super().__init__()
8687
self.hs = hs
88+
self._main_store = hs.get_datastores().main
8789

8890
# JWT configuration variables.
8991
self.jwt_enabled = hs.config.jwt.jwt_enabled
@@ -112,13 +114,13 @@ def __init__(self, hs: "HomeServer"):
112114

113115
self._well_known_builder = WellKnownBuilder(hs)
114116
self._address_ratelimiter = Ratelimiter(
115-
store=hs.get_datastores().main,
117+
store=self._main_store,
116118
clock=hs.get_clock(),
117119
rate_hz=self.hs.config.ratelimiting.rc_login_address.per_second,
118120
burst_count=self.hs.config.ratelimiting.rc_login_address.burst_count,
119121
)
120122
self._account_ratelimiter = Ratelimiter(
121-
store=hs.get_datastores().main,
123+
store=self._main_store,
122124
clock=hs.get_clock(),
123125
rate_hz=self.hs.config.ratelimiting.rc_login_account.per_second,
124126
burst_count=self.hs.config.ratelimiting.rc_login_account.burst_count,
@@ -280,6 +282,9 @@ async def _do_appservice_login(
280282
login_submission,
281283
ratelimit=appservice.is_rate_limited(),
282284
should_issue_refresh_token=should_issue_refresh_token,
285+
# The user represented by an appservice's configured sender_localpart
286+
# is not actually created in Synapse.
287+
should_check_deactivated=qualified_user_id != appservice.sender,
283288
)
284289

285290
async def _do_other_login(
@@ -326,6 +331,7 @@ async def _complete_login(
326331
auth_provider_id: Optional[str] = None,
327332
should_issue_refresh_token: bool = False,
328333
auth_provider_session_id: Optional[str] = None,
334+
should_check_deactivated: bool = True,
329335
) -> LoginResponse:
330336
"""Called when we've successfully authed the user and now need to
331337
actually login them in (e.g. create devices). This gets called on
@@ -345,6 +351,11 @@ async def _complete_login(
345351
should_issue_refresh_token: True if this login should issue
346352
a refresh token alongside the access token.
347353
auth_provider_session_id: The session ID got during login from the SSO IdP.
354+
should_check_deactivated: True if the user should be checked for
355+
deactivation status before logging in.
356+
357+
This exists purely for appservice's configured sender_localpart
358+
which doesn't have an associated user in the database.
348359
349360
Returns:
350361
Dictionary of account information after successful login.
@@ -364,6 +375,12 @@ async def _complete_login(
364375
)
365376
user_id = canonical_uid
366377

378+
# If the account has been deactivated, do not proceed with the login.
379+
if should_check_deactivated:
380+
deactivated = await self._main_store.get_user_deactivated_status(user_id)
381+
if deactivated:
382+
raise UserDeactivatedError("This account has been deactivated")
383+
367384
device_id = login_submission.get("device_id")
368385

369386
# If device_id is present, check that device_id is not longer than a reasonable 512 characters
@@ -458,7 +475,7 @@ async def _do_jwt_login(
458475
Returns:
459476
The body of the JSON response.
460477
"""
461-
user_id = await self.hs.get_jwt_handler().validate_login(login_submission)
478+
user_id = self.hs.get_jwt_handler().validate_login(login_submission)
462479
return await self._complete_login(
463480
user_id,
464481
login_submission,

tests/handlers/test_password_providers.py

+21-38
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,17 @@
1818
from typing import Any, Dict, List, Optional, Type, Union
1919
from unittest.mock import Mock
2020

21+
from twisted.test.proto_helpers import MemoryReactor
22+
2123
import synapse
2224
from synapse.api.constants import LoginType
2325
from synapse.api.errors import Codes
2426
from synapse.handlers.account import AccountHandler
2527
from synapse.module_api import ModuleApi
2628
from synapse.rest.client import account, devices, login, logout, register
29+
from synapse.server import HomeServer
2730
from synapse.types import JsonDict, UserID
31+
from synapse.util import Clock
2832

2933
from tests import unittest
3034
from tests.server import FakeChannel
@@ -162,10 +166,16 @@ class PasswordAuthProviderTests(unittest.HomeserverTestCase):
162166
CALLBACK_USERNAME = "get_username_for_registration"
163167
CALLBACK_DISPLAYNAME = "get_displayname_for_registration"
164168

165-
def setUp(self) -> None:
169+
def prepare(
170+
self, reactor: MemoryReactor, clock: Clock, homeserver: HomeServer
171+
) -> None:
166172
# we use a global mock device, so make sure we are starting with a clean slate
167173
mock_password_provider.reset_mock()
168-
super().setUp()
174+
175+
# The mock password provider doesn't register the users, so ensure they
176+
# are registered first.
177+
self.register_user("u", "not-the-tested-password")
178+
self.register_user("user", "not-the-tested-password")
169179

170180
@override_config(legacy_providers_config(LegacyPasswordOnlyAuthProvider))
171181
def test_password_only_auth_progiver_login_legacy(self) -> None:
@@ -185,33 +195,19 @@ def password_only_auth_provider_login_test_body(self) -> None:
185195
mock_password_provider.reset_mock()
186196

187197
# login with mxid should work too
188-
channel = self._send_password_login("@u:bz", "p")
198+
channel = self._send_password_login("@u:test", "p")
189199
self.assertEqual(channel.code, HTTPStatus.OK, channel.result)
190-
self.assertEqual("@u:bz", channel.json_body["user_id"])
191-
mock_password_provider.check_password.assert_called_once_with("@u:bz", "p")
200+
self.assertEqual("@u:test", channel.json_body["user_id"])
201+
mock_password_provider.check_password.assert_called_once_with("@u:test", "p")
192202
mock_password_provider.reset_mock()
193203

194-
# try a weird username / pass. Honestly it's unclear what we *expect* to happen
195-
# in these cases, but at least we can guard against the API changing
196-
# unexpectedly
197-
channel = self._send_password_login(" USER🙂NAME ", " pASS\U0001F622word ")
198-
self.assertEqual(channel.code, HTTPStatus.OK, channel.result)
199-
self.assertEqual("@ USER🙂NAME :test", channel.json_body["user_id"])
200-
mock_password_provider.check_password.assert_called_once_with(
201-
"@ USER🙂NAME :test", " pASS😢word "
202-
)
203-
204204
@override_config(legacy_providers_config(LegacyPasswordOnlyAuthProvider))
205205
def test_password_only_auth_provider_ui_auth_legacy(self) -> None:
206206
self.password_only_auth_provider_ui_auth_test_body()
207207

208208
def password_only_auth_provider_ui_auth_test_body(self) -> None:
209209
"""UI Auth should delegate correctly to the password provider"""
210210

211-
# create the user, otherwise access doesn't work
212-
module_api = self.hs.get_module_api()
213-
self.get_success(module_api.register_user("u"))
214-
215211
# log in twice, to get two devices
216212
mock_password_provider.check_password.return_value = make_awaitable(True)
217213
tok1 = self.login("u", "p")
@@ -401,29 +397,16 @@ def custom_auth_provider_login_test_body(self) -> None:
401397
mock_password_provider.check_auth.assert_not_called()
402398

403399
mock_password_provider.check_auth.return_value = make_awaitable(
404-
("@user:bz", None)
400+
("@user:test", None)
405401
)
406402
channel = self._send_login("test.login_type", "u", test_field="y")
407403
self.assertEqual(channel.code, HTTPStatus.OK, channel.result)
408-
self.assertEqual("@user:bz", channel.json_body["user_id"])
404+
self.assertEqual("@user:test", channel.json_body["user_id"])
409405
mock_password_provider.check_auth.assert_called_once_with(
410406
"u", "test.login_type", {"test_field": "y"}
411407
)
412408
mock_password_provider.reset_mock()
413409

414-
# try a weird username. Again, it's unclear what we *expect* to happen
415-
# in these cases, but at least we can guard against the API changing
416-
# unexpectedly
417-
mock_password_provider.check_auth.return_value = make_awaitable(
418-
("@ MALFORMED! :bz", None)
419-
)
420-
channel = self._send_login("test.login_type", " USER🙂NAME ", test_field=" abc ")
421-
self.assertEqual(channel.code, HTTPStatus.OK, channel.result)
422-
self.assertEqual("@ MALFORMED! :bz", channel.json_body["user_id"])
423-
mock_password_provider.check_auth.assert_called_once_with(
424-
" USER🙂NAME ", "test.login_type", {"test_field": " abc "}
425-
)
426-
427410
@override_config(legacy_providers_config(LegacyCustomAuthProvider))
428411
def test_custom_auth_provider_ui_auth_legacy(self) -> None:
429412
self.custom_auth_provider_ui_auth_test_body()
@@ -465,7 +448,7 @@ def custom_auth_provider_ui_auth_test_body(self) -> None:
465448

466449
# right params, but authing as the wrong user
467450
mock_password_provider.check_auth.return_value = make_awaitable(
468-
("@user:bz", None)
451+
("@user:test", None)
469452
)
470453
body["auth"]["test_field"] = "foo"
471454
channel = self._delete_device(tok1, "dev2", body)
@@ -498,11 +481,11 @@ def custom_auth_provider_callback_test_body(self) -> None:
498481
callback = Mock(return_value=make_awaitable(None))
499482

500483
mock_password_provider.check_auth.return_value = make_awaitable(
501-
("@user:bz", callback)
484+
("@user:test", callback)
502485
)
503486
channel = self._send_login("test.login_type", "u", test_field="y")
504487
self.assertEqual(channel.code, HTTPStatus.OK, channel.result)
505-
self.assertEqual("@user:bz", channel.json_body["user_id"])
488+
self.assertEqual("@user:test", channel.json_body["user_id"])
506489
mock_password_provider.check_auth.assert_called_once_with(
507490
"u", "test.login_type", {"test_field": "y"}
508491
)
@@ -512,7 +495,7 @@ def custom_auth_provider_callback_test_body(self) -> None:
512495
call_args, call_kwargs = callback.call_args
513496
# should be one positional arg
514497
self.assertEqual(len(call_args), 1)
515-
self.assertEqual(call_args[0]["user_id"], "@user:bz")
498+
self.assertEqual(call_args[0]["user_id"], "@user:test")
516499
for p in ["user_id", "access_token", "device_id", "home_server"]:
517500
self.assertIn(p, call_args[0])
518501

0 commit comments

Comments
 (0)