def authV2( sydent: "Sydent", request: Request, requireTermsAgreed: bool = True, ) -> "Account": """For v2 APIs check that the request has a valid access token associated with it :param sydent: The Sydent instance to use. :param request: The request to look for an access token in. :param requireTermsAgreed: Whether to deny authentication if the user hasn't accepted the terms of service. :returns Account: The account object if there is correct auth :raises MatrixRestError: If the request is v2 but could not be authed or the user has not accepted terms. """ token = tokenFromRequest(request) if token is None: raise MatrixRestError(401, "M_UNAUTHORIZED", "Unauthorized") accountStore = AccountStore(sydent) account = accountStore.getAccountByToken(token) if account is None: raise MatrixRestError(401, "M_UNAUTHORIZED", "Unauthorized") if requireTermsAgreed: terms = get_terms(sydent) if (terms.getMasterVersion() is not None and account.consentVersion != terms.getMasterVersion()): raise MatrixRestError(403, "M_TERMS_NOT_SIGNED", "Terms not signed") return account
def render_POST(self, request: Request) -> JsonDict: send_cors(request) if self.require_auth: authV2(self.sydent, request) args = get_args(request, ("private_key", "token", "mxid")) private_key_base64 = args["private_key"] token = args["token"] mxid = args["mxid"] sender = self.tokenStore.getSenderForToken(token) if sender is None: raise MatrixRestError(404, "M_UNRECOGNIZED", "Didn't recognize token") to_sign = { "mxid": mxid, "sender": sender, "token": token, } try: private_key = signedjson.key.decode_signing_key_base64( "ed25519", "0", private_key_base64 ) signed: JsonDict = signedjson.sign.sign_json( to_sign, self.server_name, private_key ) except Exception: logger.exception("signing failed") raise MatrixRestError(500, "M_UNKNOWN", "Internal Server Error") return signed
def authIfV2(sydent, request, requireTermsAgreed=True): """For v2 APIs check that the request has a valid access token associated with it :returns Account|None: The account object if there is correct auth, or None for v1 APIs :raises MatrixRestError: If the request is v2 but could not be authed or the user has not accepted terms """ if request.path.startswith('/_matrix/identity/v2'): token = tokenFromRequest(request) if token is None: raise MatrixRestError(401, "M_UNAUTHORIZED", "Unauthorized") accountStore = AccountStore(sydent) account = accountStore.getAccountByToken(token) if account is None: raise MatrixRestError(401, "M_UNAUTHORIZED", "Unauthorized") if requireTermsAgreed: terms = get_terms(sydent) if (terms.getMasterVersion() is not None and account.consentVersion != terms.getMasterVersion()): raise MatrixRestError(403, "M_TERMS_NOT_SIGNED", "Terms not signed") return account return None
def render_POST(self, request: Request) -> JsonDict: send_cors(request) account = None if self.require_auth: account = authV2(self.sydent, request) args = get_args(request, ("sid", "client_secret", "mxid")) sid = args["sid"] mxid = args["mxid"] clientSecret = args["client_secret"] if not is_valid_client_secret(clientSecret): raise MatrixRestError(400, "M_INVALID_PARAM", "Invalid client_secret provided") if account: # This is a v2 API so only allow binding to the logged in user id if account.userId != mxid: raise MatrixRestError( 403, "M_UNAUTHORIZED", "This user is prohibited from binding to the mxid", ) try: valSessionStore = ThreePidValSessionStore(self.sydent) s = valSessionStore.getValidatedSession(sid, clientSecret) except (IncorrectClientSecretException, InvalidSessionIdException): # Return the same error for not found / bad client secret otherwise # people can get information about sessions without knowing the # secret. raise MatrixRestError( 404, "M_NO_VALID_SESSION", "No valid session was found matching that sid and client secret", ) except SessionExpiredException: raise MatrixRestError( 400, "M_SESSION_EXPIRED", "This validation session has expired: call requestToken again", ) except SessionNotValidatedException: raise MatrixRestError( 400, "M_SESSION_NOT_VALIDATED", "This validation session has not yet been completed", ) res = self.sydent.threepidBinder.addBinding(s.medium, s.address, mxid) return res
def render_POST(self, request): """ Bulk-lookup for threepids. Params: 'threepids': list of threepids, each of which is a list of medium, address Returns: Object with key 'threepids', which is a list of results where each result is a 3 item list of medium, address, mxid Note that results are not streamed to the client. Threepids for which no mapping is found are omitted. """ send_cors(request) authIfV2(self.sydent, request) args = get_args(request, ('threepids', )) threepids = args['threepids'] if not isinstance(threepids, list): raise MatrixRestError(400, 'M_INVALID_PARAM', 'threepids must be a list') logger.info("Bulk lookup of %d threepids", len(threepids)) globalAssocStore = GlobalAssociationStore(self.sydent) results = globalAssocStore.getMxids(threepids) return {'threepids': results}
def render_POST(self, request: Request) -> JsonDict: """ Mark a set of terms and conditions as having been agreed to """ send_cors(request) account = authV2(self.sydent, request, False) args = get_args(request, ("user_accepts", )) user_accepts = args["user_accepts"] terms = get_terms(self.sydent) unknown_urls = list(set(user_accepts) - terms.getUrlSet()) if len(unknown_urls) > 0: raise MatrixRestError( 400, "M_UNKNOWN", "Unrecognised URLs: %s" % (", ".join(unknown_urls), )) termsStore = TermsStore(self.sydent) termsStore.addAgreedUrls(account.userId, user_accepts) all_accepted_urls = termsStore.getAgreedUrls(account.userId) if terms.urlListIsSufficient(all_accepted_urls): accountStore = AccountStore(self.sydent) accountStore.setConsentVersion(account.userId, terms.getMasterVersion()) return {}
def render_POST(self, request): send_cors(request) account = authIfV2(self.sydent, request) args = get_args(request, ('sid', 'client_secret', 'mxid')) sid = args['sid'] mxid = args['mxid'] clientSecret = args['client_secret'] if not is_valid_client_secret(clientSecret): request.setResponseCode(400) return { 'errcode': 'M_INVALID_PARAM', 'error': 'Invalid client_secret provided' } # Return the same error for not found / bad client secret otherwise people can get information about # sessions without knowing the secret noMatchError = { 'errcode': 'M_NO_VALID_SESSION', 'error': "No valid session was found matching that sid and client secret" } if account: # This is a v2 API so only allow binding to the logged in user id if account.userId != mxid: raise MatrixRestError( 403, 'M_UNAUTHORIZED', "This user is prohibited from binding to the mxid") try: valSessionStore = ThreePidValSessionStore(self.sydent) s = valSessionStore.getValidatedSession(sid, clientSecret) except IncorrectClientSecretException: return noMatchError except SessionExpiredException: return { 'errcode': 'M_SESSION_EXPIRED', 'error': "This validation session has expired: call requestToken again" } except InvalidSessionIdException: return noMatchError except SessionNotValidatedException: return { 'errcode': 'M_SESSION_NOT_VALIDATED', 'error': "This validation session has not yet been completed" } res = self.sydent.threepidBinder.addBinding(s.medium, s.address, mxid) return res
def authIfV2(sydent, request, requireTermsAgreed=True): """For v2 APIs check that the request has a valid access token associated with it :param sydent: The Sydent instance to use. :type sydent: sydent.sydent.Sydent :param request: The request to look for an access token in. :type request: twisted.web.server.Request :param requireTermsAgreed: Whether to deny authentication if the user hasn't accepted the terms of service. :returns Account|None: The account object if there is correct auth, or None for v1 APIs. :raises MatrixRestError: If the request is v2 but could not be authed or the user has not accepted terms. """ if request.path.startswith(b'/_matrix/identity/v2'): token = tokenFromRequest(request) if token is None: raise MatrixRestError(401, "M_UNAUTHORIZED", "Unauthorized") accountStore = AccountStore(sydent) account = accountStore.getAccountByToken(token) if account is None: raise MatrixRestError(401, "M_UNAUTHORIZED", "Unauthorized") if requireTermsAgreed: terms = get_terms(sydent) if ( terms.getMasterVersion() is not None and account.consentVersion != terms.getMasterVersion() ): raise MatrixRestError(403, "M_TERMS_NOT_SIGNED", "Terms not signed") return account return None
def render_POST(self, request: Request) -> JsonDict: """ Invalidate the given access token """ send_cors(request) authV2(self.sydent, request, False) token = tokenFromRequest(request) if token is None: raise MatrixRestError(400, "M_MISSING_PARAMS", "Missing token") accountStore = AccountStore(self.sydent) accountStore.delToken(token) return {}
def render_POST(self, request): peerCert = request.transport.getPeerCertificate() peerCertCn = peerCert.get_subject().commonName peerStore = PeerStore(self.sydent) peer = peerStore.getPeerByName(peerCertCn) if not peer: logger.warn( "Got connection from %s but no peer found by that name", peerCertCn) raise MatrixRestError(403, 'M_UNKNOWN_PEER', 'This peer is not known to this server') logger.info("Push connection made from peer %s", peer.servername) if not request.requestHeaders.hasHeader('Content-Type') or \ request.requestHeaders.getRawHeaders('Content-Type')[0] != 'application/json': logger.warn( "Peer %s made push connection with non-JSON content (type: %s)", peer.servername, request.requestHeaders.getRawHeaders('Content-Type')[0]) raise MatrixRestError(400, 'M_NOT_JSON', 'This endpoint expects JSON') try: # json.loads doesn't allow bytes in Python 3.5 inJson = json.loads(request.content.read().decode("UTF-8")) except ValueError: logger.warn("Peer %s made push connection with malformed JSON", peer.servername) raise MatrixRestError(400, 'M_BAD_JSON', 'Malformed JSON') if 'sgAssocs' not in inJson: logger.warn( "Peer %s made push connection with no 'sgAssocs' key in JSON", peer.servername) raise MatrixRestError(400, 'M_BAD_JSON', 'No "sgAssocs" key in JSON') failedIds = [] globalAssocsStore = GlobalAssociationStore(self.sydent) # Ensure items are pulled out of the dictionary in order of origin_id. sg_assocs = inJson.get('sgAssocs', {}) sg_assocs = sorted(sg_assocs.items(), key=lambda k: int(k[0])) for originId, sgAssoc in sg_assocs: try: peer.verifySignedAssociation(sgAssoc) logger.debug( "Signed association from %s with origin ID %s verified", peer.servername, originId) # Don't bother adding if one has already failed: we add all of them or none so # we're only going to roll back the transaction anyway (but we continue to try # & verify the rest so we can give a complete list of the ones that don't # verify) if len(failedIds) > 0: continue assocObj = threePidAssocFromDict(sgAssoc) if assocObj.mxid is not None: # Calculate the lookup hash with our own pepper for this association str_to_hash = u' '.join([ assocObj.address, assocObj.medium, self.hashing_store.get_lookup_pepper() ], ) assocObj.lookup_hash = sha256_and_url_safe_base64( str_to_hash) # Add this association globalAssocsStore.addAssociation(assocObj, json.dumps(sgAssoc), peer.servername, originId, commit=False) else: logger.info( "Incoming deletion: removing associations for %s / %s", assocObj.medium, assocObj.address) globalAssocsStore.removeAssociation( assocObj.medium, assocObj.address) logger.info("Stored association origin ID %s from %s", originId, peer.servername) except: failedIds.append(originId) logger.warn( "Failed to verify signed association from %s with origin ID %s", peer.servername, originId) twisted.python.log.err() if len(failedIds) > 0: self.sydent.db.rollback() request.setResponseCode(400) return { 'errcode': 'M_VERIFICATION_FAILED', 'error': 'Verification failed for one or more associations', 'failed_ids': failedIds } else: self.sydent.db.commit() return {'success': True}
def render_POST(self, request: Request) -> JsonDict: # Cast safety: This request has an ISSLTransport because this servlet # is a resource under the ReplicationHttpsServer and nowhere else. request.transport = cast(ISSLTransport, request.transport) peerCert = cast(X509, request.transport.getPeerCertificate()) peerCertCn = peerCert.get_subject().commonName peerStore = PeerStore(self.sydent) peer = peerStore.getPeerByName(peerCertCn) if not peer: logger.warning( "Got connection from %s but no peer found by that name", peerCertCn) raise MatrixRestError(403, "M_UNKNOWN_PEER", "This peer is not known to this server") logger.info("Push connection made from peer %s", peer.servername) if (not request.requestHeaders.hasHeader("Content-Type") # Type safety: the hasHeader call returned True, so getRawHeaders() # returns a nonempty list. or request.requestHeaders.getRawHeaders("Content-Type")[ 0] # type: ignore[index] != "application/json"): logger.warning( "Peer %s made push connection with non-JSON content (type: %s)", peer.servername, # Type safety: the hasHeader call returned True, so getRawHeaders() # returns a nonempty list. request.requestHeaders.getRawHeaders("Content-Type") [0], # type: ignore[index] ) raise MatrixRestError(400, "M_NOT_JSON", "This endpoint expects JSON") try: # json.loads doesn't allow bytes in Python 3.5 inJson = json_decoder.decode( request.content.read().decode("UTF-8")) except ValueError: logger.warning("Peer %s made push connection with malformed JSON", peer.servername) raise MatrixRestError(400, "M_BAD_JSON", "Malformed JSON") if "sgAssocs" not in inJson: logger.warning( "Peer %s made push connection with no 'sgAssocs' key in JSON", peer.servername, ) raise MatrixRestError(400, "M_BAD_JSON", 'No "sgAssocs" key in JSON') failedIds: List[int] = [] globalAssocsStore = GlobalAssociationStore(self.sydent) # Ensure items are pulled out of the dictionary in order of origin_id. sg_assocs_raw: SignedAssociations = inJson.get("sgAssocs", {}) sg_assocs = sorted(sg_assocs_raw.items(), key=lambda k: int(k[0])) for originId, sgAssoc in sg_assocs: try: peer.verifySignedAssociation(sgAssoc) logger.debug( "Signed association from %s with origin ID %s verified", peer.servername, originId, ) # Don't bother adding if one has already failed: we add all of them or none so # we're only going to roll back the transaction anyway (but we continue to try # & verify the rest so we can give a complete list of the ones that don't # verify) if len(failedIds) > 0: continue assocObj = threePidAssocFromDict(sgAssoc) # ensure we are casefolding email addresses before hashing/storing assocObj.address = normalise_address(assocObj.address, assocObj.medium) if assocObj.mxid is not None: # Calculate the lookup hash with our own pepper for this association pepper = self.hashing_store.get_lookup_pepper() assert pepper is not None str_to_hash = " ".join( [assocObj.address, assocObj.medium, pepper], ) assocObj.lookup_hash = sha256_and_url_safe_base64( str_to_hash) # Add this association globalAssocsStore.addAssociation( assocObj, json.dumps(sgAssoc), peer.servername, originId, commit=False, ) else: logger.info( "Incoming deletion: removing associations for %s / %s", assocObj.medium, assocObj.address, ) globalAssocsStore.removeAssociation( assocObj.medium, assocObj.address) logger.info("Stored association origin ID %s from %s", originId, peer.servername) except Exception: failedIds.append(originId) logger.warning( "Failed to verify signed association from %s with origin ID %s", peer.servername, originId, ) twisted.python.log.err() if len(failedIds) > 0: self.sydent.db.rollback() request.setResponseCode(400) return { "errcode": "M_VERIFICATION_FAILED", "error": "Verification failed for one or more associations", "failed_ids": failedIds, } else: self.sydent.db.commit() return {"success": True}
def render_POST(self, request: Request) -> JsonDict: send_cors(request) args = get_args( request, ( "medium", "address", "room_id", "sender", ), ) medium = args["medium"] address = args["address"] roomId = args["room_id"] sender = args["sender"] # ensure we are casefolding email address before storing normalised_address = normalise_address(address, medium) verified_sender = None if self.require_auth: account = authV2(self.sydent, request) verified_sender = sender if account.userId != sender: raise MatrixRestError(403, "M_UNAUTHORIZED", "'sender' doesn't match") globalAssocStore = GlobalAssociationStore(self.sydent) mxid = globalAssocStore.getMxid(medium, normalised_address) if mxid: request.setResponseCode(400) return { "errcode": "M_THREEPID_IN_USE", "error": "Binding already known", "mxid": mxid, } if medium != "email": request.setResponseCode(400) return { "errcode": "M_UNRECOGNIZED", "error": "Didn't understand medium '%s'" % (medium, ), } if not (0 < len(address) <= MAX_EMAIL_ADDRESS_LENGTH): request.setResponseCode(400) return { "errcode": "M_INVALID_PARAM", "error": "Invalid email provided" } token = self._randomString(128) tokenStore = JoinTokenStore(self.sydent) ephemeralPrivateKey = nacl.signing.SigningKey.generate() ephemeralPublicKey = ephemeralPrivateKey.verify_key ephemeralPrivateKeyBase64 = encode_base64(ephemeralPrivateKey.encode(), True) ephemeralPublicKeyBase64 = encode_base64(ephemeralPublicKey.encode(), True) tokenStore.storeEphemeralPublicKey(ephemeralPublicKeyBase64) tokenStore.storeToken(medium, normalised_address, roomId, sender, token) # Variables to substitute in the template. substitutions = {} # Include all arguments sent via the request. for k, v in args.items(): if isinstance(v, str): substitutions[k] = v substitutions["token"] = token # Substitutions that the template requires, but are optional to provide # to the API. extra_substitutions = [ "sender_display_name", "token", "room_name", "bracketed_room_name", "room_avatar_url", "sender_avatar_url", "guest_user_id", "guest_access_token", ] for k in extra_substitutions: substitutions.setdefault(k, "") # For MSC3288 room type, prefer the stable field, but fallback to the # unstable field. if "room_type" not in substitutions: substitutions["room_type"] = substitutions.get( "org.matrix.msc3288.room_type", "") substitutions["bracketed_verified_sender"] = "" if verified_sender: substitutions["bracketed_verified_sender"] = "(%s) " % ( verified_sender, ) substitutions["ephemeral_private_key"] = ephemeralPrivateKeyBase64 if substitutions["room_name"] != "": substitutions[ "bracketed_room_name"] = "(%s) " % substitutions["room_name"] substitutions[ "web_client_location"] = self.sydent.config.email.default_web_client_location if "org.matrix.web_client_location" in substitutions: substitutions["web_client_location"] = substitutions[ "org.matrix.web_client_location"] if substitutions["room_type"] == "m.space": subject = self.sydent.config.email.invite_subject_space % substitutions else: subject = self.sydent.config.email.invite_subject % substitutions substitutions["subject_header_value"] = Header(subject, "utf8").encode() brand = self.sydent.brand_from_request(request) # self.sydent.config.email.invite_template is deprecated if self.sydent.config.email.invite_template is None: templateFile = self.sydent.get_branded_template( brand, "invite_template.eml", ) else: templateFile = self.sydent.config.email.invite_template try: sendEmail(self.sydent, templateFile, normalised_address, substitutions) except EmailAddressException: request.setResponseCode(HTTPStatus.BAD_REQUEST) return { "errcode": "M_INVALID_EMAIL", "error": "Invalid email address" } pubKey = self.sydent.keyring.ed25519.verify_key pubKeyBase64 = encode_base64(pubKey.encode()) baseUrl = "%s/_matrix/identity/api/v1" % ( self.sydent.config.http.server_http_url_base, ) keysToReturn = [] keysToReturn.append({ "public_key": pubKeyBase64, "key_validity_url": baseUrl + "/pubkey/isvalid", }) keysToReturn.append({ "public_key": ephemeralPublicKeyBase64, "key_validity_url": baseUrl + "/pubkey/ephemeral/isvalid", }) resp = { "token": token, "public_key": pubKeyBase64, "public_keys": keysToReturn, "display_name": self.redact_email_address(address), } return resp