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

Warn, instead of erroring, if the client dict changes during UI Auth. #7483

Merged
merged 6 commits into from
May 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/7483.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Restore compatibility with non-compliant clients during the user interactive authentication process.
37 changes: 18 additions & 19 deletions synapse/handlers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,6 @@ async def check_auth(
clientdict: Dict[str, Any],
clientip: str,
description: str,
validate_clientdict: bool = True,
) -> Tuple[dict, dict, str]:
"""
Takes a dictionary sent by the client in the login / registration
Expand All @@ -278,10 +277,6 @@ async def check_auth(
description: A human readable string to be displayed to the user that
describes the operation happening on their account.

validate_clientdict: Whether to validate that the operation happening
on the account has not changed. If this is false,
the client dict is persisted instead of validated.

Returns:
A tuple of (creds, params, session_id).

Expand Down Expand Up @@ -346,26 +341,30 @@ async def check_auth(

# Ensure that the queried operation does not vary between stages of
# the UI authentication session. This is done by generating a stable
# comparator based on the URI, method, and client dict (minus the
# auth dict) and storing it during the initial query. Subsequent
# comparator and storing it during the initial query. Subsequent
# queries ensure that this comparator has not changed.
if validate_clientdict:
session_comparator = (session.uri, session.method, session.clientdict)
comparator = (uri, method, clientdict)
else:
session_comparator = (session.uri, session.method) # type: ignore
comparator = (uri, method) # type: ignore

if session_comparator != comparator:
#
# The comparator is based on the requested URI and HTTP method. The
# client dict (minus the auth dict) should also be checked, but some
# clients are not spec compliant, just warn for now if the client
# dict changes.
if (session.uri, session.method) != (uri, method):
raise SynapseError(
403,
"Requested operation has changed during the UI authentication session.",
)

# For backwards compatibility the registration endpoint persists
# changes to the client dict instead of validating them.
if not validate_clientdict:
await self.store.set_ui_auth_clientdict(sid, clientdict)
if session.clientdict != clientdict:
logger.warning(
"Requested operation has changed during the UI "
"authentication session. A future version of Synapse "
"will remove this capability."
)

# For backwards compatibility, changes to the client dict are
# persisted as clients modify them throughout their user interactive
# authentication flow.
await self.store.set_ui_auth_clientdict(sid, clientdict)

if not authdict:
raise InteractiveAuthIncompleteError(
Expand Down
1 change: 0 additions & 1 deletion synapse/rest/client/v2_alpha/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,6 @@ async def on_POST(self, request):
body,
self.hs.get_ip_from_request(request),
"register a new account",
validate_clientdict=False,
)

# Check that we're not trying to register a denied 3pid.
Expand Down
55 changes: 10 additions & 45 deletions tests/rest/client/v2_alpha/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,47 +133,6 @@ def test_fallback_captcha(self):
# We're given a registered user.
self.assertEqual(channel.json_body["user_id"], "@user:test")

def test_legacy_registration(self):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test becomes redundant with the test modified below since registration is not special anymore.

"""
Registration allows the parameters to vary through the process.
"""

# Make the initial request to register. (Later on a different password
# will be used.)
# Returns a 401 as per the spec
channel = self.register(
401, {"username": "user", "type": "m.login.password", "password": "bar"},
)

# Grab the session
session = channel.json_body["session"]
# Assert our configured public key is being given
self.assertEqual(
channel.json_body["params"]["m.login.recaptcha"]["public_key"], "brokencake"
)

# Complete the recaptcha step.
self.recaptcha(session, 200)

# also complete the dummy auth
self.register(200, {"auth": {"session": session, "type": "m.login.dummy"}})

# Now we should have fulfilled a complete auth flow, including
# the recaptcha fallback step. Make the initial request again, but
# with a changed password. This still completes.
channel = self.register(
200,
{
"username": "user",
"type": "m.login.password",
"password": "foo", # Note that this is different.
"auth": {"session": session},
},
)

# We're given a registered user.
self.assertEqual(channel.json_body["user_id"], "@user:test")

def test_complete_operation_unknown_session(self):
"""
Attempting to mark an invalid session as complete should error.
Expand Down Expand Up @@ -282,9 +241,15 @@ def test_ui_auth(self):
},
)

def test_cannot_change_body(self):
def test_can_change_body(self):
"""
The initial requested client dict cannot be modified during the user interactive authentication session.
The client dict can be modified during the user interactive authentication session.

Note that it is not spec compliant to modify the client dict during a
user interactive authentication session, but many clients currently do.

When Synapse is updated to be spec compliant, the call to re-use the
session ID should be rejected.
"""
# Create a second login.
self.login("test", self.user_pass)
Expand All @@ -302,9 +267,9 @@ def test_cannot_change_body(self):
self.assertIn({"stages": ["m.login.password"]}, channel.json_body["flows"])

# Make another request providing the UI auth flow, but try to delete the
# second device. This results in an error.
# second device.
self.delete_devices(
403,
200,
{
"devices": [device_ids[1]],
"auth": {
Expand Down