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

Support 3PID login in password providers #4931

Merged
merged 14 commits into from
Mar 26, 2019
1 change: 1 addition & 0 deletions changelog.d/4931.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add ability for password providers to login/register a user via 3PID (email, phone).
13 changes: 13 additions & 0 deletions docs/password_auth_providers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,19 @@ Password auth provider classes may optionally provide the following methods.
result from the ``/login`` call (including ``access_token``, ``device_id``,
etc.)

``someprovider.check_3pid_auth``\(*medium*, *address*, *password*)

This method, if implemented, is called when a user attempts to register or
log in with a third party identifier, such as email. It is passed the
medium (ex. "email"), an address (ex. "[email protected]") and the user's
password.

The method should return a Twisted ``Deferred`` object, which resolves to
a ``str`` containing the user's (canonical) User ID if authentication was
successful, and ``None`` if not. The ``Deferred`` can also instead
Copy link
Member

Choose a reason for hiding this comment

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

"also instead"

How about:

As with check_auth, the Deferred may alternatively resolve to a (user_id, callback) tuple.

... rather than duplicating the whole thing. Suggest a separate paragraph for clarity too.

resolve to a tuple of ``(str, callback)``, where ``callback`` is a
function that is run after login/registration has completed successfully.

``someprovider.check_password``\(*user_id*, *password*)

This method provides a simpler interface than ``get_supported_login_types``
Expand Down
22 changes: 11 additions & 11 deletions synapse/api/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -621,13 +621,13 @@ def check_redaction(self, room_version, event, auth_events):

Returns:
True if the the sender is allowed to redact the target event if the
target event was created by them.
target event was created by them.
False if the sender is allowed to redact the target event with no
further checks.
further checks.

Raises:
AuthError if the event sender is definitely not allowed to redact
the target event.
the target event.
"""
return event_auth.check_redaction(room_version, event, auth_events)

Expand Down Expand Up @@ -743,9 +743,9 @@ def check_in_room_or_world_readable(self, room_id, user_id):

Returns:
Deferred[tuple[str, str|None]]: Resolves to the current membership of
the user in the room and the membership event ID of the user. If
the user is not in the room and never has been, then
`(Membership.JOIN, None)` is returned.
the user in the room and the membership event ID of the user. If
the user is not in the room and never has been, then
`(Membership.JOIN, None)` is returned.
"""

try:
Expand Down Expand Up @@ -777,13 +777,13 @@ def check_auth_blocking(self, user_id=None, threepid=None):

Args:
user_id(str|None): If present, checks for presence against existing
MAU cohort
MAU cohort

threepid(dict|None): If present, checks for presence against configured
reserved threepid. Used in cases where the user is trying register
with a MAU blocked server, normally they would be rejected but their
threepid is on the reserved list. user_id and
threepid should never be set at the same time.
reserved threepid. Used in cases where the user is trying register
with a MAU blocked server, normally they would be rejected but their
threepid is on the reserved list. user_id and
threepid should never be set at the same time.
"""

# Never fail an auth check for the server notices users or support user
Expand Down
34 changes: 33 additions & 1 deletion synapse/handlers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,37 @@ def validate_login(self, username, login_submission):
errcode=Codes.FORBIDDEN
)

@defer.inlineCallbacks
def check_password_provider_3pid(self, medium, address, password):
"""Check if a password provider is able to validate a thirdparty login

Args:
medium (str): The medium of the 3pid (ex. email).
address (str): The address of the 3pid (ex. [email protected]).
password (str): The password of the user.

Returns:
Deferred[(str|None, None)]
Copy link
Member

Choose a reason for hiding this comment

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

what does the result mean?

"""
for provider in self.password_providers:
if hasattr(provider, "check_3pid_auth"):
# This function is able to return a deferred that either
# resolves None, meaning authentication failure, or upon
# success, to a str (which is the user_id) or a tuple of
# (user_id, callback_func), where callback_func should be run
# after we've finished everything else
result = yield provider.check_3pid_auth(
medium, address, password,
)
if result:
# Check if the return value is a str or a tuple
if isinstance(result, str):
richvdh marked this conversation as resolved.
Show resolved Hide resolved
# If it's a str, set callback function to None
result = (result, None)
defer.returnValue(result)

defer.returnValue((None, None))

@defer.inlineCallbacks
def _check_local_password(self, user_id, password):
"""Authenticate a user against the local password database.
Expand All @@ -756,7 +787,8 @@ def _check_local_password(self, user_id, password):
user_id (unicode): complete @user:id
password (unicode): the provided password
Returns:
(unicode) the canonical_user_id, or None if unknown user / bad password
Deferred[unicode] the canonical_user_id, or Deferred[None] if
unknown user/bad password

Raises:
LimitExceededError if the ratelimiter's login requests count for this
Expand Down
10 changes: 8 additions & 2 deletions synapse/handlers/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,14 @@ def get_displayname(self, target_user):

@defer.inlineCallbacks
def set_displayname(self, target_user, requester, new_displayname, by_admin=False):
"""target_user is the user whose displayname is to be changed;
auth_user is the user attempting to make this change."""
"""Set the displayname of a user

Args:
target_user (UserID): the user whose displayname is to be changed.
requester (Requester): The user attempting to make this change.
new_displayname (str): The displayname to give this user.
by_admin (bool): Whether this change was made by an administrator.
"""
if not self.hs.is_mine(target_user):
raise SynapseError(400, "User is not hosted on this Home Server")

Expand Down
10 changes: 5 additions & 5 deletions synapse/handlers/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ def register(
api.constants.UserTypes, or None for a normal user.
default_display_name (unicode|None): if set, the new user's displayname
will be set to this. Defaults to 'localpart'.
address (str|None): the IP address used to perform the regitration.
address (str|None): the IP address used to perform the registration.
Returns:
A tuple of (user_id, access_token).
Raises:
Expand Down Expand Up @@ -623,7 +623,7 @@ def register_with_store(self, user_id, token=None, password_hash=None,
admin (boolean): is an admin user?
user_type (str|None): type of user. One of the values from
api.constants.UserTypes, or None for a normal user.
address (str|None): the IP address used to perform the regitration.
address (str|None): the IP address used to perform the registration.

Returns:
Deferred
Expand Down Expand Up @@ -721,9 +721,9 @@ def post_registration_actions(self, user_id, auth_result, access_token,
access_token (str|None): The access token of the newly logged in
device, or None if `inhibit_login` enabled.
bind_email (bool): Whether to bind the email with the identity
server
server.
bind_msisdn (bool): Whether to bind the msisdn with the identity
server
server.
"""
if self.hs.config.worker_app:
yield self._post_registration_client(
Expand Down Expand Up @@ -765,7 +765,7 @@ def _on_user_consented(self, user_id, consent_version):
"""A user consented to the terms on registration

Args:
user_id (str): The user ID that consented
user_id (str): The user ID that consented.
consent_version (str): version of the policy the user has
consented to.
"""
Expand Down
17 changes: 14 additions & 3 deletions synapse/module_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,25 @@ def check_user_exists(self, user_id):
"""
return self._auth_handler.check_user_exists(user_id)

def register(self, localpart):
"""Registers a new user with given localpart
@defer.inlineCallbacks
def register(self, localpart, displayname=None):
"""Registers a new user with given localpart and optional
displayname.

Args:
localpart (str): The localpart of the new user.
displayname (str|None): The displayname of the new user.
Copy link
Member

Choose a reason for hiding this comment

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

what happens if it is set to None?


Returns:
Deferred: a 2-tuple of (user_id, access_token)
"""
# Register the user
reg = self.hs.get_registration_handler()
return reg.register(localpart=localpart)
user_id, access_token = yield reg.register(
localpart=localpart, default_display_name=displayname,
)

defer.returnValue((user_id, access_token))

@defer.inlineCallbacks
def invalidate_access_token(self, access_token):
Expand Down
49 changes: 45 additions & 4 deletions synapse/rest/client/v1/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,24 @@ def _do_other_login(self, login_submission):
# We store all email addreses as lowercase in the DB.
# (See add_threepid in synapse/handlers/auth.py)
address = address.lower()

# Check for login providers that support 3pid login types
canonical_user_id, callback_3pid = (
yield self.auth_handler.check_password_provider_3pid(
medium,
address,
login_submission["password"],
)
)
if canonical_user_id:
# Authentication through password provider and 3pid succeeded
result = yield self._register_device_with_callback(
canonical_user_id, login_submission, callback_3pid,
)
defer.returnValue(result)

# No password providers were able to handle this 3pid
# Check local store
user_id = yield self.hs.get_datastore().get_user_id_by_threepid(
medium, address,
)
Expand All @@ -223,20 +241,43 @@ def _do_other_login(self, login_submission):
if "user" not in identifier:
raise SynapseError(400, "User identifier is missing 'user' key")

auth_handler = self.auth_handler
canonical_user_id, callback = yield auth_handler.validate_login(
canonical_user_id, callback = yield self.auth_handler.validate_login(
identifier["user"],
login_submission,
)

result = yield self._register_device_with_callback(
canonical_user_id, login_submission, callback,
)
defer.returnValue(result)

@defer.inlineCallbacks
def _register_device_with_callback(
self,
user_id,
login_submission,
callback=None,
):
""" Registers a device with a given user_id. Optionally run a callback
function after registration has completed.

Args:
user_id (str): ID of the user to register.
login_submission (dict): Dictionary of login information.
callback (func|None): Callback function to run after registration.

Returns:
result (Dict[str,str]): Dictionary of account information after
successful registration.
"""
device_id = login_submission.get("device_id")
initial_display_name = login_submission.get("initial_device_display_name")
device_id, access_token = yield self.registration_handler.register_device(
canonical_user_id, device_id, initial_display_name,
user_id, device_id, initial_display_name,
)

result = {
"user_id": canonical_user_id,
"user_id": user_id,
"access_token": access_token,
"home_server": self.hs.hostname,
"device_id": device_id,
Expand Down