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

Commit 663396e

Browse files
authored
Merge pull request #1971 from matrix-org/dbkr/msisdn_signin
Support registration & login with phone number
2 parents 6ad71cc + ece7e00 commit 663396e

File tree

9 files changed

+395
-50
lines changed

9 files changed

+395
-50
lines changed

synapse/api/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# -*- coding: utf-8 -*-
22
# Copyright 2014-2016 OpenMarket Ltd
3+
# Copyright 2017 Vector Creations Ltd
34
#
45
# Licensed under the Apache License, Version 2.0 (the "License");
56
# you may not use this file except in compliance with the License.
@@ -44,6 +45,7 @@ class JoinRules(object):
4445
class LoginType(object):
4546
PASSWORD = u"m.login.password"
4647
EMAIL_IDENTITY = u"m.login.email.identity"
48+
MSISDN = u"m.login.msisdn"
4749
RECAPTCHA = u"m.login.recaptcha"
4850
DUMMY = u"m.login.dummy"
4951

synapse/handlers/auth.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# -*- coding: utf-8 -*-
22
# Copyright 2014 - 2016 OpenMarket Ltd
3+
# Copyright 2017 Vector Creations Ltd
34
#
45
# Licensed under the Apache License, Version 2.0 (the "License");
56
# you may not use this file except in compliance with the License.
@@ -47,6 +48,7 @@ def __init__(self, hs):
4748
LoginType.PASSWORD: self._check_password_auth,
4849
LoginType.RECAPTCHA: self._check_recaptcha,
4950
LoginType.EMAIL_IDENTITY: self._check_email_identity,
51+
LoginType.MSISDN: self._check_msisdn,
5052
LoginType.DUMMY: self._check_dummy_auth,
5153
}
5254
self.bcrypt_rounds = hs.config.bcrypt_rounds
@@ -307,31 +309,47 @@ def _check_recaptcha(self, authdict, clientip):
307309
defer.returnValue(True)
308310
raise LoginError(401, "", errcode=Codes.UNAUTHORIZED)
309311

310-
@defer.inlineCallbacks
311312
def _check_email_identity(self, authdict, _):
313+
return self._check_threepid('email', authdict)
314+
315+
def _check_msisdn(self, authdict, _):
316+
return self._check_threepid('msisdn', authdict)
317+
318+
@defer.inlineCallbacks
319+
def _check_dummy_auth(self, authdict, _):
320+
yield run_on_reactor()
321+
defer.returnValue(True)
322+
323+
@defer.inlineCallbacks
324+
def _check_threepid(self, medium, authdict):
312325
yield run_on_reactor()
313326

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

317330
threepid_creds = authdict['threepid_creds']
331+
318332
identity_handler = self.hs.get_handlers().identity_handler
319333

320-
logger.info("Getting validated threepid. threepidcreds: %r" % (threepid_creds,))
334+
logger.info("Getting validated threepid. threepidcreds: %r", (threepid_creds,))
321335
threepid = yield identity_handler.threepid_from_creds(threepid_creds)
322336

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

340+
if threepid['medium'] != medium:
341+
raise LoginError(
342+
401,
343+
"Expecting threepid of type '%s', got '%s'" % (
344+
medium, threepid['medium'],
345+
),
346+
errcode=Codes.UNAUTHORIZED
347+
)
348+
326349
threepid['threepid_creds'] = authdict['threepid_creds']
327350

328351
defer.returnValue(threepid)
329352

330-
@defer.inlineCallbacks
331-
def _check_dummy_auth(self, authdict, _):
332-
yield run_on_reactor()
333-
defer.returnValue(True)
334-
335353
def _get_params_recaptcha(self):
336354
return {"public_key": self.hs.config.recaptcha_public_key}
337355

synapse/handlers/identity.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# -*- coding: utf-8 -*-
22
# Copyright 2015, 2016 OpenMarket Ltd
3+
# Copyright 2017 Vector Creations Ltd
34
#
45
# Licensed under the Apache License, Version 2.0 (the "License");
56
# you may not use this file except in compliance with the License.
@@ -150,7 +151,7 @@ def requestEmailToken(self, id_server, email, client_secret, send_attempt, **kwa
150151
params.update(kwargs)
151152

152153
try:
153-
data = yield self.http_client.post_urlencoded_get_json(
154+
data = yield self.http_client.post_json_get_json(
154155
"https://%s%s" % (
155156
id_server,
156157
"/_matrix/identity/api/v1/validate/email/requestToken"
@@ -161,3 +162,37 @@ def requestEmailToken(self, id_server, email, client_secret, send_attempt, **kwa
161162
except CodeMessageException as e:
162163
logger.info("Proxied requestToken failed: %r", e)
163164
raise e
165+
166+
@defer.inlineCallbacks
167+
def requestMsisdnToken(
168+
self, id_server, country, phone_number,
169+
client_secret, send_attempt, **kwargs
170+
):
171+
yield run_on_reactor()
172+
173+
if not self._should_trust_id_server(id_server):
174+
raise SynapseError(
175+
400, "Untrusted ID server '%s'" % id_server,
176+
Codes.SERVER_NOT_TRUSTED
177+
)
178+
179+
params = {
180+
'country': country,
181+
'phone_number': phone_number,
182+
'client_secret': client_secret,
183+
'send_attempt': send_attempt,
184+
}
185+
params.update(kwargs)
186+
187+
try:
188+
data = yield self.http_client.post_json_get_json(
189+
"https://%s%s" % (
190+
id_server,
191+
"/_matrix/identity/api/v1/validate/msisdn/requestToken"
192+
),
193+
params
194+
)
195+
defer.returnValue(data)
196+
except CodeMessageException as e:
197+
logger.info("Proxied requestToken failed: %r", e)
198+
raise e

synapse/http/servlet.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,16 @@ def parse_json_object_from_request(request):
192192
return content
193193

194194

195+
def assert_params_in_request(body, required):
196+
absent = []
197+
for k in required:
198+
if k not in body:
199+
absent.append(k)
200+
201+
if len(absent) > 0:
202+
raise SynapseError(400, "Missing params: %r" % absent, Codes.MISSING_PARAM)
203+
204+
195205
class RestServlet(object):
196206

197207
""" A Synapse REST Servlet.

synapse/python_dependencies.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Copyright 2015, 2016 OpenMarket Ltd
2+
# Copyright 2017 Vector Creations Ltd
23
#
34
# Licensed under the Apache License, Version 2.0 (the "License");
45
# you may not use this file except in compliance with the License.
@@ -37,6 +38,7 @@
3738
"pysaml2>=3.0.0,<4.0.0": ["saml2>=3.0.0,<4.0.0"],
3839
"pymacaroons-pynacl": ["pymacaroons"],
3940
"msgpack-python>=0.3.0": ["msgpack"],
41+
"phonenumbers>=8.2.0": ["phonenumbers"],
4042
}
4143
CONDITIONAL_REQUIREMENTS = {
4244
"web_client": {

synapse/rest/client/v1/login.py

Lines changed: 82 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from synapse.types import UserID
2020
from synapse.http.server import finish_request
2121
from synapse.http.servlet import parse_json_object_from_request
22+
from synapse.util.msisdn import phone_number_to_msisdn
2223

2324
from .base import ClientV1RestServlet, client_path_patterns
2425

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

3940

41+
def login_submission_legacy_convert(submission):
42+
"""
43+
If the input login submission is an old style object
44+
(ie. with top-level user / medium / address) convert it
45+
to a typed object.
46+
"""
47+
if "user" in submission:
48+
submission["identifier"] = {
49+
"type": "m.id.user",
50+
"user": submission["user"],
51+
}
52+
del submission["user"]
53+
54+
if "medium" in submission and "address" in submission:
55+
submission["identifier"] = {
56+
"type": "m.id.thirdparty",
57+
"medium": submission["medium"],
58+
"address": submission["address"],
59+
}
60+
del submission["medium"]
61+
del submission["address"]
62+
63+
64+
def login_id_thirdparty_from_phone(identifier):
65+
"""
66+
Convert a phone login identifier type to a generic threepid identifier
67+
Args:
68+
identifier(dict): Login identifier dict of type 'm.id.phone'
69+
70+
Returns: Login identifier dict of type 'm.id.threepid'
71+
"""
72+
if "country" not in identifier or "number" not in identifier:
73+
raise SynapseError(400, "Invalid phone-type identifier")
74+
75+
msisdn = phone_number_to_msisdn(identifier["country"], identifier["number"])
76+
77+
return {
78+
"type": "m.id.thirdparty",
79+
"medium": "msisdn",
80+
"address": msisdn,
81+
}
82+
83+
4084
class LoginRestServlet(ClientV1RestServlet):
4185
PATTERNS = client_path_patterns("/login$")
4286
PASS_TYPE = "m.login.password"
@@ -117,20 +161,52 @@ def on_POST(self, request):
117161

118162
@defer.inlineCallbacks
119163
def do_password_login(self, login_submission):
120-
if 'medium' in login_submission and 'address' in login_submission:
121-
address = login_submission['address']
122-
if login_submission['medium'] == 'email':
164+
if "password" not in login_submission:
165+
raise SynapseError(400, "Missing parameter: password")
166+
167+
login_submission_legacy_convert(login_submission)
168+
169+
if "identifier" not in login_submission:
170+
raise SynapseError(400, "Missing param: identifier")
171+
172+
identifier = login_submission["identifier"]
173+
if "type" not in identifier:
174+
raise SynapseError(400, "Login identifier has no type")
175+
176+
# convert phone type identifiers to generic threepids
177+
if identifier["type"] == "m.id.phone":
178+
identifier = login_id_thirdparty_from_phone(identifier)
179+
180+
# convert threepid identifiers to user IDs
181+
if identifier["type"] == "m.id.thirdparty":
182+
if 'medium' not in identifier or 'address' not in identifier:
183+
raise SynapseError(400, "Invalid thirdparty identifier")
184+
185+
address = identifier['address']
186+
if identifier['medium'] == 'email':
123187
# For emails, transform the address to lowercase.
124188
# We store all email addreses as lowercase in the DB.
125189
# (See add_threepid in synapse/handlers/auth.py)
126190
address = address.lower()
127191
user_id = yield self.hs.get_datastore().get_user_id_by_threepid(
128-
login_submission['medium'], address
192+
identifier['medium'], address
129193
)
130194
if not user_id:
131195
raise LoginError(403, "", errcode=Codes.FORBIDDEN)
132-
else:
133-
user_id = login_submission['user']
196+
197+
identifier = {
198+
"type": "m.id.user",
199+
"user": user_id,
200+
}
201+
202+
# by this point, the identifier should be an m.id.user: if it's anything
203+
# else, we haven't understood it.
204+
if identifier["type"] != "m.id.user":
205+
raise SynapseError(400, "Unknown login identifier type")
206+
if "user" not in identifier:
207+
raise SynapseError(400, "User identifier is missing 'user' key")
208+
209+
user_id = identifier["user"]
134210

135211
if not user_id.startswith('@'):
136212
user_id = UserID.create(

0 commit comments

Comments
 (0)