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

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

@anoadragon453

Description

@anoadragon453

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:

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:

# 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:

# 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    S-MinorBlocks non-critical functionality, workarounds exist.T-DefectBugs, crashes, hangs, security vulnerabilities, or other reported issues.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions