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 @@
Ability for password providers to login/register a user via 3PID (email, phone).
richvdh marked this conversation as resolved.
Show resolved Hide resolved
11 changes: 11 additions & 0 deletions docs/password_auth_providers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,14 @@ Password auth provider classes may optionally provide the following methods.

It may return a Twisted ``Deferred`` object; the logout request will wait
for the deferred to complete but the result is ignored.

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

Choose a reason for hiding this comment

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

suggest putting this up with check_auth


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 ``String`` containing the user's User ID if authentication was
richvdh marked this conversation as resolved.
Show resolved Hide resolved
successful, and ``None`` if not.
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
15 changes: 15 additions & 0 deletions synapse/handlers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,21 @@ 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"""
richvdh marked this conversation as resolved.
Show resolved Hide resolved
for provider in self.password_providers:
if hasattr(provider, "check_3pid_auth"):
result = yield provider.check_3pid_auth(
medium, address, password,
)
if result:
if isinstance(result, str):
richvdh marked this conversation as resolved.
Show resolved Hide resolved
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 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 @@ -170,7 +170,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 @@ -618,7 +618,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 @@ -716,9 +716,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 @@ -760,7 +760,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
35 changes: 32 additions & 3 deletions synapse/module_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import time

from twisted.internet import defer

from synapse.types import UserID
Expand Down Expand Up @@ -73,14 +75,41 @@ 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, emails=[]):
"""Registers a new user with given localpart and optional
displayname, email.

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?

emails (List[str]): List of emails to assign to the new user.

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,
)

# Bind email address with the registered identity service
richvdh marked this conversation as resolved.
Show resolved Hide resolved
unix_secs = int(time.time())
Copy link
Member

Choose a reason for hiding this comment

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

you want to use hs.get_clock(), mostly because it makes testing easier if anyone ever comes to unit test this

for email in emails:
# generate threepid dict
threepid_dict = {
"medium": "email",
"address": email,
"validated_at": unix_secs,
}

# Bind email to new account
yield reg._register_email_threepid(
user_id, threepid_dict, None, False,
)

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 @@ -192,6 +192,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 @@ -214,20 +232,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