register.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2014, 2015 OpenMarket Ltd
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. """This module contains REST servlets to do with registration: /register"""
  16. from twisted.internet import defer
  17. from synapse.api.errors import SynapseError, Codes
  18. from synapse.api.constants import LoginType
  19. from base import ClientV1RestServlet, client_path_pattern
  20. import synapse.util.stringutils as stringutils
  21. from synapse.util.async import run_on_reactor
  22. from hashlib import sha1
  23. import hmac
  24. import simplejson as json
  25. import logging
  26. import urllib
  27. logger = logging.getLogger(__name__)
  28. # We ought to be using hmac.compare_digest() but on older pythons it doesn't
  29. # exist. It's a _really minor_ security flaw to use plain string comparison
  30. # because the timing attack is so obscured by all the other code here it's
  31. # unlikely to make much difference
  32. if hasattr(hmac, "compare_digest"):
  33. compare_digest = hmac.compare_digest
  34. else:
  35. compare_digest = lambda a, b: a == b
  36. class RegisterRestServlet(ClientV1RestServlet):
  37. """Handles registration with the home server.
  38. This servlet is in control of the registration flow; the registration
  39. handler doesn't have a concept of multi-stages or sessions.
  40. """
  41. PATTERN = client_path_pattern("/register$")
  42. def __init__(self, hs):
  43. super(RegisterRestServlet, self).__init__(hs)
  44. # sessions are stored as:
  45. # self.sessions = {
  46. # "session_id" : { __session_dict__ }
  47. # }
  48. # TODO: persistent storage
  49. self.sessions = {}
  50. self.disable_registration = hs.config.disable_registration
  51. def on_GET(self, request):
  52. if self.hs.config.enable_registration_captcha:
  53. return (
  54. 200,
  55. {"flows": [
  56. {
  57. "type": LoginType.RECAPTCHA,
  58. "stages": [
  59. LoginType.RECAPTCHA,
  60. LoginType.EMAIL_IDENTITY,
  61. LoginType.PASSWORD
  62. ]
  63. },
  64. {
  65. "type": LoginType.RECAPTCHA,
  66. "stages": [LoginType.RECAPTCHA, LoginType.PASSWORD]
  67. }
  68. ]}
  69. )
  70. else:
  71. return (
  72. 200,
  73. {"flows": [
  74. {
  75. "type": LoginType.EMAIL_IDENTITY,
  76. "stages": [
  77. LoginType.EMAIL_IDENTITY, LoginType.PASSWORD
  78. ]
  79. },
  80. {
  81. "type": LoginType.PASSWORD
  82. }
  83. ]}
  84. )
  85. @defer.inlineCallbacks
  86. def on_POST(self, request):
  87. register_json = _parse_json(request)
  88. session = (register_json["session"]
  89. if "session" in register_json else None)
  90. login_type = None
  91. if "type" not in register_json:
  92. raise SynapseError(400, "Missing 'type' key.")
  93. try:
  94. login_type = register_json["type"]
  95. is_application_server = login_type == LoginType.APPLICATION_SERVICE
  96. if self.disable_registration and not is_application_server:
  97. raise SynapseError(403, "Registration has been disabled")
  98. stages = {
  99. LoginType.RECAPTCHA: self._do_recaptcha,
  100. LoginType.PASSWORD: self._do_password,
  101. LoginType.EMAIL_IDENTITY: self._do_email_identity,
  102. LoginType.APPLICATION_SERVICE: self._do_app_service
  103. }
  104. session_info = self._get_session_info(request, session)
  105. logger.debug("%s : session info %s request info %s",
  106. login_type, session_info, register_json)
  107. response = yield stages[login_type](
  108. request,
  109. register_json,
  110. session_info
  111. )
  112. if "access_token" not in response:
  113. # isn't a final response
  114. response["session"] = session_info["id"]
  115. defer.returnValue((200, response))
  116. except KeyError as e:
  117. logger.exception(e)
  118. raise SynapseError(400, "Missing JSON keys for login type %s." % (
  119. login_type,
  120. ))
  121. def on_OPTIONS(self, request):
  122. return (200, {})
  123. def _get_session_info(self, request, session_id):
  124. if not session_id:
  125. # create a new session
  126. while session_id is None or session_id in self.sessions:
  127. session_id = stringutils.random_string(24)
  128. self.sessions[session_id] = {
  129. "id": session_id,
  130. LoginType.EMAIL_IDENTITY: False,
  131. LoginType.RECAPTCHA: False
  132. }
  133. return self.sessions[session_id]
  134. def _save_session(self, session):
  135. # TODO: Persistent storage
  136. logger.debug("Saving session %s", session)
  137. self.sessions[session["id"]] = session
  138. def _remove_session(self, session):
  139. logger.debug("Removing session %s", session)
  140. self.sessions.pop(session["id"])
  141. @defer.inlineCallbacks
  142. def _do_recaptcha(self, request, register_json, session):
  143. if not self.hs.config.enable_registration_captcha:
  144. raise SynapseError(400, "Captcha not required.")
  145. yield self._check_recaptcha(request, register_json, session)
  146. session[LoginType.RECAPTCHA] = True # mark captcha as done
  147. self._save_session(session)
  148. defer.returnValue({
  149. "next": [LoginType.PASSWORD, LoginType.EMAIL_IDENTITY]
  150. })
  151. @defer.inlineCallbacks
  152. def _check_recaptcha(self, request, register_json, session):
  153. if ("captcha_bypass_hmac" in register_json and
  154. self.hs.config.captcha_bypass_secret):
  155. if "user" not in register_json:
  156. raise SynapseError(400, "Captcha bypass needs 'user'")
  157. want = hmac.new(
  158. key=self.hs.config.captcha_bypass_secret,
  159. msg=register_json["user"],
  160. digestmod=sha1,
  161. ).hexdigest()
  162. # str() because otherwise hmac complains that 'unicode' does not
  163. # have the buffer interface
  164. got = str(register_json["captcha_bypass_hmac"])
  165. if compare_digest(want, got):
  166. session["user"] = register_json["user"]
  167. defer.returnValue(None)
  168. else:
  169. raise SynapseError(
  170. 400, "Captcha bypass HMAC incorrect",
  171. errcode=Codes.CAPTCHA_NEEDED
  172. )
  173. challenge = None
  174. user_response = None
  175. try:
  176. challenge = register_json["challenge"]
  177. user_response = register_json["response"]
  178. except KeyError:
  179. raise SynapseError(400, "Captcha response is required",
  180. errcode=Codes.CAPTCHA_NEEDED)
  181. ip_addr = self.hs.get_ip_from_request(request)
  182. handler = self.handlers.registration_handler
  183. yield handler.check_recaptcha(
  184. ip_addr,
  185. self.hs.config.recaptcha_private_key,
  186. challenge,
  187. user_response
  188. )
  189. @defer.inlineCallbacks
  190. def _do_email_identity(self, request, register_json, session):
  191. if (self.hs.config.enable_registration_captcha and
  192. not session[LoginType.RECAPTCHA]):
  193. raise SynapseError(400, "Captcha is required.")
  194. threepidCreds = register_json['threepidCreds']
  195. handler = self.handlers.registration_handler
  196. logger.debug("Registering email. threepidcreds: %s" % (threepidCreds))
  197. yield handler.register_email(threepidCreds)
  198. session["threepidCreds"] = threepidCreds # store creds for next stage
  199. session[LoginType.EMAIL_IDENTITY] = True # mark email as done
  200. self._save_session(session)
  201. defer.returnValue({
  202. "next": LoginType.PASSWORD
  203. })
  204. @defer.inlineCallbacks
  205. def _do_password(self, request, register_json, session):
  206. yield run_on_reactor()
  207. if (self.hs.config.enable_registration_captcha and
  208. not session[LoginType.RECAPTCHA]):
  209. # captcha should've been done by this stage!
  210. raise SynapseError(400, "Captcha is required.")
  211. if ("user" in session and "user" in register_json and
  212. session["user"] != register_json["user"]):
  213. raise SynapseError(
  214. 400, "Cannot change user ID during registration"
  215. )
  216. password = register_json["password"].encode("utf-8")
  217. desired_user_id = (register_json["user"].encode("utf-8")
  218. if "user" in register_json else None)
  219. if (desired_user_id
  220. and urllib.quote(desired_user_id) != desired_user_id):
  221. raise SynapseError(
  222. 400,
  223. "User ID must only contain characters which do not " +
  224. "require URL encoding.")
  225. handler = self.handlers.registration_handler
  226. (user_id, token) = yield handler.register(
  227. localpart=desired_user_id,
  228. password=password
  229. )
  230. if session[LoginType.EMAIL_IDENTITY]:
  231. logger.debug("Binding emails %s to %s" % (
  232. session["threepidCreds"], user_id)
  233. )
  234. yield handler.bind_emails(user_id, session["threepidCreds"])
  235. result = {
  236. "user_id": user_id,
  237. "access_token": token,
  238. "home_server": self.hs.hostname,
  239. }
  240. self._remove_session(session)
  241. defer.returnValue(result)
  242. @defer.inlineCallbacks
  243. def _do_app_service(self, request, register_json, session):
  244. if "access_token" not in request.args:
  245. raise SynapseError(400, "Expected application service token.")
  246. if "user" not in register_json:
  247. raise SynapseError(400, "Expected 'user' key.")
  248. as_token = request.args["access_token"][0]
  249. user_localpart = register_json["user"].encode("utf-8")
  250. handler = self.handlers.registration_handler
  251. (user_id, token) = yield handler.appservice_register(
  252. user_localpart, as_token
  253. )
  254. self._remove_session(session)
  255. defer.returnValue({
  256. "user_id": user_id,
  257. "access_token": token,
  258. "home_server": self.hs.hostname,
  259. })
  260. def _parse_json(request):
  261. try:
  262. content = json.loads(request.content.read())
  263. if type(content) != dict:
  264. raise SynapseError(400, "Content must be a JSON object.")
  265. return content
  266. except ValueError:
  267. raise SynapseError(400, "Content not JSON.")
  268. def register_servlets(hs, http_server):
  269. RegisterRestServlet(hs).register(http_server)