-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Trying to set a password during UI Auth while also failing to authenticate during a stage will result in the password not being saved #9263
Description
Related: https://github.com/matrix-org/matrix-doc/issues/2907, #8009
CC: @clokep
DINUM were finding after upgrading to Synapse v1.23.0 that email registration was no longer working due to "Missing params: password". Their flow consisted of the following requests.
Check with the homeserver what the required flows to register are:
POST /register
{}
# get back a sessionID
Request a validation token from Synapse (no Sydent involved here):
POST /register/email/requestToken
{some client_secret and email address info}
# get back a sid (session ID for validating the email)
Request /register again, this time with a password which we want to save. We're doing to do a m.login.email.identity
here even though we know that it's going to fail. Note that m.login.email.identity
is the only flow the server gives us. We just want to save the password.
POST /register
{
"auth": {
"threepid_creds": { threepid_validation stuff that won't succeed because the link in the email hasn't been pressed yet },
"session": "what_we_got_from_the_first_request",
"type": "m.login.email.identity"
},
"password": "please_save_this_password123"
}
# Get back a 401. Fair, I expected this. But my password_hash has been recorded, right?
The client got a 401 one, but not from a InteractiveAuthIncompleteError
exception, but a LoginError
coming from here:
synapse/synapse/handlers/ui_auth/checkers.py
Lines 191 to 192 in c9c0ad5
if not threepid: | |
raise LoginError(401, "", errcode=Codes.UNAUTHORIZED) |
Now unfortunately, the code that stores password_hash
in ui_auth_sessions
for subsequent UI Auth requests will only do so if a InteractiveAuthIncompleteError
is raised, not LoginError
:
synapse/synapse/rest/client/v2_alpha/register.py
Lines 513 to 536 in 789d9eb
# Check if the user-interactive authentication flows are complete, if | |
# not this will raise a user-interactive auth error. | |
try: | |
auth_result, params, session_id = await self.auth_handler.check_ui_auth( | |
self._registration_flows, request, body, "register a new account", | |
) | |
except InteractiveAuthIncompleteError as e: | |
# The user needs to provide more steps to complete auth. | |
# | |
# Hash the password and store it with the session since the client | |
# is not required to provide the password again. | |
# | |
# If a password hash was previously stored we will not attempt to | |
# re-hash and store it for efficiency. This assumes the password | |
# does not change throughout the authentication flow, but this | |
# should be fine since the data is meant to be consistent. | |
if not password_hash and password: | |
password_hash = await self.auth_handler.hash(password) | |
await self.auth_handler.set_session_data( | |
e.session_id, | |
UIAuthSessionDataConstants.PASSWORD_HASH, | |
password_hash, | |
) | |
raise |
So uh oh. A password_hash
never got saved. In the case of DINUM, a user now clicks the link in their email and is brought to a completely separate client which doesn't know what the password is supposed to be.
That client (having validated the threepid) then makes a final request to /register
:
POST /register
{
"auth": {
"threepid_creds": { threepid_validation stuff that won't succeed because the link in the email hasn't been pressed yet }
}
}
# Gets back a 400! Missing params: password
And thus registration fails.
I think a simple solution here would be to catch LoginError
in addition to InteractiveAuthIncompleteError
where we store the pasword_hash:
synapse/synapse/rest/client/v2_alpha/register.py
Lines 513 to 536 in 789d9eb
# Check if the user-interactive authentication flows are complete, if | |
# not this will raise a user-interactive auth error. | |
try: | |
auth_result, params, session_id = await self.auth_handler.check_ui_auth( | |
self._registration_flows, request, body, "register a new account", | |
) | |
except InteractiveAuthIncompleteError as e: | |
# The user needs to provide more steps to complete auth. | |
# | |
# Hash the password and store it with the session since the client | |
# is not required to provide the password again. | |
# | |
# If a password hash was previously stored we will not attempt to | |
# re-hash and store it for efficiency. This assumes the password | |
# does not change throughout the authentication flow, but this | |
# should be fine since the data is meant to be consistent. | |
if not password_hash and password: | |
password_hash = await self.auth_handler.hash(password) | |
await self.auth_handler.set_session_data( | |
e.session_id, | |
UIAuthSessionDataConstants.PASSWORD_HASH, | |
password_hash, | |
) | |
raise |
That way the client would still get the same exact error, but now we're storing the hash when a LoginError
is raised. If we go with this, we should also do it in /account/password
, which does the same:
except InteractiveAuthIncompleteError as e: |
Additionally, the client could just send the password without attempting to complete an auth step, though this isn't really clear from the perspective of the client developer.