123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706 |
- # -*- 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.
- # You may obtain a copy of the License at
- #
- # http://www.apache.org/licenses/LICENSE-2.0
- #
- # Unless required by applicable law or agreed to in writing, software
- # distributed under the License is distributed on an "AS IS" BASIS,
- # 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.
- from twisted.internet import defer
- import synapse
- import synapse.types
- from synapse.api.auth import get_access_token_from_request, has_access_token
- from synapse.api.constants import LoginType
- from synapse.types import RoomID, RoomAlias
- from synapse.api.errors import SynapseError, Codes, UnrecognizedRequestError
- from synapse.http.servlet import (
- RestServlet, parse_json_object_from_request, assert_params_in_request, parse_string
- )
- from synapse.util.msisdn import phone_number_to_msisdn
- from synapse.util.threepids import check_3pid_allowed
- from ._base import client_v2_patterns, interactive_auth_handler
- import logging
- import hmac
- from hashlib import sha1
- from synapse.util.async import run_on_reactor
- from synapse.util.ratelimitutils import FederationRateLimiter
- # We ought to be using hmac.compare_digest() but on older pythons it doesn't
- # exist. It's a _really minor_ security flaw to use plain string comparison
- # because the timing attack is so obscured by all the other code here it's
- # unlikely to make much difference
- if hasattr(hmac, "compare_digest"):
- compare_digest = hmac.compare_digest
- else:
- def compare_digest(a, b):
- return a == b
- logger = logging.getLogger(__name__)
- class EmailRegisterRequestTokenRestServlet(RestServlet):
- PATTERNS = client_v2_patterns("/register/email/requestToken$")
- def __init__(self, hs):
- """
- Args:
- hs (synapse.server.HomeServer): server
- """
- super(EmailRegisterRequestTokenRestServlet, self).__init__()
- self.hs = hs
- self.identity_handler = hs.get_handlers().identity_handler
- @defer.inlineCallbacks
- def on_POST(self, request):
- body = parse_json_object_from_request(request)
- assert_params_in_request(body, [
- 'id_server', 'client_secret', 'email', 'send_attempt'
- ])
- if not check_3pid_allowed(self.hs, "email", body['email']):
- raise SynapseError(
- 403, "Third party identifier is not allowed", Codes.THREEPID_DENIED,
- )
- existingUid = yield self.hs.get_datastore().get_user_id_by_threepid(
- 'email', body['email']
- )
- if existingUid is not None:
- raise SynapseError(400, "Email is already in use", Codes.THREEPID_IN_USE)
- ret = yield self.identity_handler.requestEmailToken(**body)
- defer.returnValue((200, ret))
- class MsisdnRegisterRequestTokenRestServlet(RestServlet):
- PATTERNS = client_v2_patterns("/register/msisdn/requestToken$")
- def __init__(self, hs):
- """
- Args:
- hs (synapse.server.HomeServer): server
- """
- super(MsisdnRegisterRequestTokenRestServlet, self).__init__()
- self.hs = hs
- self.identity_handler = hs.get_handlers().identity_handler
- @defer.inlineCallbacks
- def on_POST(self, request):
- body = parse_json_object_from_request(request)
- assert_params_in_request(body, [
- 'id_server', 'client_secret',
- 'country', 'phone_number',
- 'send_attempt',
- ])
- msisdn = phone_number_to_msisdn(body['country'], body['phone_number'])
- if not check_3pid_allowed(self.hs, "msisdn", msisdn):
- raise SynapseError(
- 403, "Third party identifier is not allowed", Codes.THREEPID_DENIED,
- )
- existingUid = yield self.hs.get_datastore().get_user_id_by_threepid(
- 'msisdn', msisdn
- )
- if existingUid is not None:
- raise SynapseError(
- 400, "Phone number is already in use", Codes.THREEPID_IN_USE
- )
- ret = yield self.identity_handler.requestMsisdnToken(**body)
- defer.returnValue((200, ret))
- class UsernameAvailabilityRestServlet(RestServlet):
- PATTERNS = client_v2_patterns("/register/available")
- def __init__(self, hs):
- """
- Args:
- hs (synapse.server.HomeServer): server
- """
- super(UsernameAvailabilityRestServlet, self).__init__()
- self.hs = hs
- self.registration_handler = hs.get_handlers().registration_handler
- self.ratelimiter = FederationRateLimiter(
- hs.get_clock(),
- # Time window of 2s
- window_size=2000,
- # Artificially delay requests if rate > sleep_limit/window_size
- sleep_limit=1,
- # Amount of artificial delay to apply
- sleep_msec=1000,
- # Error with 429 if more than reject_limit requests are queued
- reject_limit=1,
- # Allow 1 request at a time
- concurrent_requests=1,
- )
- @defer.inlineCallbacks
- def on_GET(self, request):
- ip = self.hs.get_ip_from_request(request)
- with self.ratelimiter.ratelimit(ip) as wait_deferred:
- yield wait_deferred
- username = parse_string(request, "username", required=True)
- yield self.registration_handler.check_username(username)
- defer.returnValue((200, {"available": True}))
- class RegisterRestServlet(RestServlet):
- PATTERNS = client_v2_patterns("/register$")
- def __init__(self, hs):
- """
- Args:
- hs (synapse.server.HomeServer): server
- """
- super(RegisterRestServlet, self).__init__()
- self.hs = hs
- self.auth = hs.get_auth()
- self.store = hs.get_datastore()
- self.auth_handler = hs.get_auth_handler()
- self.registration_handler = hs.get_handlers().registration_handler
- self.identity_handler = hs.get_handlers().identity_handler
- self.room_member_handler = hs.get_room_member_handler()
- self.device_handler = hs.get_device_handler()
- self.macaroon_gen = hs.get_macaroon_generator()
- @interactive_auth_handler
- @defer.inlineCallbacks
- def on_POST(self, request):
- yield run_on_reactor()
- body = parse_json_object_from_request(request)
- kind = "user"
- if "kind" in request.args:
- kind = request.args["kind"][0]
- if kind == "guest":
- ret = yield self._do_guest_registration(body)
- defer.returnValue(ret)
- return
- elif kind != "user":
- raise UnrecognizedRequestError(
- "Do not understand membership kind: %s" % (kind,)
- )
- # we do basic sanity checks here because the auth layer will store these
- # in sessions. Pull out the username/password provided to us.
- desired_password = None
- if 'password' in body:
- if (not isinstance(body['password'], basestring) or
- len(body['password']) > 512):
- raise SynapseError(400, "Invalid password")
- desired_password = body["password"]
- desired_username = None
- if 'username' in body:
- if (not isinstance(body['username'], basestring) or
- len(body['username']) > 512):
- raise SynapseError(400, "Invalid username")
- desired_username = body['username']
- appservice = None
- if has_access_token(request):
- appservice = yield self.auth.get_appservice_by_req(request)
- # fork off as soon as possible for ASes and shared secret auth which
- # have completely different registration flows to normal users
- # == Application Service Registration ==
- if appservice:
- # Set the desired user according to the AS API (which uses the
- # 'user' key not 'username'). Since this is a new addition, we'll
- # fallback to 'username' if they gave one.
- desired_username = body.get("user", desired_username)
- # XXX we should check that desired_username is valid. Currently
- # we give appservices carte blanche for any insanity in mxids,
- # because the IRC bridges rely on being able to register stupid
- # IDs.
- access_token = get_access_token_from_request(request)
- if isinstance(desired_username, basestring):
- result = yield self._do_appservice_registration(
- desired_username, access_token, body
- )
- defer.returnValue((200, result)) # we throw for non 200 responses
- return
- # for either shared secret or regular registration, downcase the
- # provided username before attempting to register it. This should mean
- # that people who try to register with upper-case in their usernames
- # don't get a nasty surprise. (Note that we treat username
- # case-insenstively in login, so they are free to carry on imagining
- # that their username is CrAzYh4cKeR if that keeps them happy)
- if desired_username is not None:
- desired_username = desired_username.lower()
- # == Shared Secret Registration == (e.g. create new user scripts)
- if 'mac' in body:
- # FIXME: Should we really be determining if this is shared secret
- # auth based purely on the 'mac' key?
- result = yield self._do_shared_secret_registration(
- desired_username, desired_password, body
- )
- defer.returnValue((200, result)) # we throw for non 200 responses
- return
- # == Normal User Registration == (everyone else)
- if not self.hs.config.enable_registration:
- raise SynapseError(403, "Registration has been disabled")
- guest_access_token = body.get("guest_access_token", None)
- if (
- 'initial_device_display_name' in body and
- 'password' not in body
- ):
- # ignore 'initial_device_display_name' if sent without
- # a password to work around a client bug where it sent
- # the 'initial_device_display_name' param alone, wiping out
- # the original registration params
- logger.warn("Ignoring initial_device_display_name without password")
- del body['initial_device_display_name']
- session_id = self.auth_handler.get_session_id(body)
- registered_user_id = None
- if session_id:
- # if we get a registered user id out of here, it means we previously
- # registered a user for this session, so we could just return the
- # user here. We carry on and go through the auth checks though,
- # for paranoia.
- registered_user_id = self.auth_handler.get_session_data(
- session_id, "registered_user_id", None
- )
- if desired_username is not None:
- yield self.registration_handler.check_username(
- desired_username,
- guest_access_token=guest_access_token,
- assigned_user_id=registered_user_id,
- )
- # Only give msisdn flows if the x_show_msisdn flag is given:
- # this is a hack to work around the fact that clients were shipped
- # that use fallback registration if they see any flows that they don't
- # recognise, which means we break registration for these clients if we
- # advertise msisdn flows. Once usage of Riot iOS <=0.3.9 and Riot
- # Android <=0.6.9 have fallen below an acceptable threshold, this
- # parameter should go away and we should always advertise msisdn flows.
- show_msisdn = False
- if 'x_show_msisdn' in body and body['x_show_msisdn']:
- show_msisdn = True
- # FIXME: need a better error than "no auth flow found" for scenarios
- # where we required 3PID for registration but the user didn't give one
- require_email = 'email' in self.hs.config.registrations_require_3pid
- require_msisdn = 'msisdn' in self.hs.config.registrations_require_3pid
- flows = []
- if self.hs.config.enable_registration_captcha:
- # only support 3PIDless registration if no 3PIDs are required
- if not require_email and not require_msisdn:
- flows.extend([[LoginType.RECAPTCHA]])
- # only support the email-only flow if we don't require MSISDN 3PIDs
- if not require_msisdn:
- flows.extend([[LoginType.EMAIL_IDENTITY, LoginType.RECAPTCHA]])
- if show_msisdn:
- # only support the MSISDN-only flow if we don't require email 3PIDs
- if not require_email:
- flows.extend([[LoginType.MSISDN, LoginType.RECAPTCHA]])
- # always let users provide both MSISDN & email
- flows.extend([
- [LoginType.MSISDN, LoginType.EMAIL_IDENTITY, LoginType.RECAPTCHA],
- ])
- else:
- # only support 3PIDless registration if no 3PIDs are required
- if not require_email and not require_msisdn:
- flows.extend([[LoginType.DUMMY]])
- # only support the email-only flow if we don't require MSISDN 3PIDs
- if not require_msisdn:
- flows.extend([[LoginType.EMAIL_IDENTITY]])
- if show_msisdn:
- # only support the MSISDN-only flow if we don't require email 3PIDs
- if not require_email or require_msisdn:
- flows.extend([[LoginType.MSISDN]])
- # always let users provide both MSISDN & email
- flows.extend([
- [LoginType.MSISDN, LoginType.EMAIL_IDENTITY]
- ])
- auth_result, params, session_id = yield self.auth_handler.check_auth(
- flows, body, self.hs.get_ip_from_request(request)
- )
- # Check that we're not trying to register a denied 3pid.
- #
- # the user-facing checks will probably already have happened in
- # /register/email/requestToken when we requested a 3pid, but that's not
- # guaranteed.
- if auth_result:
- for login_type in [LoginType.EMAIL_IDENTITY, LoginType.MSISDN]:
- if login_type in auth_result:
- medium = auth_result[login_type]['medium']
- address = auth_result[login_type]['address']
- if not check_3pid_allowed(self.hs, medium, address):
- raise SynapseError(
- 403, "Third party identifier is not allowed",
- Codes.THREEPID_DENIED,
- )
- if registered_user_id is not None:
- logger.info(
- "Already registered user ID %r for this session",
- registered_user_id
- )
- # don't re-register the threepids
- add_email = False
- add_msisdn = False
- else:
- # NB: This may be from the auth handler and NOT from the POST
- if 'password' not in params:
- raise SynapseError(400, "Missing password.",
- Codes.MISSING_PARAM)
- desired_username = params.get("username", None)
- new_password = params.get("password", None)
- guest_access_token = params.get("guest_access_token", None)
- if desired_username is not None:
- desired_username = desired_username.lower()
- (registered_user_id, _) = yield self.registration_handler.register(
- localpart=desired_username,
- password=new_password,
- guest_access_token=guest_access_token,
- generate_token=False,
- )
- # auto-join the user to any rooms we're supposed to dump them into
- fake_requester = synapse.types.create_requester(registered_user_id)
- for r in self.hs.config.auto_join_rooms:
- try:
- yield self._join_user_to_room(fake_requester, r)
- except Exception as e:
- logger.error("Failed to join new user to %r: %r", r, e)
- # remember that we've now registered that user account, and with
- # what user ID (since the user may not have specified)
- self.auth_handler.set_session_data(
- session_id, "registered_user_id", registered_user_id
- )
- add_email = True
- add_msisdn = True
- return_dict = yield self._create_registration_details(
- registered_user_id, params
- )
- if add_email and auth_result and LoginType.EMAIL_IDENTITY in auth_result:
- threepid = auth_result[LoginType.EMAIL_IDENTITY]
- yield self._register_email_threepid(
- registered_user_id, threepid, return_dict["access_token"],
- params.get("bind_email")
- )
- if add_msisdn and auth_result and LoginType.MSISDN in auth_result:
- threepid = auth_result[LoginType.MSISDN]
- yield self._register_msisdn_threepid(
- registered_user_id, threepid, return_dict["access_token"],
- params.get("bind_msisdn")
- )
- defer.returnValue((200, return_dict))
- def on_OPTIONS(self, _):
- return 200, {}
- @defer.inlineCallbacks
- def _join_user_to_room(self, requester, room_identifier):
- room_id = None
- if RoomID.is_valid(room_identifier):
- room_id = room_identifier
- elif RoomAlias.is_valid(room_identifier):
- room_alias = RoomAlias.from_string(room_identifier)
- room_id, remote_room_hosts = (
- yield self.room_member_handler.lookup_room_alias(room_alias)
- )
- room_id = room_id.to_string()
- else:
- raise SynapseError(400, "%s was not legal room ID or room alias" % (
- room_identifier,
- ))
- yield self.room_member_handler.update_membership(
- requester=requester,
- target=requester.user,
- room_id=room_id,
- action="join",
- )
- @defer.inlineCallbacks
- def _do_appservice_registration(self, username, as_token, body):
- user_id = yield self.registration_handler.appservice_register(
- username, as_token
- )
- defer.returnValue((yield self._create_registration_details(user_id, body)))
- @defer.inlineCallbacks
- def _do_shared_secret_registration(self, username, password, body):
- if not self.hs.config.registration_shared_secret:
- raise SynapseError(400, "Shared secret registration is not enabled")
- if not username:
- raise SynapseError(
- 400, "username must be specified", errcode=Codes.BAD_JSON,
- )
- # use the username from the original request rather than the
- # downcased one in `username` for the mac calculation
- user = body["username"].encode("utf-8")
- # str() because otherwise hmac complains that 'unicode' does not
- # have the buffer interface
- got_mac = str(body["mac"])
- # FIXME this is different to the /v1/register endpoint, which
- # includes the password and admin flag in the hashed text. Why are
- # these different?
- want_mac = hmac.new(
- key=self.hs.config.registration_shared_secret,
- msg=user,
- digestmod=sha1,
- ).hexdigest()
- if not compare_digest(want_mac, got_mac):
- raise SynapseError(
- 403, "HMAC incorrect",
- )
- (user_id, _) = yield self.registration_handler.register(
- localpart=username, password=password, generate_token=False,
- )
- result = yield self._create_registration_details(user_id, body)
- defer.returnValue(result)
- @defer.inlineCallbacks
- def _register_email_threepid(self, user_id, threepid, token, bind_email):
- """Add an email address as a 3pid identifier
- Also adds an email pusher for the email address, if configured in the
- HS config
- Also optionally binds emails to the given user_id on the identity server
- Args:
- user_id (str): id of user
- threepid (object): m.login.email.identity auth response
- token (str): access_token for the user
- bind_email (bool): true if the client requested the email to be
- bound at the identity server
- Returns:
- defer.Deferred:
- """
- reqd = ('medium', 'address', 'validated_at')
- if any(x not in threepid for x in reqd):
- # This will only happen if the ID server returns a malformed response
- logger.info("Can't add incomplete 3pid")
- return
- yield self.auth_handler.add_threepid(
- user_id,
- threepid['medium'],
- threepid['address'],
- threepid['validated_at'],
- )
- # And we add an email pusher for them by default, but only
- # if email notifications are enabled (so people don't start
- # getting mail spam where they weren't before if email
- # notifs are set up on a home server)
- if (self.hs.config.email_enable_notifs and
- self.hs.config.email_notif_for_new_users):
- # Pull the ID of the access token back out of the db
- # It would really make more sense for this to be passed
- # up when the access token is saved, but that's quite an
- # invasive change I'd rather do separately.
- user_tuple = yield self.store.get_user_by_access_token(
- token
- )
- token_id = user_tuple["token_id"]
- yield self.hs.get_pusherpool().add_pusher(
- user_id=user_id,
- access_token=token_id,
- kind="email",
- app_id="m.email",
- app_display_name="Email Notifications",
- device_display_name=threepid["address"],
- pushkey=threepid["address"],
- lang=None, # We don't know a user's language here
- data={},
- )
- if bind_email:
- logger.info("bind_email specified: binding")
- logger.debug("Binding emails %s to %s" % (
- threepid, user_id
- ))
- yield self.identity_handler.bind_threepid(
- threepid['threepid_creds'], user_id
- )
- else:
- logger.info("bind_email not specified: not binding email")
- @defer.inlineCallbacks
- def _register_msisdn_threepid(self, user_id, threepid, token, bind_msisdn):
- """Add a phone number as a 3pid identifier
- Also optionally binds msisdn to the given user_id on the identity server
- Args:
- user_id (str): id of user
- threepid (object): m.login.msisdn auth response
- token (str): access_token for the user
- bind_email (bool): true if the client requested the email to be
- bound at the identity server
- Returns:
- defer.Deferred:
- """
- reqd = ('medium', 'address', 'validated_at')
- if any(x not in threepid for x in reqd):
- # This will only happen if the ID server returns a malformed response
- logger.info("Can't add incomplete 3pid")
- defer.returnValue()
- yield self.auth_handler.add_threepid(
- user_id,
- threepid['medium'],
- threepid['address'],
- threepid['validated_at'],
- )
- if bind_msisdn:
- logger.info("bind_msisdn specified: binding")
- logger.debug("Binding msisdn %s to %s", threepid, user_id)
- yield self.identity_handler.bind_threepid(
- threepid['threepid_creds'], user_id
- )
- else:
- logger.info("bind_msisdn not specified: not binding msisdn")
- @defer.inlineCallbacks
- def _create_registration_details(self, user_id, params):
- """Complete registration of newly-registered user
- Allocates device_id if one was not given; also creates access_token.
- Args:
- (str) user_id: full canonical @user:id
- (object) params: registration parameters, from which we pull
- device_id, initial_device_name and inhibit_login
- Returns:
- defer.Deferred: (object) dictionary for response from /register
- """
- result = {
- "user_id": user_id,
- "home_server": self.hs.hostname,
- }
- if not params.get("inhibit_login", False):
- device_id = yield self._register_device(user_id, params)
- access_token = (
- yield self.auth_handler.get_access_token_for_user_id(
- user_id, device_id=device_id,
- )
- )
- result.update({
- "access_token": access_token,
- "device_id": device_id,
- })
- defer.returnValue(result)
- def _register_device(self, user_id, params):
- """Register a device for a user.
- This is called after the user's credentials have been validated, but
- before the access token has been issued.
- Args:
- (str) user_id: full canonical @user:id
- (object) params: registration parameters, from which we pull
- device_id and initial_device_name
- Returns:
- defer.Deferred: (str) device_id
- """
- # register the user's device
- device_id = params.get("device_id")
- initial_display_name = params.get("initial_device_display_name")
- return self.device_handler.check_device_registered(
- user_id, device_id, initial_display_name
- )
- @defer.inlineCallbacks
- def _do_guest_registration(self, params):
- if not self.hs.config.allow_guest_access:
- defer.returnValue((403, "Guest access is disabled"))
- user_id, _ = yield self.registration_handler.register(
- generate_token=False,
- make_guest=True
- )
- # we don't allow guests to specify their own device_id, because
- # we have nowhere to store it.
- device_id = synapse.api.auth.GUEST_DEVICE_ID
- initial_display_name = params.get("initial_device_display_name")
- yield self.device_handler.check_device_registered(
- user_id, device_id, initial_display_name
- )
- access_token = self.macaroon_gen.generate_access_token(
- user_id, ["guest = true"]
- )
- defer.returnValue((200, {
- "user_id": user_id,
- "device_id": device_id,
- "access_token": access_token,
- "home_server": self.hs.hostname,
- }))
- def register_servlets(hs, http_server):
- EmailRegisterRequestTokenRestServlet(hs).register(http_server)
- MsisdnRegisterRequestTokenRestServlet(hs).register(http_server)
- UsernameAvailabilityRestServlet(hs).register(http_server)
- RegisterRestServlet(hs).register(http_server)
|