Compare commits

...

4 Commits

Author SHA1 Message Date
Richard van der Hoff 4d9496559d
Support "identifier" dicts in UIA (#8848)
The spec requires synapse to support `identifier` dicts for `m.login.password`
user-interactive auth, which it did not (instead, it required an undocumented
`user` parameter.)

To fix this properly, we need to pull the code that interprets `identifier`
into `AuthHandler.validate_login` so that it can be called from the UIA code.

Fixes #5665.
2020-12-01 17:42:26 +00:00
Richard van der Hoff 9edff901d1
Add missing `ordering` to background updates (#8850)
It's important that we make sure our background updates happen in a defined
order, to avoid disasters like #6923.

Add an ordering to all of the background updates that have landed since #7190.
2020-12-01 15:52:49 +00:00
Nicolas Chamo 3f0cba657c
Allow Date header through CORS (#8804) 2020-12-01 13:24:56 +00:00
Richard van der Hoff 89f7930730
Don't offer password login when it is disabled (#8835)
Fix a minor bug where we would offer "m.login.password" login if a custom auth provider supported it, even if password login was disabled.
2020-12-01 13:04:03 +00:00
13 changed files with 318 additions and 162 deletions

1
changelog.d/8804.feature Normal file
View File

@ -0,0 +1 @@
Allow Date header through CORS. Contributed by Nicolas Chamo.

1
changelog.d/8835.bugfix Normal file
View File

@ -0,0 +1 @@
Fix minor long-standing bug in login, where we would offer the `password` login type if a custom auth provider supported it, even if password login was disabled.

1
changelog.d/8848.bugfix Normal file
View File

@ -0,0 +1 @@
Fix a long-standing bug which caused Synapse to require unspecified parameters during user-interactive authentication.

1
changelog.d/8850.misc Normal file
View File

@ -0,0 +1 @@
Add missing `ordering` to background database updates.

View File

@ -205,15 +205,23 @@ class AuthHandler(BaseHandler):
# type in the list. (NB that the spec doesn't require us to do so and # type in the list. (NB that the spec doesn't require us to do so and
# clients which favour types that they don't understand over those that # clients which favour types that they don't understand over those that
# they do are technically broken) # they do are technically broken)
# start out by assuming PASSWORD is enabled; we will remove it later if not.
login_types = [] login_types = []
if self._password_enabled: if hs.config.password_localdb_enabled:
login_types.append(LoginType.PASSWORD) login_types.append(LoginType.PASSWORD)
for provider in self.password_providers: for provider in self.password_providers:
if hasattr(provider, "get_supported_login_types"): if hasattr(provider, "get_supported_login_types"):
for t in provider.get_supported_login_types().keys(): for t in provider.get_supported_login_types().keys():
if t not in login_types: if t not in login_types:
login_types.append(t) login_types.append(t)
if not self._password_enabled:
login_types.remove(LoginType.PASSWORD)
self._supported_login_types = login_types self._supported_login_types = login_types
# Login types and UI Auth types have a heavy overlap, but are not # Login types and UI Auth types have a heavy overlap, but are not
# necessarily identical. Login types have SSO (and other login types) # necessarily identical. Login types have SSO (and other login types)
# added in the rest layer, see synapse.rest.client.v1.login.LoginRestServerlet.on_GET. # added in the rest layer, see synapse.rest.client.v1.login.LoginRestServerlet.on_GET.
@ -230,6 +238,13 @@ class AuthHandler(BaseHandler):
burst_count=self.hs.config.rc_login_failed_attempts.burst_count, burst_count=self.hs.config.rc_login_failed_attempts.burst_count,
) )
# Ratelimitier for failed /login attempts
self._failed_login_attempts_ratelimiter = Ratelimiter(
clock=hs.get_clock(),
rate_hz=self.hs.config.rc_login_failed_attempts.per_second,
burst_count=self.hs.config.rc_login_failed_attempts.burst_count,
)
self._clock = self.hs.get_clock() self._clock = self.hs.get_clock()
# Expire old UI auth sessions after a period of time. # Expire old UI auth sessions after a period of time.
@ -642,14 +657,8 @@ class AuthHandler(BaseHandler):
res = await checker.check_auth(authdict, clientip=clientip) res = await checker.check_auth(authdict, clientip=clientip)
return res return res
# build a v1-login-style dict out of the authdict and fall back to the # fall back to the v1 login flow
# v1 code canonical_id, _ = await self.validate_login(authdict)
user_id = authdict.get("user")
if user_id is None:
raise SynapseError(400, "", Codes.MISSING_PARAM)
(canonical_id, callback) = await self.validate_login(user_id, authdict)
return canonical_id return canonical_id
def _get_params_recaptcha(self) -> dict: def _get_params_recaptcha(self) -> dict:
@ -824,15 +833,155 @@ class AuthHandler(BaseHandler):
return self._supported_login_types return self._supported_login_types
async def validate_login( async def validate_login(
self, username: str, login_submission: Dict[str, Any] self, login_submission: Dict[str, Any], ratelimit: bool = False,
) -> Tuple[str, Optional[Callable[[Dict[str, str]], None]]]: ) -> Tuple[str, Optional[Callable[[Dict[str, str]], None]]]:
"""Authenticates the user for the /login API """Authenticates the user for the /login API
Also used by the user-interactive auth flow to validate Also used by the user-interactive auth flow to validate auth types which don't
m.login.password auth types. have an explicit UIA handler, including m.password.auth.
Args: Args:
username: username supplied by the user login_submission: the whole of the login submission
(including 'type' and other relevant fields)
ratelimit: whether to apply the failed_login_attempt ratelimiter
Returns:
A tuple of the canonical user id, and optional callback
to be called once the access token and device id are issued
Raises:
StoreError if there was a problem accessing the database
SynapseError if there was a problem with the request
LoginError if there was an authentication problem.
"""
login_type = login_submission.get("type")
# ideally, we wouldn't be checking the identifier unless we know we have a login
# method which uses it (https://github.com/matrix-org/synapse/issues/8836)
#
# But the auth providers' check_auth interface requires a username, so in
# practice we can only support login methods which we can map to a username
# anyway.
# special case to check for "password" for the check_password interface
# for the auth providers
password = login_submission.get("password")
if login_type == LoginType.PASSWORD:
if not self._password_enabled:
raise SynapseError(400, "Password login has been disabled.")
if not isinstance(password, str):
raise SynapseError(400, "Bad parameter: password", Codes.INVALID_PARAM)
# map old-school login fields into new-school "identifier" fields.
identifier_dict = convert_client_dict_legacy_fields_to_identifier(
login_submission
)
# convert phone type identifiers to generic threepids
if identifier_dict["type"] == "m.id.phone":
identifier_dict = login_id_phone_to_thirdparty(identifier_dict)
# convert threepid identifiers to user IDs
if identifier_dict["type"] == "m.id.thirdparty":
address = identifier_dict.get("address")
medium = identifier_dict.get("medium")
if medium is None or address is None:
raise SynapseError(400, "Invalid thirdparty identifier")
# For emails, canonicalise the address.
# We store all email addresses canonicalised in the DB.
# (See add_threepid in synapse/handlers/auth.py)
if medium == "email":
try:
address = canonicalise_email(address)
except ValueError as e:
raise SynapseError(400, str(e))
# We also apply account rate limiting using the 3PID as a key, as
# otherwise using 3PID bypasses the ratelimiting based on user ID.
if ratelimit:
self._failed_login_attempts_ratelimiter.ratelimit(
(medium, address), update=False
)
# Check for login providers that support 3pid login types
if login_type == LoginType.PASSWORD:
# we've already checked that there is a (valid) password field
assert isinstance(password, str)
(
canonical_user_id,
callback_3pid,
) = await self.check_password_provider_3pid(medium, address, password)
if canonical_user_id:
# Authentication through password provider and 3pid succeeded
return canonical_user_id, callback_3pid
# No password providers were able to handle this 3pid
# Check local store
user_id = await self.hs.get_datastore().get_user_id_by_threepid(
medium, address
)
if not user_id:
logger.warning(
"unknown 3pid identifier medium %s, address %r", medium, address
)
# We mark that we've failed to log in here, as
# `check_password_provider_3pid` might have returned `None` due
# to an incorrect password, rather than the account not
# existing.
#
# If it returned None but the 3PID was bound then we won't hit
# this code path, which is fine as then the per-user ratelimit
# will kick in below.
if ratelimit:
self._failed_login_attempts_ratelimiter.can_do_action(
(medium, address)
)
raise LoginError(403, "", errcode=Codes.FORBIDDEN)
identifier_dict = {"type": "m.id.user", "user": user_id}
# by this point, the identifier should be an m.id.user: if it's anything
# else, we haven't understood it.
if identifier_dict["type"] != "m.id.user":
raise SynapseError(400, "Unknown login identifier type")
username = identifier_dict.get("user")
if not username:
raise SynapseError(400, "User identifier is missing 'user' key")
if username.startswith("@"):
qualified_user_id = username
else:
qualified_user_id = UserID(username, self.hs.hostname).to_string()
# Check if we've hit the failed ratelimit (but don't update it)
if ratelimit:
self._failed_login_attempts_ratelimiter.ratelimit(
qualified_user_id.lower(), update=False
)
try:
return await self._validate_userid_login(username, login_submission)
except LoginError:
# The user has failed to log in, so we need to update the rate
# limiter. Using `can_do_action` avoids us raising a ratelimit
# exception and masking the LoginError. The actual ratelimiting
# should have happened above.
if ratelimit:
self._failed_login_attempts_ratelimiter.can_do_action(
qualified_user_id.lower()
)
raise
async def _validate_userid_login(
self, username: str, login_submission: Dict[str, Any],
) -> Tuple[str, Optional[Callable[[Dict[str, str]], None]]]:
"""Helper for validate_login
Handles login, once we've mapped 3pids onto userids
Args:
username: the username, from the identifier dict
login_submission: the whole of the login submission login_submission: the whole of the login submission
(including 'type' and other relevant fields) (including 'type' and other relevant fields)
Returns: Returns:
@ -843,7 +992,6 @@ class AuthHandler(BaseHandler):
SynapseError if there was a problem with the request SynapseError if there was a problem with the request
LoginError if there was an authentication problem. LoginError if there was an authentication problem.
""" """
if username.startswith("@"): if username.startswith("@"):
qualified_user_id = username qualified_user_id = username
else: else:
@ -852,20 +1000,13 @@ class AuthHandler(BaseHandler):
login_type = login_submission.get("type") login_type = login_submission.get("type")
known_login_type = False known_login_type = False
# special case to check for "password" for the check_password interface
# for the auth providers
password = login_submission.get("password")
if login_type == LoginType.PASSWORD:
if not self._password_enabled:
raise SynapseError(400, "Password login has been disabled.")
if not password:
raise SynapseError(400, "Missing parameter: password")
for provider in self.password_providers: for provider in self.password_providers:
if hasattr(provider, "check_password") and login_type == LoginType.PASSWORD: if hasattr(provider, "check_password") and login_type == LoginType.PASSWORD:
known_login_type = True known_login_type = True
is_valid = await provider.check_password(qualified_user_id, password) # we've already checked that there is a (valid) password field
is_valid = await provider.check_password(
qualified_user_id, login_submission["password"]
)
if is_valid: if is_valid:
return qualified_user_id, None return qualified_user_id, None
@ -906,8 +1047,12 @@ class AuthHandler(BaseHandler):
if login_type == LoginType.PASSWORD and self.hs.config.password_localdb_enabled: if login_type == LoginType.PASSWORD and self.hs.config.password_localdb_enabled:
known_login_type = True known_login_type = True
# we've already checked that there is a (valid) password field
password = login_submission["password"]
assert isinstance(password, str)
canonical_user_id = await self._check_local_password( canonical_user_id = await self._check_local_password(
qualified_user_id, password # type: ignore qualified_user_id, password
) )
if canonical_user_id: if canonical_user_id:

View File

@ -674,7 +674,7 @@ def set_cors_headers(request: Request):
) )
request.setHeader( request.setHeader(
b"Access-Control-Allow-Headers", b"Access-Control-Allow-Headers",
b"Origin, X-Requested-With, Content-Type, Accept, Authorization", b"Origin, X-Requested-With, Content-Type, Accept, Authorization, Date",
) )

View File

@ -19,10 +19,6 @@ from typing import Awaitable, Callable, Dict, Optional
from synapse.api.errors import Codes, LoginError, SynapseError from synapse.api.errors import Codes, LoginError, SynapseError
from synapse.api.ratelimiting import Ratelimiter from synapse.api.ratelimiting import Ratelimiter
from synapse.appservice import ApplicationService from synapse.appservice import ApplicationService
from synapse.handlers.auth import (
convert_client_dict_legacy_fields_to_identifier,
login_id_phone_to_thirdparty,
)
from synapse.http.server import finish_request from synapse.http.server import finish_request
from synapse.http.servlet import ( from synapse.http.servlet import (
RestServlet, RestServlet,
@ -33,7 +29,6 @@ from synapse.http.site import SynapseRequest
from synapse.rest.client.v2_alpha._base import client_patterns from synapse.rest.client.v2_alpha._base import client_patterns
from synapse.rest.well_known import WellKnownBuilder from synapse.rest.well_known import WellKnownBuilder
from synapse.types import JsonDict, UserID from synapse.types import JsonDict, UserID
from synapse.util.threepids import canonicalise_email
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -78,11 +73,6 @@ class LoginRestServlet(RestServlet):
rate_hz=self.hs.config.rc_login_account.per_second, rate_hz=self.hs.config.rc_login_account.per_second,
burst_count=self.hs.config.rc_login_account.burst_count, burst_count=self.hs.config.rc_login_account.burst_count,
) )
self._failed_attempts_ratelimiter = Ratelimiter(
clock=hs.get_clock(),
rate_hz=self.hs.config.rc_login_failed_attempts.per_second,
burst_count=self.hs.config.rc_login_failed_attempts.burst_count,
)
def on_GET(self, request: SynapseRequest): def on_GET(self, request: SynapseRequest):
flows = [] flows = []
@ -140,17 +130,6 @@ class LoginRestServlet(RestServlet):
result["well_known"] = well_known_data result["well_known"] = well_known_data
return 200, result return 200, result
def _get_qualified_user_id(self, identifier):
if identifier["type"] != "m.id.user":
raise SynapseError(400, "Unknown login identifier type")
if "user" not in identifier:
raise SynapseError(400, "User identifier is missing 'user' key")
if identifier["user"].startswith("@"):
return identifier["user"]
else:
return UserID(identifier["user"], self.hs.hostname).to_string()
async def _do_appservice_login( async def _do_appservice_login(
self, login_submission: JsonDict, appservice: ApplicationService self, login_submission: JsonDict, appservice: ApplicationService
): ):
@ -201,91 +180,9 @@ class LoginRestServlet(RestServlet):
login_submission.get("address"), login_submission.get("address"),
login_submission.get("user"), login_submission.get("user"),
) )
identifier = convert_client_dict_legacy_fields_to_identifier(login_submission)
# convert phone type identifiers to generic threepids
if identifier["type"] == "m.id.phone":
identifier = login_id_phone_to_thirdparty(identifier)
# convert threepid identifiers to user IDs
if identifier["type"] == "m.id.thirdparty":
address = identifier.get("address")
medium = identifier.get("medium")
if medium is None or address is None:
raise SynapseError(400, "Invalid thirdparty identifier")
# For emails, canonicalise the address.
# We store all email addresses canonicalised in the DB.
# (See add_threepid in synapse/handlers/auth.py)
if medium == "email":
try:
address = canonicalise_email(address)
except ValueError as e:
raise SynapseError(400, str(e))
# We also apply account rate limiting using the 3PID as a key, as
# otherwise using 3PID bypasses the ratelimiting based on user ID.
self._failed_attempts_ratelimiter.ratelimit((medium, address), update=False)
# Check for login providers that support 3pid login types
(
canonical_user_id,
callback_3pid,
) = await self.auth_handler.check_password_provider_3pid(
medium, address, login_submission["password"]
)
if canonical_user_id:
# Authentication through password provider and 3pid succeeded
result = await self._complete_login(
canonical_user_id, login_submission, callback_3pid
)
return result
# No password providers were able to handle this 3pid
# Check local store
user_id = await self.hs.get_datastore().get_user_id_by_threepid(
medium, address
)
if not user_id:
logger.warning(
"unknown 3pid identifier medium %s, address %r", medium, address
)
# We mark that we've failed to log in here, as
# `check_password_provider_3pid` might have returned `None` due
# to an incorrect password, rather than the account not
# existing.
#
# If it returned None but the 3PID was bound then we won't hit
# this code path, which is fine as then the per-user ratelimit
# will kick in below.
self._failed_attempts_ratelimiter.can_do_action((medium, address))
raise LoginError(403, "", errcode=Codes.FORBIDDEN)
identifier = {"type": "m.id.user", "user": user_id}
# by this point, the identifier should be an m.id.user: if it's anything
# else, we haven't understood it.
qualified_user_id = self._get_qualified_user_id(identifier)
# Check if we've hit the failed ratelimit (but don't update it)
self._failed_attempts_ratelimiter.ratelimit(
qualified_user_id.lower(), update=False
)
try:
canonical_user_id, callback = await self.auth_handler.validate_login( canonical_user_id, callback = await self.auth_handler.validate_login(
identifier["user"], login_submission login_submission, ratelimit=True
) )
except LoginError:
# The user has failed to log in, so we need to update the rate
# limiter. Using `can_do_action` avoids us raising a ratelimit
# exception and masking the LoginError. The actual ratelimiting
# should have happened above.
self._failed_attempts_ratelimiter.can_do_action(qualified_user_id.lower())
raise
result = await self._complete_login( result = await self._complete_login(
canonical_user_id, login_submission, callback canonical_user_id, login_submission, callback
) )

View File

@ -20,14 +20,14 @@
*/ */
-- add new index that includes method to local media -- add new index that includes method to local media
INSERT INTO background_updates (update_name, progress_json) VALUES INSERT INTO background_updates (ordering, update_name, progress_json) VALUES
('local_media_repository_thumbnails_method_idx', '{}'); (5807, 'local_media_repository_thumbnails_method_idx', '{}');
-- add new index that includes method to remote media -- add new index that includes method to remote media
INSERT INTO background_updates (update_name, progress_json, depends_on) VALUES INSERT INTO background_updates (ordering, update_name, progress_json, depends_on) VALUES
('remote_media_repository_thumbnails_method_idx', '{}', 'local_media_repository_thumbnails_method_idx'); (5807, 'remote_media_repository_thumbnails_method_idx', '{}', 'local_media_repository_thumbnails_method_idx');
-- drop old index -- drop old index
INSERT INTO background_updates (update_name, progress_json, depends_on) VALUES INSERT INTO background_updates (ordering, update_name, progress_json, depends_on) VALUES
('media_repository_drop_index_wo_method', '{}', 'remote_media_repository_thumbnails_method_idx'); (5807, 'media_repository_drop_index_wo_method', '{}', 'remote_media_repository_thumbnails_method_idx');

View File

@ -28,5 +28,5 @@
-- functionality as the old one. This effectively restarts the background job -- functionality as the old one. This effectively restarts the background job
-- from the beginning, without running it twice in a row, supporting both -- from the beginning, without running it twice in a row, supporting both
-- upgrade usecases. -- upgrade usecases.
INSERT INTO background_updates (update_name, progress_json) VALUES INSERT INTO background_updates (ordering, update_name, progress_json) VALUES
('populate_stats_process_rooms_2', '{}'); (5812, 'populate_stats_process_rooms_2', '{}');

View File

@ -1,2 +1,2 @@
INSERT INTO background_updates (update_name, progress_json) VALUES INSERT INTO background_updates (ordering, update_name, progress_json) VALUES
('users_have_local_media', '{}'); (5822, 'users_have_local_media', '{}');

View File

@ -13,5 +13,5 @@
* limitations under the License. * limitations under the License.
*/ */
INSERT INTO background_updates (update_name, progress_json) VALUES INSERT INTO background_updates (ordering, update_name, progress_json) VALUES
('e2e_cross_signing_keys_idx', '{}'); (5823, 'e2e_cross_signing_keys_idx', '{}');

View File

@ -70,6 +70,24 @@ class CustomAuthProvider:
return mock_password_provider.check_auth(*args) return mock_password_provider.check_auth(*args)
class PasswordCustomAuthProvider:
"""A password_provider which implements password login via `check_auth`, as well
as a custom type."""
@staticmethod
def parse_config(self):
pass
def __init__(self, config, account_handler):
pass
def get_supported_login_types(self):
return {"m.login.password": ["password"], "test.login_type": ["test_field"]}
def check_auth(self, *args):
return mock_password_provider.check_auth(*args)
def providers_config(*providers: Type[Any]) -> dict: def providers_config(*providers: Type[Any]) -> dict:
"""Returns a config dict that will enable the given password auth providers""" """Returns a config dict that will enable the given password auth providers"""
return { return {
@ -246,7 +264,11 @@ class PasswordAuthProviderTests(unittest.HomeserverTestCase):
mock_password_provider.check_password.reset_mock() mock_password_provider.check_password.reset_mock()
# first delete should give a 401 # first delete should give a 401
session = self._start_delete_device_session(tok1, "dev2") channel = self._delete_device(tok1, "dev2")
self.assertEqual(channel.code, 401)
# there are no valid flows here!
self.assertEqual(channel.json_body["flows"], [])
session = channel.json_body["session"]
mock_password_provider.check_password.assert_not_called() mock_password_provider.check_password.assert_not_called()
# now try deleting with the local password # now try deleting with the local password
@ -336,9 +358,6 @@ class PasswordAuthProviderTests(unittest.HomeserverTestCase):
"auth": { "auth": {
"type": "test.login_type", "type": "test.login_type",
"identifier": {"type": "m.id.user", "user": "localuser"}, "identifier": {"type": "m.id.user", "user": "localuser"},
# FIXME "identifier" is ignored
# https://github.com/matrix-org/synapse/issues/5665
"user": "localuser",
"session": session, "session": session,
}, },
} }
@ -410,6 +429,85 @@ class PasswordAuthProviderTests(unittest.HomeserverTestCase):
self.assertEqual(channel.code, 400, channel.result) self.assertEqual(channel.code, 400, channel.result)
mock_password_provider.check_auth.assert_not_called() mock_password_provider.check_auth.assert_not_called()
@override_config(
{
**providers_config(PasswordCustomAuthProvider),
"password_config": {"enabled": False},
}
)
def test_password_custom_auth_password_disabled_login(self):
"""log in with a custom auth provider which implements password, but password
login is disabled"""
self.register_user("localuser", "localpass")
flows = self._get_login_flows()
self.assertEqual(flows, [{"type": "test.login_type"}] + ADDITIONAL_LOGIN_FLOWS)
# login shouldn't work and should be rejected with a 400 ("unknown login type")
channel = self._send_password_login("localuser", "localpass")
self.assertEqual(channel.code, 400, channel.result)
mock_password_provider.check_auth.assert_not_called()
@override_config(
{
**providers_config(PasswordCustomAuthProvider),
"password_config": {"enabled": False},
}
)
def test_password_custom_auth_password_disabled_ui_auth(self):
"""UI Auth with a custom auth provider which implements password, but password
login is disabled"""
# register the user and log in twice via the test login type to get two devices,
self.register_user("localuser", "localpass")
mock_password_provider.check_auth.return_value = defer.succeed(
"@localuser:test"
)
channel = self._send_login("test.login_type", "localuser", test_field="")
self.assertEqual(channel.code, 200, channel.result)
tok1 = channel.json_body["access_token"]
channel = self._send_login(
"test.login_type", "localuser", test_field="", device_id="dev2"
)
self.assertEqual(channel.code, 200, channel.result)
# make the initial request which returns a 401
channel = self._delete_device(tok1, "dev2")
self.assertEqual(channel.code, 401)
# Ensure that flows are what is expected. In particular, "password" should *not*
# be present.
self.assertIn({"stages": ["test.login_type"]}, channel.json_body["flows"])
session = channel.json_body["session"]
mock_password_provider.reset_mock()
# check that auth with password is rejected
body = {
"auth": {
"type": "m.login.password",
"identifier": {"type": "m.id.user", "user": "localuser"},
"password": "localpass",
"session": session,
},
}
channel = self._delete_device(tok1, "dev2", body)
self.assertEqual(channel.code, 400)
self.assertEqual(
"Password login has been disabled.", channel.json_body["error"]
)
mock_password_provider.check_auth.assert_not_called()
mock_password_provider.reset_mock()
# successful auth
body["auth"]["type"] = "test.login_type"
body["auth"]["test_field"] = "x"
channel = self._delete_device(tok1, "dev2", body)
self.assertEqual(channel.code, 200)
mock_password_provider.check_auth.assert_called_once_with(
"localuser", "test.login_type", {"test_field": "x"}
)
@override_config( @override_config(
{ {
**providers_config(CustomAuthProvider), **providers_config(CustomAuthProvider),
@ -428,8 +526,6 @@ class PasswordAuthProviderTests(unittest.HomeserverTestCase):
channel = self._send_password_login("localuser", "localpass") channel = self._send_password_login("localuser", "localpass")
self.assertEqual(channel.code, 400, channel.result) self.assertEqual(channel.code, 400, channel.result)
test_custom_auth_no_local_user_fallback.skip = "currently broken"
def _get_login_flows(self) -> JsonDict: def _get_login_flows(self) -> JsonDict:
_, channel = self.make_request("GET", "/_matrix/client/r0/login") _, channel = self.make_request("GET", "/_matrix/client/r0/login")
self.assertEqual(channel.code, 200, channel.result) self.assertEqual(channel.code, 200, channel.result)
@ -439,7 +535,7 @@ class PasswordAuthProviderTests(unittest.HomeserverTestCase):
return self._send_login(type="m.login.password", user=user, password=password) return self._send_login(type="m.login.password", user=user, password=password)
def _send_login(self, type, user, **params) -> FakeChannel: def _send_login(self, type, user, **params) -> FakeChannel:
params.update({"user": user, "type": type}) params.update({"identifier": {"type": "m.id.user", "user": user}, "type": type})
_, channel = self.make_request("POST", "/_matrix/client/r0/login", params) _, channel = self.make_request("POST", "/_matrix/client/r0/login", params)
return channel return channel
@ -467,9 +563,6 @@ class PasswordAuthProviderTests(unittest.HomeserverTestCase):
"auth": { "auth": {
"type": "m.login.password", "type": "m.login.password",
"identifier": {"type": "m.id.user", "user": user_id}, "identifier": {"type": "m.id.user", "user": user_id},
# FIXME "identifier" is ignored
# https://github.com/matrix-org/synapse/issues/5665
"user": user_id,
"password": password, "password": password,
"session": session, "session": session,
}, },

View File

@ -38,11 +38,6 @@ class DummyRecaptchaChecker(UserInteractiveAuthChecker):
return succeed(True) return succeed(True)
class DummyPasswordChecker(UserInteractiveAuthChecker):
def check_auth(self, authdict, clientip):
return succeed(authdict["identifier"]["user"])
class FallbackAuthTests(unittest.HomeserverTestCase): class FallbackAuthTests(unittest.HomeserverTestCase):
servlets = [ servlets = [
@ -162,9 +157,6 @@ class UIAuthTests(unittest.HomeserverTestCase):
] ]
def prepare(self, reactor, clock, hs): def prepare(self, reactor, clock, hs):
auth_handler = hs.get_auth_handler()
auth_handler.checkers[LoginType.PASSWORD] = DummyPasswordChecker(hs)
self.user_pass = "pass" self.user_pass = "pass"
self.user = self.register_user("test", self.user_pass) self.user = self.register_user("test", self.user_pass)
self.user_tok = self.login("test", self.user_pass) self.user_tok = self.login("test", self.user_pass)
@ -234,6 +226,31 @@ class UIAuthTests(unittest.HomeserverTestCase):
}, },
) )
def test_grandfathered_identifier(self):
"""Check behaviour without "identifier" dict
Synapse used to require clients to submit a "user" field for m.login.password
UIA - check that still works.
"""
device_id = self.get_device_ids()[0]
channel = self.delete_device(device_id, 401)
session = channel.json_body["session"]
# Make another request providing the UI auth flow.
self.delete_device(
device_id,
200,
{
"auth": {
"type": "m.login.password",
"user": self.user,
"password": self.user_pass,
"session": session,
},
},
)
def test_can_change_body(self): def test_can_change_body(self):
""" """
The client dict can be modified during the user interactive authentication session. The client dict can be modified during the user interactive authentication session.