def reset_password(username): """ Reset a user's password using their secret answers. :rtype: flask.Response """ req_json = request.get_json(force=True) _validate_api_input(req_json, 'answers', list) _validate_api_input(req_json, 'new_password', string_types) answers = req_json['answers'] new_password = req_json['new_password'] not_setup_msg = ('You must have configured at least {0} secret answers before resetting your ' 'password').format(current_app.config['REQUIRED_ANSWERS']) # Verify the user exists in the database ad = adreset.ad.AD() ad.service_account_login() user_id = User.get_id_from_ad_username(username, ad) if not user_id: msg = 'The user attempted a password reset but does not exist in the database' log.debug({'message': msg, 'user': username}) raise ValidationError(not_setup_msg) # Make sure the user isn't locked out if User.is_user_locked_out(user_id): msg = 'The user attempted a password reset but their account is locked in ADReset' log.info({'message': msg, 'user': username}) raise Unauthorized('Your account is locked. Please try again later.') db_answers = Answer.query.filter_by(user_id=user_id).all() # Create a dictionary of question_id to answer from entries in the database. This will avoid # the need to continuously loop through these answers looking for specific answers later on. q_id_to_answer_db = {} for answer in db_answers: q_id_to_answer_db[answer.question_id] = answer.answer # Make sure the user has all their answers configured if len(q_id_to_answer_db.keys()) != current_app.config['REQUIRED_ANSWERS']: msg = ('The user did not have their secret answers configured and attempted to reset their ' 'password') log.debug({'message': msg, 'user': username}) raise ValidationError(not_setup_msg) seen_question_ids = set() for answer in answers: if not isinstance(answer, dict) or 'question_id' not in answer or 'answer' not in answer: raise ValidationError( 'The answers must be an object with the keys "question_id" and "answer"') _validate_api_input(answer, 'question_id', int) _validate_api_input(answer, 'answer', string_types) if answer['question_id'] not in q_id_to_answer_db: msg = ('The user answered a question they did not previously configure while ' 'attempting to reset their password') log.info({'message': msg, 'user': username}) raise ValidationError( 'One of the answers was to a question that wasn\'t previously configured') # Don't allow an attacker to enter in the same question and answer combination more than # once if answer['question_id'] in seen_question_ids: msg = ('The user answered the same question multiple times while attempting to reset ' 'their password') log.info({'message': msg, 'user': username}) raise ValidationError('You must answer {0} different questions'.format( current_app.config['REQUIRED_ANSWERS'])) seen_question_ids.add(answer['question_id']) # Only check if the answers are correct after knowing the input is valid as to not give away # any hints as to which answer is incorrect for an attacker for answer in answers: if current_app.config['CASE_SENSITIVE_ANSWERS'] is True: input_answer = answer['answer'] else: input_answer = answer['answer'].lower() is_correct_answer = Answer.verify_answer( input_answer, q_id_to_answer_db[answer['question_id']]) if is_correct_answer is not True: log.info({'message': 'The user entered an incorrect answer', 'user': username}) failed_attempt = FailedAttempt(user_id=user_id, time=datetime.utcnow()) db.session.add(failed_attempt) db.session.commit() if User.is_user_locked_out(user_id): msg = 'The user failed too many password reset attempts. They are now locked out.' log.info({'message': msg, 'user': username}) raise Unauthorized('You have answered incorrectly too many times. Your account is ' 'now locked. Please try again later.') raise Unauthorized('One or more answers were incorrect. Please try again.') log.debug({'message': 'The user successfully answered their questions', 'user': username}) ad.reset_password(username, new_password) log.info({'message': 'The user successfully reset their password', 'user': username}) return jsonify({}), 204
def reset_password(): """ Reset a user's password using their secret answers. :rtype: flask.Response """ req_json = request.get_json(force=True) _validate_api_input(req_json, 'answers', list) _validate_api_input(req_json, 'new_password', string_types) _validate_api_input(req_json, 'username', string_types) answers = req_json['answers'] new_password = req_json['new_password'] username = req_json['username'] not_setup_msg = ( 'You must have configured at least {0} secret answers before resetting your ' 'password').format(current_app.config['REQUIRED_ANSWERS']) # Verify the user exists in the database ad = adreset.ad.AD() ad.service_account_login() user_id = User.get_id_from_ad_username(username, ad) if not user_id: msg = 'The user attempted a password reset but does not exist in the database' log.debug({'message': msg, 'user': username}) raise ValidationError(not_setup_msg) # Make sure the user isn't locked out if User.is_user_locked_out(user_id): msg = 'The user attempted a password reset but their account is locked in ADReset' log.info({'message': msg, 'user': username}) raise Unauthorized('Your account is locked. Please try again later.') db_answers = Answer.query.filter_by(user_id=user_id).all() # Create a dictionary of question_id to answer from entries in the database. This will avoid # the need to continuously loop through these answers looking for specific answers later on. q_id_to_answer_db = {} for answer in db_answers: q_id_to_answer_db[answer.question_id] = answer.answer # Make sure the user has all their answers configured if len(q_id_to_answer_db.keys()) != current_app.config['REQUIRED_ANSWERS']: msg = ( 'The user did not have their secret answers configured and attempted to reset their ' 'password') log.debug({'message': msg, 'user': username}) raise ValidationError(not_setup_msg) seen_question_ids = set() for answer in answers: if not isinstance( answer, dict) or 'question_id' not in answer or 'answer' not in answer: raise ValidationError( 'The answers must be an object with the keys "question_id" and "answer"' ) _validate_api_input(answer, 'question_id', int) _validate_api_input(answer, 'answer', string_types) if answer['question_id'] not in q_id_to_answer_db: msg = ( 'The user answered a question they did not previously configure while ' 'attempting to reset their password') log.info({'message': msg, 'user': username}) raise ValidationError( 'One of the answers was to a question that wasn\'t previously configured' ) # Don't allow an attacker to enter in the same question and answer combination more than # once if answer['question_id'] in seen_question_ids: msg = ( 'The user answered the same question multiple times while attempting to reset ' 'their password') log.info({'message': msg, 'user': username}) raise ValidationError( 'You must answer {0} different questions'.format( current_app.config['REQUIRED_ANSWERS'])) seen_question_ids.add(answer['question_id']) # Only check if the answers are correct after knowing the input is valid as to not give away # any hints as to which answer is incorrect for an attacker for answer in answers: if current_app.config['CASE_SENSITIVE_ANSWERS'] is True: input_answer = answer['answer'] else: input_answer = answer['answer'].lower() is_correct_answer = Answer.verify_answer( input_answer, q_id_to_answer_db[answer['question_id']]) if is_correct_answer is not True: log.info({ 'message': 'The user entered an incorrect answer', 'user': username }) failed_attempt = FailedAttempt(user_id=user_id, time=datetime.utcnow()) db.session.add(failed_attempt) db.session.commit() if User.is_user_locked_out(user_id): msg = 'The user failed too many password reset attempts. They are now locked out.' log.info({'message': msg, 'user': username}) raise Unauthorized( 'You have answered incorrectly too many times. Your account is ' 'now locked. Please try again later.') raise Unauthorized( 'One or more answers were incorrect. Please try again.') log.debug({ 'message': 'The user successfully answered their questions', 'user': username }) ad.reset_password(username, new_password) log.info({ 'message': 'The user successfully reset their password', 'user': username }) return jsonify({}), 204
def add_answers(): """ Add a user's secret answers tied to administrator approved questions. :rtype: flask.Response """ user_ad_guid = get_jwt_identity()['guid'] user_id = db.session.query(User.id).filter_by(ad_guid=user_ad_guid).scalar() username = get_jwt_identity()['username'] # Make sure the user hasn't already set the required amount of secret answers num_answers_in_db = \ (db.session.query(func.count(Answer.answer))).filter_by(user_id=user_id).scalar() if num_answers_in_db != 0: log.debug({ 'message': 'The user attempted to set their secret answers but had them already set', 'user': username, }) raise ValidationError( 'You\'ve previously set your secret answers. Please reset them to set them again.') req_json = copy.deepcopy(request.get_json(force=True)) if not isinstance(req_json, list): log.debug({'message': 'The user did not supply an array', 'user': username}) raise ValidationError('The input must be an array') num_answers = len(req_json) # Verify that the user supplied the required amount of answers if num_answers != current_app.config['REQUIRED_ANSWERS']: log.info({'message': 'The user supplied an invalid amount of answers', 'user': username}) if num_answers == 1: error_prefix = '1 answer was' else: error_prefix = '{0} answers were'.format(num_answers) raise ValidationError('{0} supplied but {1} are required'.format( error_prefix, current_app.config['REQUIRED_ANSWERS'] )) question_ids = set() answer_strings = set() for answer in req_json: _validate_api_input(answer, 'answer', string_types) _validate_api_input(answer, 'question_id', int) # Verify the answers meet the length requirements if len(answer['answer']) < current_app.config['ANSWERS_MINIMUM_LENGTH']: log.info({ 'message': 'The user supplied an answer of length {0}, but {1} is required'.format( len(answer['answer']), current_app.config['ANSWERS_MINIMUM_LENGTH'] ), 'user': username, }) raise ValidationError('The answer must be at least {0} characters long'.format( current_app.config['ANSWERS_MINIMUM_LENGTH'])) # If answers aren't stored as case-sensitive, then convert it to lowercase if current_app.config['CASE_SENSITIVE_ANSWERS'] is False: log.debug({'message': 'Setting the answer to lowercase', 'user': username}) answer['answer'] = answer['answer'].lower() # Make sure the supplied question_id maps to a real and enabled question in the database question = Question.query.get(answer['question_id']) if not question: log.info({'message': 'The user supplied an invalid question', 'user': username}) raise ValidationError('The "question_id" is invalid') elif question.enabled is False: log.info({'message': 'The user tried to use a disabled question', 'user': username}) raise ValidationError( 'The "question_id" of {0} is to a disabled question'.format(question.id)) # Store these in sets to check duplicates question_ids.add(answer['question_id']) answer_strings.add(answer['answer']) # Make sure the user doesn't try to reuse the same question if len(question_ids) != num_answers: log.info({'message': 'The user supplied duplicate questions', 'user': username}) raise ValidationError( 'One or more questions were the same. Please provide unique questions.') # If duplicate answers aren't allowed, then verify the answers are unique allow_dup_answers = current_app.config['ALLOW_DUPLICATE_ANSWERS'] if allow_dup_answers is False and num_answers != len(answer_strings): log.info({'message': 'The user supplied duplicate answers', 'user': username}) raise ValidationError('One or more answers were the same. Please provide unique answers.') # Now that the input is validated, add the entries to the database answer_objects = [] for answer in req_json: hashed_answer = Answer.hash_answer(answer['answer']) answer_obj = Answer( answer=hashed_answer, question_id=answer['question_id'], user_id=user_id) db.session.add(answer_obj) answer_objects.append(answer_obj) db.session.commit() # This must be run after the session is committed because the ID needs to be set answers_json = [answer.to_json() for answer in answer_objects] log.info({'message': 'The user successfully set their secret answers', 'user': username}) return jsonify(answers_json), 201
def add_answers(): """ Add a user's secret answers tied to administrator approved questions. :rtype: flask.Response """ user_ad_guid = get_jwt_identity()['guid'] user_id = db.session.query( User.id).filter_by(ad_guid=user_ad_guid).scalar() username = get_jwt_identity()['username'] # Make sure the user hasn't already set the required amount of secret answers num_answers_in_db = \ (db.session.query(func.count(Answer.answer))).filter_by(user_id=user_id).scalar() if num_answers_in_db != 0: log.debug({ 'message': 'The user attempted to set their secret answers but had them already set', 'user': username, }) raise ValidationError( 'You\'ve previously set your secret answers. Please reset them to set them again.' ) req_json = copy.deepcopy(request.get_json(force=True)) if not isinstance(req_json, list): log.debug({ 'message': 'The user did not supply an array', 'user': username }) raise ValidationError('The input must be an array') num_answers = len(req_json) # Verify that the user supplied the required amount of answers if num_answers != current_app.config['REQUIRED_ANSWERS']: log.info({ 'message': 'The user supplied an invalid amount of answers', 'user': username }) if num_answers == 1: error_prefix = '1 answer was' else: error_prefix = '{0} answers were'.format(num_answers) raise ValidationError('{0} supplied but {1} are required'.format( error_prefix, current_app.config['REQUIRED_ANSWERS'])) question_ids = set() answer_strings = set() for answer in req_json: _validate_api_input(answer, 'answer', string_types) _validate_api_input(answer, 'question_id', int) # Verify the answers meet the length requirements if len(answer['answer'] ) < current_app.config['ANSWERS_MINIMUM_LENGTH']: log.info({ 'message': 'The user supplied an answer of length {0}, but {1} is required' .format(len(answer['answer']), current_app.config['ANSWERS_MINIMUM_LENGTH']), 'user': username, }) raise ValidationError( 'The answer must be at least {0} characters long'.format( current_app.config['ANSWERS_MINIMUM_LENGTH'])) # If answers aren't stored as case-sensitive, then convert it to lowercase if current_app.config['CASE_SENSITIVE_ANSWERS'] is False: log.debug({ 'message': 'Setting the answer to lowercase', 'user': username }) answer['answer'] = answer['answer'].lower() # Make sure the supplied question_id maps to a real and enabled question in the database question = Question.query.get(answer['question_id']) if not question: log.info({ 'message': 'The user supplied an invalid question', 'user': username }) raise ValidationError('The "question_id" is invalid') elif question.enabled is False: log.info({ 'message': 'The user tried to use a disabled question', 'user': username }) raise ValidationError( 'The "question_id" of {0} is to a disabled question'.format( question.id)) # Store these in sets to check duplicates question_ids.add(answer['question_id']) answer_strings.add(answer['answer']) # Make sure the user doesn't try to reuse the same question if len(question_ids) != num_answers: log.info({ 'message': 'The user supplied duplicate questions', 'user': username }) raise ValidationError( 'One or more questions were the same. Please provide unique questions.' ) # If duplicate answers aren't allowed, then verify the answers are unique allow_dup_answers = current_app.config['ALLOW_DUPLICATE_ANSWERS'] if allow_dup_answers is False and num_answers != len(answer_strings): log.info({ 'message': 'The user supplied duplicate answers', 'user': username }) raise ValidationError( 'One or more answers were the same. Please provide unique answers.' ) # Now that the input is validated, add the entries to the database answer_objects = [] for answer in req_json: hashed_answer = Answer.hash_answer(answer['answer']) answer_obj = Answer(answer=hashed_answer, question_id=answer['question_id'], user_id=user_id) db.session.add(answer_obj) answer_objects.append(answer_obj) db.session.commit() # This must be run after the session is committed because the ID needs to be set answers_json = [answer.to_json() for answer in answer_objects] log.info({ 'message': 'The user successfully set their secret answers', 'user': username }) return jsonify(answers_json), 201