def setUp(self): self.clock = MemoryReactorClock() self.hs_clock = Clock(self.clock) self.url = b"/_matrix/client/r0/register" self.appservice = None self.auth = Mock(get_appservice_by_req=Mock( side_effect=lambda x: self.appservice)) self.auth_result = failure.Failure( InteractiveAuthIncompleteError(None)) self.auth_handler = Mock( check_auth=Mock(side_effect=lambda x, y, z: self.auth_result), get_session_data=Mock(return_value=None), ) self.registration_handler = Mock() self.identity_handler = Mock() self.login_handler = Mock() self.device_handler = Mock() self.device_handler.check_device_registered = Mock(return_value="FAKE") self.datastore = Mock(return_value=Mock()) self.datastore.get_current_state_deltas = Mock(return_value=[]) # do the dance to hook it up to the hs global self.handlers = Mock( registration_handler=self.registration_handler, identity_handler=self.identity_handler, login_handler=self.login_handler, ) self.hs = setup_test_homeserver(self.addCleanup, http_client=None, clock=self.hs_clock, reactor=self.clock) self.hs.get_auth = Mock(return_value=self.auth) self.hs.get_handlers = Mock(return_value=self.handlers) self.hs.get_auth_handler = Mock(return_value=self.auth_handler) self.hs.get_device_handler = Mock(return_value=self.device_handler) self.hs.get_datastore = Mock(return_value=self.datastore) self.hs.config.enable_registration = True self.hs.config.registrations_require_3pid = [] self.hs.config.auto_join_rooms = [] self.resource = JsonResource(self.hs) register_servlets(self.hs, self.resource)
def setUp(self): # do the dance to hook up request data to self.request_data self.request_data = "" self.request = Mock( content=Mock(read=Mock(side_effect=lambda: self.request_data)), path='/_matrix/api/v2_alpha/register' ) self.request.args = {} self.request.requestHeaders.getRawHeaders = mock_getRawHeaders() self.appservice = None self.auth = Mock(get_appservice_by_req=Mock( side_effect=lambda x: self.appservice) ) self.auth_result = failure.Failure(InteractiveAuthIncompleteError(None)) self.auth_handler = Mock( check_auth=Mock(side_effect=lambda x, y, z: self.auth_result), get_session_data=Mock(return_value=None) ) self.registration_handler = Mock() self.identity_handler = Mock() self.login_handler = Mock() self.device_handler = Mock() # do the dance to hook it up to the hs global self.handlers = Mock( registration_handler=self.registration_handler, identity_handler=self.identity_handler, login_handler=self.login_handler ) self.hs = Mock() self.hs.hostname = "superbig~testing~thing.com" self.hs.get_auth = Mock(return_value=self.auth) self.hs.get_handlers = Mock(return_value=self.handlers) self.hs.get_auth_handler = Mock(return_value=self.auth_handler) self.hs.get_device_handler = Mock(return_value=self.device_handler) self.hs.config.enable_registration = True self.hs.config.auto_join_rooms = [] # init the thing we're testing self.servlet = RegisterRestServlet(self.hs)
def check_auth(self, flows, clientdict, clientip): """ Takes a dictionary sent by the client in the login / registration protocol and handles the User-Interactive Auth flow. As a side effect, this function fills in the 'creds' key on the user's session with a map, which maps each auth-type (str) to the relevant identity authenticated by that auth-type (mostly str, but for captcha, bool). If no auth flows have been completed successfully, raises an InteractiveAuthIncompleteError. To handle this, you can use synapse.rest.client.v2_alpha._base.interactive_auth_handler as a decorator. Args: flows (list): A list of login flows. Each flow is an ordered list of strings representing auth-types. At least one full flow must be completed in order for auth to be successful. clientdict: The dictionary from the client root level, not the 'auth' key: this method prompts for auth if none is sent. clientip (str): The IP address of the client. Returns: defer.Deferred[dict, dict, str]: a deferred tuple of (creds, params, session_id). 'creds' contains the authenticated credentials of each stage. 'params' contains the parameters for this request (which may have been given only in a previous call). 'session_id' is the ID of this session, either passed in by the client or assigned by this call Raises: InteractiveAuthIncompleteError if the client has not yet completed all the stages in any of the permitted flows. """ authdict = None sid = None if clientdict and "auth" in clientdict: authdict = clientdict["auth"] del clientdict["auth"] if "session" in authdict: sid = authdict["session"] session = self._get_session_info(sid) if len(clientdict) > 0: # This was designed to allow the client to omit the parameters # and just supply the session in subsequent calls so it split # auth between devices by just sharing the session, (eg. so you # could continue registration from your phone having clicked the # email auth link on there). It's probably too open to abuse # because it lets unauthenticated clients store arbitrary objects # on a homeserver. # Revisit: Assumimg the REST APIs do sensible validation, the data # isn't arbintrary. session["clientdict"] = clientdict self._save_session(session) elif "clientdict" in session: clientdict = session["clientdict"] if not authdict: raise InteractiveAuthIncompleteError( self._auth_dict_for_flows(flows, session)) if "creds" not in session: session["creds"] = {} creds = session["creds"] # check auth type currently being presented errordict = {} if "type" in authdict: login_type = authdict["type"] try: result = yield self._check_auth_dict(authdict, clientip) if result: creds[login_type] = result self._save_session(session) except LoginError as e: if login_type == LoginType.EMAIL_IDENTITY: # riot used to have a bug where it would request a new # validation token (thus sending a new email) each time it # got a 401 with a 'flows' field. # (https://github.com/vector-im/vector-web/issues/2447). # # Grandfather in the old behaviour for now to avoid # breaking old riot deployments. raise # this step failed. Merge the error dict into the response # so that the client can have another go. errordict = e.error_dict() for f in flows: if len(set(f) - set(creds)) == 0: # it's very useful to know what args are stored, but this can # include the password in the case of registering, so only log # the keys (confusingly, clientdict may contain a password # param, creds is just what the user authed as for UI auth # and is not sensitive). logger.info( "Auth completed with creds: %r. Client dict has keys: %r", creds, list(clientdict), ) return creds, clientdict, session["id"] ret = self._auth_dict_for_flows(flows, session) ret["completed"] = list(creds) ret.update(errordict) raise InteractiveAuthIncompleteError(ret)
async def check_auth( self, flows: List[List[str]], request: SynapseRequest, clientdict: Dict[str, Any], clientip: str, description: str, ) -> Tuple[dict, dict, str]: """ Takes a dictionary sent by the client in the login / registration protocol and handles the User-Interactive Auth flow. If no auth flows have been completed successfully, raises an InteractiveAuthIncompleteError. To handle this, you can use synapse.rest.client.v2_alpha._base.interactive_auth_handler as a decorator. Args: flows: A list of login flows. Each flow is an ordered list of strings representing auth-types. At least one full flow must be completed in order for auth to be successful. request: The request sent by the client. clientdict: The dictionary from the client root level, not the 'auth' key: this method prompts for auth if none is sent. clientip: The IP address of the client. description: A human readable string to be displayed to the user that describes the operation happening on their account. Returns: A tuple of (creds, params, session_id). 'creds' contains the authenticated credentials of each stage. 'params' contains the parameters for this request (which may have been given only in a previous call). 'session_id' is the ID of this session, either passed in by the client or assigned by this call Raises: InteractiveAuthIncompleteError if the client has not yet completed all the stages in any of the permitted flows. """ authdict = None sid = None # type: Optional[str] if clientdict and "auth" in clientdict: authdict = clientdict["auth"] del clientdict["auth"] if "session" in authdict: sid = authdict["session"] # Convert the URI and method to strings. uri = request.uri.decode("utf-8") method = request.uri.decode("utf-8") # If there's no session ID, create a new session. if not sid: session = await self.store.create_ui_auth_session( clientdict, uri, method, description) else: try: session = await self.store.get_ui_auth_session(sid) except StoreError: raise SynapseError(400, "Unknown session ID: %s" % (sid, )) # If the client provides parameters, update what is persisted, # otherwise use whatever was last provided. # # This was designed to allow the client to omit the parameters # and just supply the session in subsequent calls so it split # auth between devices by just sharing the session, (eg. so you # could continue registration from your phone having clicked the # email auth link on there). It's probably too open to abuse # because it lets unauthenticated clients store arbitrary objects # on a homeserver. # # Revisit: Assuming the REST APIs do sensible validation, the data # isn't arbitrary. # # Note that the registration endpoint explicitly removes the # "initial_device_display_name" parameter if it is provided # without a "password" parameter. See the changes to # synapse.rest.client.v2_alpha.register.RegisterRestServlet.on_POST # in commit 544722bad23fc31056b9240189c3cbbbf0ffd3f9. if not clientdict: clientdict = session.clientdict # Ensure that the queried operation does not vary between stages of # the UI authentication session. This is done by generating a stable # comparator and storing it during the initial query. Subsequent # queries ensure that this comparator has not changed. # # The comparator is based on the requested URI and HTTP method. The # client dict (minus the auth dict) should also be checked, but some # clients are not spec compliant, just warn for now if the client # dict changes. if (session.uri, session.method) != (uri, method): raise SynapseError( 403, "Requested operation has changed during the UI authentication session.", ) if session.clientdict != clientdict: logger.warning( "Requested operation has changed during the UI " "authentication session. A future version of Synapse " "will remove this capability.") # For backwards compatibility, changes to the client dict are # persisted as clients modify them throughout their user interactive # authentication flow. await self.store.set_ui_auth_clientdict(sid, clientdict) if not authdict: raise InteractiveAuthIncompleteError( self._auth_dict_for_flows(flows, session.session_id)) # check auth type currently being presented errordict = {} # type: Dict[str, Any] if "type" in authdict: login_type = authdict["type"] # type: str try: result = await self._check_auth_dict(authdict, clientip) if result: await self.store.mark_ui_auth_stage_complete( session.session_id, login_type, result) except LoginError as e: if login_type == LoginType.EMAIL_IDENTITY: # riot used to have a bug where it would request a new # validation token (thus sending a new email) each time it # got a 401 with a 'flows' field. # (https://github.com/vector-im/vector-web/issues/2447). # # Grandfather in the old behaviour for now to avoid # breaking old riot deployments. raise # this step failed. Merge the error dict into the response # so that the client can have another go. errordict = e.error_dict() creds = await self.store.get_completed_ui_auth_stages( session.session_id) for f in flows: if len(set(f) - set(creds)) == 0: # it's very useful to know what args are stored, but this can # include the password in the case of registering, so only log # the keys (confusingly, clientdict may contain a password # param, creds is just what the user authed as for UI auth # and is not sensitive). logger.info( "Auth completed with creds: %r. Client dict has keys: %r", creds, list(clientdict), ) return creds, clientdict, session.session_id ret = self._auth_dict_for_flows(flows, session.session_id) ret["completed"] = list(creds) ret.update(errordict) raise InteractiveAuthIncompleteError(ret)