def validate_number(phone_number): """ Validate a phone number, converting it to E.164 format. """ raise UnauthorizedException() # Disable for now. logger.debug("in core.api.users.validate_number(" + "phone_number=%s)" % repr(phone_number)) if phone_number == None: raise InvalidParametersException() return utils.format_phone_number(phone_number)
def phone_number_exists(phone_number): """ Return True iff the given phone number exists. """ raise UnauthorizedException() # Disable for now. logger.debug("in core.api.users.phone_number_exists(" + "phone_number=%s)" % repr(phone_number)) if phone_number == None: raise InvalidParametersException() else: phone_number = utils.format_phone_number(phone_number) return User.objects.filter(phone_number=phone_number).exists()
def test_send_verification_code(self): """ Test users/send_verification_code. """ # Create a user with a phone number, for testing. formatted_number = utils.format_phone_number(PHONE_NUMBER) User.objects.filter(phone_number=formatted_number).delete() user = users.create(phone_number=PHONE_NUMBER) # Set up a signal listener to check that the verification message is # being sent via Twilio. self.twilio_sms_sent = False # initially. self.twilio_from_phone = None # ditto. self.twilio_to_phone = None # ditto. def twilio_signal_handler(sender, **kwargs): self.twilio_sms_sent = True self.twilio_from_phone = kwargs.get("from_phone") self.twilio_to_phone = kwargs.get("to_phone") signals.twilio_sms_sent.connect(twilio_signal_handler) # Ask the user API to send the verification code. Note that we disable # Twilio so that no actual SMS is sent. with self.settings(ENABLE_TWILIO=False): users.send_verification_code(user_id=user['id']) # Check that the Twilio gateway sent the verification message. self.assertTrue(self.twilio_sms_sent) self.assertEqual(self.twilio_from_phone, settings.SYSTEM_PHONE_NUMBER) self.assertEqual(self.twilio_to_phone, formatted_number) # Run the test again, this time supplying the phone number rather than # the user ID. self.twilio_sms_sent = False with self.settings(ENABLE_TWILIO=False): users.send_verification_code(phone_number=PHONE_NUMBER) self.assertTrue(self.twilio_sms_sent) # Finally, clean everything up. signals.twilio_sms_sent.disconnect(twilio_signal_handler)
def verify_user_owns_number(session, phone_number, verification_code): """ Verify that the currently logged-in user owns the given phone number. """ raise UnauthorizedException() # Disable for now. logger.debug("in core.api.users.verify_user_owns_number(" + "session=%s, phone_number=%s, verification_code=%s)" % (repr(session), repr(phone_number), repr(verification_code))) if session == None or phone_number == None or verification_code == None: raise InvalidParametersException() sessionHandler.validate(session) user = sessionHandler.get_user(session) phone_number = utils.format_phone_number(phone_number) try: user_with_phone = User.objects.get(phone_number=phone_number) except User.DoesNotExist: user_with_phone = None if user_with_phone == None: raise NoSuchPhoneNumberException() if verification_code != user_with_phone.verification_code: raise UnauthorizedException() if user == user_with_phone: # The user already has this phone number. Simply verify it. user.verified = True user.save() return # If we get here, the phone number was originally owned by someone else. # Transfer the phone number over to the currently logged-in user. user.phone_number = user_with_phone.phone_number user.verification_code = user_with_phone.verification_code user.verified = True user.save() user_with_phone.phone_number = None user_with_phone.verification_code = None user_with_phone.verified = False user_with_phone.save()
def send_verification_code(user_id=None, phone_number=None): """ Calculate and send out a verification code to a phone number. """ raise UnauthorizedException() # Disable for now. logger.debug("in core.api.users.send_verification_code(" + "user_id=%s, phone_number=%s)" % (repr(user_id), repr(phone_number))) # Get the user to send the verification message to. if user_id != None: try: user = User.objects.get(id=user_id) except User.DoesNotExist: raise NoSuchUserException() elif phone_number != None: phone_number = utils.format_phone_number(phone_number) try: user = User.objects.get(phone_number=phone_number) except User.DoesNotExist: raise NoSuchPhoneNumberException() else: raise InvalidParametersException() # Check that the user has a phone number. if user.phone_number == None: raise NoPhoneNumberException() # Calculate a new verification code for this user. user.verification_code = utils.random_digits(min_length=4, max_length=4) user.save() # Finally, send the verification code to the user's phone as an SMS # message. message = settings.SMS_TEXT_VERIFICATION_CODE % user.verification_code twilio_gateway.send_sms(from_phone=settings.SYSTEM_PHONE_NUMBER, to_phone=user.phone_number, sms_message=message)
def test_phone_number_exists(self): """ Test users/phone_number_exists. """ # Create a user with a phone number. username = utils.random_username() password = utils.random_password() formatted_number = utils.format_phone_number(PHONE_NUMBER) User.objects.filter(phone_number=formatted_number).delete() user = users.create(phone_number=PHONE_NUMBER) # Check that the phone number can be found. self.assertTrue(users.phone_number_exists(PHONE_NUMBER)) # Check that a different phone number can't be found. self.assertFalse(users.phone_number_exists(PHONE_NUMBER+"123"))
def test_create(self): """ Test users/create. """ # Test the process of creating a user without any details. user = users.create() self.assertItemsEqual(user.keys(), ["id", "ad_hoc", "verified", "created_at", "updated_at"]) self.assertTrue(user['ad_hoc']) # Test the process of creating a user with a username and password. user = users.create(username=utils.random_username(), password=utils.random_password()) self.assertItemsEqual(user.keys(), ["id", "ad_hoc", "username", "verified", "created_at", "updated_at"]) self.assertFalse(user['ad_hoc']) # Check that a default topic has been created for the new user. has_default_topic = False for topic in Topic.objects.filter(user_id=user['id']): if topic.default: has_default_topic = True break self.assertTrue(has_default_topic) # Check that we can't create a user with an invalid username. with self.assertRaises(InvalidUsernameException): user = users.create(username="******", password="******") # Check that we can't create a user with an invalid password. with self.assertRaises(InvalidPasswordException): user = users.create(username=utils.random_username(), password="******") # Check that we can't create a user with a username but no password. with self.assertRaises(InvalidParametersException): user = users.create(username=utils.random_username(), password=None) # Check that we can't create a user with a password but not username. with self.assertRaises(InvalidParametersException): user = users.create(username=None, password=utils.random_password()) # Check that we can create a user with a valid phone number, and that # the phone number is converted to E.164 standard. formatted_number = utils.format_phone_number(PHONE_NUMBER) User.objects.filter(phone_number=formatted_number).delete() user = users.create(phone_number=PHONE_NUMBER) self.assertEqual(user['phone_number'], formatted_number) # Check that we can't create a second user with the same number. with self.assertRaises(DuplicatePhoneNumberException): user = users.create(phone_number=PHONE_NUMBER) # Check that we can't create a user with an invalid phone number. with self.assertRaises(InvalidPhoneNumberException): user = users.create(phone_number="INVALID") # Check that we can't create two users with the same username. username = utils.random_username() password = utils.random_password() user1 = users.create(username=username, password=password) with self.assertRaises(DuplicateUsernameException): user2 = users.create(username=username, password=password)
def test_login(self): """ Test users/login. """ # Try logging in without any parameters. This should create a new ad # hoc user on-the-fly. session_token = users.login() self.assertTrue(users.get(session_token)['ad_hoc']) # Create an ad-hoc user, and log in using the user ID. user_id = users.create()['id'] session_token = users.login(user_id=user_id) # Create a user with a username, password and phone number, for testing. username = utils.random_username() password = utils.random_password() formatted_number = utils.format_phone_number(PHONE_NUMBER) User.objects.filter(phone_number=formatted_number).delete() user_id = users.create(username=username, password=password, phone_number=PHONE_NUMBER)['id'] # Create two random verification codes, making sure they're different. while True: code_1 = utils.random_letters(min_length=4, max_length=4) code_2 = utils.random_letters(min_length=4, max_length=4) if code_1 != code_2: break else: continue # Store the first verification code into the User object. user = User.objects.get(id=user_id) user.verification_code = code_1 user.verified = False user.save() # Attempt to log in using the supplied phone number and verification # code, deliberately using the wrong code. This should fail. with self.assertRaises(LoginRejectedException): session_token = users.login(phone_number=PHONE_NUMBER, verification_code=code_2) # Attempt to log in using the username and an incorrect password. Once # again, this should fail. with self.assertRaises(LoginRejectedException): session_token = users.login(username=username, password=password+"X") # Now try logging in with the correct username and password. This # should succeed. session_token = users.login(username=username, password=password) sessionHandler.validate(session_token) # Finally, try logging in again using the phone number and verification # code. This should not only log the user it, but also verify the # phone number. session_token = users.login(phone_number=PHONE_NUMBER, verification_code=code_1) sessionHandler.validate(session_token) user = User.objects.get(id=user_id) self.assertEqual(user.verified, True)
def update(session, username=None, password=None, phone_number=None): """ Update the details of the currently logged-in user. """ raise UnauthorizedException() # Disable for now. logger.debug("in core.api.users.update(" + "session=%s, username=%s, password=%s, phone_number=%s)" % (repr(session), repr(username), repr(password), repr(phone_number))) if session == None: raise InvalidParametersException() sessionHandler.validate(session) user = sessionHandler.get_user(session) # Remember if the user had a username or password. if user.username not in ["", None]: had_username = True else: had_username = False if user.password_salt not in ["", None]: had_password = True else: had_password = False # If we're setting a username and password for this user, and we didn't # have one previously, create a 3taps Identity for this user. Note that # this may fail, if the username is already in use. if not had_username and username != None: if password == None: raise InvalidParametersException() _check_username(username) _check_password(password) # Try creating this user within the 3taps Identity API. success,response = identity_api.create(username, password) if not success: if response.startswith("403 error"): raise DuplicateUsernameException() else: raise InvalidParametersException() # Check that we don't have a local user with that username. try: existing_user = User.objects.get(username__iexact=username) except User.DoesNotExist: existing_user = None if existing_user != None: raise DuplicateUsernameException() # Finally, save the updated user details into our database. salt = response['server_salt'] hash = hashlib.md5(password + salt).hexdigest() user.uses_identity_api = True user.username = username user.identity_api_salt = salt user.identity_api_hash = hash user.save() # If we're changing the username for this user, ask the 3taps Identity API # to change the username. Note that this may fail, if the new username is # already in use. if had_username and username != None and username != user.username: success,response = identity_api.login(user.username, pass_hash=user.identity_api_hash) if not success: raise UnauthorizedException() session = response success,response = identity_api.update(session, {'username' : username}) if not success: if response.startswith("403 error"): raise DuplicateUsernameException() else: raise InvalidParametersException() identity_api.logout(session) # Check that we don't have a local user with that username. try: existing_user = User.objects.get(username__iexact=username) except User.DoesNotExist: existing_user = None if existing_user != None: raise DuplicateUsernameException() # Finally, save the updated user details into our database. user.username = username user.save() # If we're changing the password for this user, ask the 3taps Identity API # to change the password. if password != None: if user.username in ["", None]: # We can't change the password if we don't have a username. raise InvalidParametersException() if user.uses_identity_api: success,response = \ identity_api.login(user.username, pass_hash=user.identity_api_hash) else: success,response = \ identity_api.login(user.username, password="******") if not success: raise UnauthorizedException() session = response success,response = identity_api.update(session, {'username' : username}) if not success: if response.startswith("403 error"): raise DuplicateUsernameException() else: raise InvalidParametersException() identity_api.logout(session) salt = response['server_salt'] hash = hashlib.md5(password + salt).hexdigest() user.uses_identity_api = True user.identity_api_salt = salt user.identity_api_hash = hash user.save() # If we've been asked to update the user's phone number, do so. # NOTE: someone was using this to hack our system, so I've disabled it. if False: # phone_number != None: if phone_number == "": user.phone_number = None # Remove current phone number. else: phone_number = utils.format_phone_number(phone_number) try: existing_user = User.objects.get(phone_number=phone_number) except User.DoesNotExist: existing_user = None if existing_user != None and user.id != existing_user.id: raise DuplicatePhoneNumberException() user.phone_number = phone_number # If this was an ad hoc user who we're now making permanent, change their # "ad hoc" status, and create a new default topic for the user. if user.ad_hoc and (username != None or password != None or phone_number != None): user.ad_hoc = False _create_default_topic(user) # If we have been given a username and password for this user, record them # as signing up. if not had_username and not had_password: if username not in ["", None] and password not in ["", None]: eventRecorder.record_event(eventRecorder.EVENT_TYPE_NEW_USER_SIGNUP) # Finally, save the updated user and return a copy of it back to the # caller. user.updated_at = datetime.datetime.utcnow() user.save() return user.to_dict()
def create(username=None, password=None, phone_number=None): """ Create a new User within the system. """ raise UnauthorizedException() # Disable for now. logger.debug("in core.api.users.create(" + "username=%s, password=%s, phone_number=%s)" % (repr(username), repr(password), repr(phone_number))) if username == "": username = None if password == "": password = None if phone_number == "": phone_number = None if username == None and password == None and phone_number == None: ad_hoc = True else: ad_hoc = False if username != None: _check_username(username) if password != None: _check_password(password) if username != None or password != None: if username == None or password == None: # username and password must both be set at the same time. raise InvalidParametersException() if phone_number != None: phone_number = utils.format_phone_number(phone_number) try: existing_user = User.objects.get(phone_number=phone_number) except User.DoesNotExist: existing_user = None if existing_user != None: raise DuplicatePhoneNumberException() if username != None: # The user is attempting to create a new user with a username and # password. Try to create the 3taps identity for this new user, and # raise a DuplicateUsernameException if the user already exists. success,response = identity_api.create(username, password) if not success: if response.startswith("403 error"): raise DuplicateUsernameException() else: raise InvalidParametersException() user = User() user.ad_hoc = ad_hoc user.username = username if username != None: salt = response['server_salt'] hash = hashlib.md5(password + salt).hexdigest() user.uses_identity_api = True user.identity_api_salt = salt user.identity_api_hash = hash else: user.uses_identity_api = False user.identity_api_hash = None user.identity_api_salt = None user.phone_number = phone_number user.verification_code = None user.verified = False user.created_at = datetime.datetime.utcnow() user.updated_at = datetime.datetime.utcnow() user.save() # If the new user has a username and password, record it as a new user # signup. if username != None and password != None: eventRecorder.record_event(eventRecorder.EVENT_TYPE_NEW_USER_SIGNUP) # While we're at it, create a default topic for the new user if they're not # an ad hoc user. if not ad_hoc: _create_default_topic(user) # Finally, return the new user's details to the caller. return user.to_dict()
def login(user_id=None, username=None, password=None, phone_number=None, verification_code=None): """ Log a user in, creating a new login session. """ raise UnauthorizedException() # Disable for now. logger.debug("in core.api.users.login(" + "user_id=%s, username=%s, password=%s, phone=%s, code=%s)" % (repr(user_id), repr(username), repr(password), repr(phone_number), repr(verification_code))) if user_id != None: try: user = User.objects.get(id=user_id) except User.DoesNotExist: raise NoSuchUserException() if not user.ad_hoc: raise UnauthorizedException() elif username != None and password != None: # Log in using a username and password. Note that we have to use the # 3taps identity API for this. try: user = User.objects.get(username__iexact=username) except User.DoesNotExist: # We don't know about this user, but that doesn't mean the user # doesn't exist. It could be that the user already has an identity # within the 3taps Identity API, but hasn't used MessageMe before. # See if the user exists in the identity API. success,response = identity_api.login(username=username, password=password) if success: identity_api.logout(response) # Create a MessageMe record for this user. salt = response['server_salt'] hash = hashlib.md5(password + salt).hexdigest() user = User() user.username = username user.uses_identity_api = True user.identity_api_salt = salt user.identity_api_hash = hash user.phone_number = phone_number user.verification_code = None user.verified = False user.created_at = datetime.datetime.utcnow() user.updated_at = datetime.datetime.utcnow() user.save() eventRecorder.record_event( eventRecorder.EVENT_TYPE_NEW_USER_SIGNUP) _create_default_topic(user) else: # We don't know about this user, and the 3taps Identity API # doesn't either -> give up. raise LoginRejectedException() if user.uses_identity_api: # Ask the 3taps Identity API to validate the supplied username and # password. salt = user.identity_api_salt hash = hashlib.md5(password + salt).hexdigest() success,response = identity_api.login(username=username, pass_hash=hash) if success: identity_api.logout(response) # Update the password hash, just in case the user changed their # password. salt = response['server_salt'] hash = hashlib.md5(password + salt).hexdigest() user.identity_api_salt = salt user.identity_api_hash = hash user.save() else: raise LoginRejectedException() else: # We haven't yet migrated this user over to the identity API. We # first check the password against the old password hash to see if # it matches our private user details. hash = bcrypt.hashpw(password, user.password_salt) if hash == user.password_hash: # We know the user entered the correct password. Try logging # in with the "mm_temp" password, and if this works update the # user's details to use the supplied (real) password. success,response = identity_api.login(username=user.username, password="******") if success: session = response ignore = identity_api.update(session, {'password' : password}) identity_api.logout(session) salt = response['server_salt'] hash = hashlib.md5(password + salt).hexdigest() user.uses_identity_api = True user.identity_api_salt = salt user.identity_api_hash = hash user.save() else: # The user may have updated their password using another # client of the 3taps Identity API. Try logging in using # the supplied password. success,response = \ identity_api.login(username=user.username, password=password) if success: session = response identity_api.logout(session) salt = response['server_salt'] hash = hashlib.md5(password + salt).hexdigest() user.uses_identity_api = True user.identity_api_salt = salt user.identity_api_hash = hash user.save() else: # We can't log in -> give up. raise LoginRejectedException() else: # The supplied password doesn't match our local records. All # we can do is hope the password is accepted by the 3taps # Identity API, and if so accept the login. success,response = identity_api.login(username=user.username, password=password) if success: session = response identity_api.logout(session) salt = response['server_salt'] hash = hashlib.md5(password + salt).hexdigest() user.uses_identity_api = True user.identity_api_salt = salt user.identity_api_hash = hash user.save() else: # We can't log in -> give up. raise LoginRejectedException() elif phone_number != None and verification_code != None: phone_number = utils.format_phone_number(phone_number) try: user = User.objects.get(phone_number=phone_number, verification_code=verification_code) except User.DoesNotExist: raise LoginRejectedException() user.verified = True user.save() elif (user_id == None and username == None and password == None and phone_number == None and verification_code == None): # Create a new ad hoc user on-the-fly for this session. user = User() user.ad_hoc = True user.username = None user.password_salt = None user.password_hash = None user.phone_number = None user.verification_code = None user.verified = False user.created_at = datetime.datetime.utcnow() user.updated_at = datetime.datetime.utcnow() user.save() else: raise InvalidParametersException() return sessionHandler.create(user)
def receive_sms(from_phone, to_phone, sms_message): """ Respond to an incoming SMS message from Twilio. 'from_phone' is the phone number the SMS message was sent from, 'to_phone' is the phone number the message was sent to, and 'sms_message' is the text of the incoming SMS message. We process the SMS message, attempting to link it to an existing SMS channel so we can identify the recipient of the message. Upon completion, we return a string to use as a reply to send back to the 'from_phone', or None if no reply should be sent. Note that we raise an InvalidPhoneNumberException if either phone number cannot be parsed. """ logger.debug('twilio.receive_sms(' + 'from_phone="%s", to_phone="%s", sms_message="%s")' % (from_phone, to_phone, sms_message)) # Convert the phone numbers to E.164 format. from_phone = utils.format_phone_number(from_phone) to_phone = utils.format_phone_number(to_phone) # Find the User record associated with the sender's phone number. try: sender = User.objects.get(phone_number=from_phone) except User.DoesNotExist: return settings.SMS_TEXT_UNKNOWN_USER # Find an SMS channel which uses the phone number that the message was sent # to, and which was sending messages to the given user. possible_channels = TwilioSMSChannel.objects.filter( phone_number__number=to_phone, sender=sender) if len(possible_channels) == 0: return settings.SMS_TEXT_CONVERSATION_CLOSED if len(possible_channels) > 1: return settings.SMS_TEXT_UNABLE_TO_PROCESS_REPLY # Should never happen. found_channel = possible_channels[0] # If we get here, we've found the SMS channel that was sending messages to # this user. This tells us the conversation this message was about, and # hence the topic. conversation = found_channel.conversation topic = conversation.topic # If the conversation has been stopped, don't try to forward the reply. # Instead, return an appropriate message back to the sender. if conversation.stopped: return settings.SMS_TEXT_CONVERSATION_CLOSED # Calculate the recipient for the reply. This is the other party in the # conversation. if sender == conversation.user_1: recipient = conversation.user_2 else: recipient = conversation.user_1 # If we're going to be forwarding this message via SMS, check that the # recipient hasn't reached their rate limit. if recipient.phone_number not in [None, ""] and recipient.verified: if not rateLimiter.is_phone_number_below_rate_limit( recipient.phone_number): return settings.SMS_TEXT_RATE_LIMIT_EXCEEDED # Process the incoming SMS. from messageme.core.lib import messageHandler if not messageHandler.handle_special_message(sender, recipient, topic, sms_message): # Pass an ordinary SMS message on to the recipient. msg = messageHandler.send_message(sender, recipient, topic, None, sms_message) return None # Don't send a message back to the 'from_phone' number.