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

Commit

Permalink
Merge pull request #1994 from matrix-org/dbkr/msisdn_signin_2
Browse files Browse the repository at this point in the history
Phone number registration / login support v2
  • Loading branch information
erikjohnston committed Mar 15, 2017
2 parents f2ed64e + 7b6ed98 commit 7827251
Show file tree
Hide file tree
Showing 9 changed files with 412 additions and 50 deletions.
2 changes: 2 additions & 0 deletions synapse/api/constants.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2014-2016 OpenMarket Ltd
# Copyright 2017 Vector Creations Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -44,6 +45,7 @@ class JoinRules(object):
class LoginType(object):
PASSWORD = u"m.login.password"
EMAIL_IDENTITY = u"m.login.email.identity"
MSISDN = u"m.login.msisdn"
RECAPTCHA = u"m.login.recaptcha"
DUMMY = u"m.login.dummy"

Expand Down
32 changes: 25 additions & 7 deletions synapse/handlers/auth.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2014 - 2016 OpenMarket Ltd
# Copyright 2017 Vector Creations Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -47,6 +48,7 @@ def __init__(self, hs):
LoginType.PASSWORD: self._check_password_auth,
LoginType.RECAPTCHA: self._check_recaptcha,
LoginType.EMAIL_IDENTITY: self._check_email_identity,
LoginType.MSISDN: self._check_msisdn,
LoginType.DUMMY: self._check_dummy_auth,
}
self.bcrypt_rounds = hs.config.bcrypt_rounds
Expand Down Expand Up @@ -307,31 +309,47 @@ def _check_recaptcha(self, authdict, clientip):
defer.returnValue(True)
raise LoginError(401, "", errcode=Codes.UNAUTHORIZED)

@defer.inlineCallbacks
def _check_email_identity(self, authdict, _):
return self._check_threepid('email', authdict)

def _check_msisdn(self, authdict, _):
return self._check_threepid('msisdn', authdict)

@defer.inlineCallbacks
def _check_dummy_auth(self, authdict, _):
yield run_on_reactor()
defer.returnValue(True)

@defer.inlineCallbacks
def _check_threepid(self, medium, authdict):
yield run_on_reactor()

if 'threepid_creds' not in authdict:
raise LoginError(400, "Missing threepid_creds", Codes.MISSING_PARAM)

threepid_creds = authdict['threepid_creds']

identity_handler = self.hs.get_handlers().identity_handler

logger.info("Getting validated threepid. threepidcreds: %r" % (threepid_creds,))
logger.info("Getting validated threepid. threepidcreds: %r", (threepid_creds,))
threepid = yield identity_handler.threepid_from_creds(threepid_creds)

if not threepid:
raise LoginError(401, "", errcode=Codes.UNAUTHORIZED)

if threepid['medium'] != medium:
raise LoginError(
401,
"Expecting threepid of type '%s', got '%s'" % (
medium, threepid['medium'],
),
errcode=Codes.UNAUTHORIZED
)

threepid['threepid_creds'] = authdict['threepid_creds']

defer.returnValue(threepid)

@defer.inlineCallbacks
def _check_dummy_auth(self, authdict, _):
yield run_on_reactor()
defer.returnValue(True)

def _get_params_recaptcha(self):
return {"public_key": self.hs.config.recaptcha_public_key}

Expand Down
37 changes: 36 additions & 1 deletion synapse/handlers/identity.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2015, 2016 OpenMarket Ltd
# Copyright 2017 Vector Creations Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -150,7 +151,7 @@ def requestEmailToken(self, id_server, email, client_secret, send_attempt, **kwa
params.update(kwargs)

try:
data = yield self.http_client.post_urlencoded_get_json(
data = yield self.http_client.post_json_get_json(
"https://%s%s" % (
id_server,
"/_matrix/identity/api/v1/validate/email/requestToken"
Expand All @@ -161,3 +162,37 @@ def requestEmailToken(self, id_server, email, client_secret, send_attempt, **kwa
except CodeMessageException as e:
logger.info("Proxied requestToken failed: %r", e)
raise e

@defer.inlineCallbacks
def requestMsisdnToken(
self, id_server, country, phone_number,
client_secret, send_attempt, **kwargs
):
yield run_on_reactor()

if not self._should_trust_id_server(id_server):
raise SynapseError(
400, "Untrusted ID server '%s'" % id_server,
Codes.SERVER_NOT_TRUSTED
)

params = {
'country': country,
'phone_number': phone_number,
'client_secret': client_secret,
'send_attempt': send_attempt,
}
params.update(kwargs)

try:
data = yield self.http_client.post_json_get_json(
"https://%s%s" % (
id_server,
"/_matrix/identity/api/v1/validate/msisdn/requestToken"
),
params
)
defer.returnValue(data)
except CodeMessageException as e:
logger.info("Proxied requestToken failed: %r", e)
raise e
10 changes: 10 additions & 0 deletions synapse/http/servlet.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,16 @@ def parse_json_object_from_request(request):
return content


def assert_params_in_request(body, required):
absent = []
for k in required:
if k not in body:
absent.append(k)

if len(absent) > 0:
raise SynapseError(400, "Missing params: %r" % absent, Codes.MISSING_PARAM)


class RestServlet(object):

""" A Synapse REST Servlet.
Expand Down
2 changes: 2 additions & 0 deletions synapse/python_dependencies.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Copyright 2015, 2016 OpenMarket Ltd
# Copyright 2017 Vector Creations Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -37,6 +38,7 @@
"pysaml2>=3.0.0,<4.0.0": ["saml2>=3.0.0,<4.0.0"],
"pymacaroons-pynacl": ["pymacaroons"],
"msgpack-python>=0.3.0": ["msgpack"],
"phonenumbers>=8.2.0": ["phonenumbers"],
}
CONDITIONAL_REQUIREMENTS = {
"web_client": {
Expand Down
88 changes: 82 additions & 6 deletions synapse/rest/client/v1/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from synapse.types import UserID
from synapse.http.server import finish_request
from synapse.http.servlet import parse_json_object_from_request
from synapse.util.msisdn import phone_number_to_msisdn

from .base import ClientV1RestServlet, client_path_patterns

Expand All @@ -37,6 +38,49 @@
logger = logging.getLogger(__name__)


def login_submission_legacy_convert(submission):
"""
If the input login submission is an old style object
(ie. with top-level user / medium / address) convert it
to a typed object.
"""
if "user" in submission:
submission["identifier"] = {
"type": "m.id.user",
"user": submission["user"],
}
del submission["user"]

if "medium" in submission and "address" in submission:
submission["identifier"] = {
"type": "m.id.thirdparty",
"medium": submission["medium"],
"address": submission["address"],
}
del submission["medium"]
del submission["address"]


def login_id_thirdparty_from_phone(identifier):
"""
Convert a phone login identifier type to a generic threepid identifier
Args:
identifier(dict): Login identifier dict of type 'm.id.phone'
Returns: Login identifier dict of type 'm.id.threepid'
"""
if "country" not in identifier or "number" not in identifier:
raise SynapseError(400, "Invalid phone-type identifier")

msisdn = phone_number_to_msisdn(identifier["country"], identifier["number"])

return {
"type": "m.id.thirdparty",
"medium": "msisdn",
"address": msisdn,
}


class LoginRestServlet(ClientV1RestServlet):
PATTERNS = client_path_patterns("/login$")
PASS_TYPE = "m.login.password"
Expand Down Expand Up @@ -117,20 +161,52 @@ def on_POST(self, request):

@defer.inlineCallbacks
def do_password_login(self, login_submission):
if 'medium' in login_submission and 'address' in login_submission:
address = login_submission['address']
if login_submission['medium'] == 'email':
if "password" not in login_submission:
raise SynapseError(400, "Missing parameter: password")

login_submission_legacy_convert(login_submission)

if "identifier" not in login_submission:
raise SynapseError(400, "Missing param: identifier")

identifier = login_submission["identifier"]
if "type" not in identifier:
raise SynapseError(400, "Login identifier has no type")

# convert phone type identifiers to generic threepids
if identifier["type"] == "m.id.phone":
identifier = login_id_thirdparty_from_phone(identifier)

# convert threepid identifiers to user IDs
if identifier["type"] == "m.id.thirdparty":
if 'medium' not in identifier or 'address' not in identifier:
raise SynapseError(400, "Invalid thirdparty identifier")

address = identifier['address']
if identifier['medium'] == 'email':
# For emails, transform the address to lowercase.
# We store all email addreses as lowercase in the DB.
# (See add_threepid in synapse/handlers/auth.py)
address = address.lower()
user_id = yield self.hs.get_datastore().get_user_id_by_threepid(
login_submission['medium'], address
identifier['medium'], address
)
if not user_id:
raise LoginError(403, "", errcode=Codes.FORBIDDEN)
else:
user_id = login_submission['user']

identifier = {
"type": "m.id.user",
"user": user_id,
}

# by this point, the identifier should be an m.id.user: if it's anything
# else, we haven't understood it.
if identifier["type"] != "m.id.user":
raise SynapseError(400, "Unknown login identifier type")
if "user" not in identifier:
raise SynapseError(400, "User identifier is missing 'user' key")

user_id = identifier["user"]

if not user_id.startswith('@'):
user_id = UserID.create(
Expand Down
Loading

0 comments on commit 7827251

Please sign in to comment.