def login(): """ Login the user using their Active Directory credentials. :rtype: flask.Response """ req_json = request.get_json(force=True) _validate_api_input(req_json, 'username', string_types) _validate_api_input(req_json, 'password', string_types) ad = adreset.ad.AD() ad.login(req_json['username'], req_json['password']) username = ad.get_loggedin_user() guid = ad.get_guid(username) user = User.query.filter_by(ad_guid=guid).first() # If the user doesn't exist in the database, this must be their first time logging in, # therefore, an entry for that user must be added to the database if not user: ad.log( 'debug', 'The user doesn\'t exist in the database, so it will be created') user = User(ad_guid=guid) db.session.add(user) db.session.commit() ad.log('debug', 'The user was successfully created in the database') # The token's identity has the user's GUID since that is unique across the AD Forest and won't # change if the account gets renamed token = create_access_token(identity={ 'guid': user.ad_guid, 'username': username }) return jsonify({'token': token})
def get_answers_unauthenticated(username): """ List all the answers associated with the input user. :rtype: flask.Response """ user_id = User.get_id_from_ad_username(username) return Answer.query.filter_by(user_id=user_id)
def logged_in_headers(app): """Pytest fixture that creates a valid token.""" guid = '10385a23-6def-4990-84a8-32444e36e496' user = User(ad_guid=guid) db.session.add(user) db.session.commit() token = create_access_token(identity=guid) return { 'Authorization': 'Bearer {0}'.format(token), 'Content-Type': 'application/json' }
def _configure_user(): """Configure testuser2 in the database.""" user = User(ad_guid='10385a23-6def-4990-84a8-32444e36e496') db.session.add(user) answer = Answer(answer=Answer.hash_answer('strawberry'), user_id=1, question_id=1) answer2 = Answer(answer=Answer.hash_answer('green'), user_id=1, question_id=2) answer3 = Answer(answer=Answer.hash_answer('buzz lightyear'), user_id=1, question_id=3) db.session.add(answer) db.session.add(answer2) db.session.add(answer3) db.session.commit()
def test_get_answer_different_user(client, logged_in_headers): """Test accessing the answer of a different user in the answers/<id> route.""" user = User(ad_guid='5609c5ec-c0df-4480-a94b-b6eb0fc4c066') db.session.add(user) db.session.commit() answer = Answer(answer=Answer.hash_answer('strawberry'), user_id=user.id, question_id=1) db.session.add(answer) db.session.commit() rv = client.get('/api/v1/answers/1', headers=logged_in_headers) assert json.loads(rv.data.decode('utf-8')) == { 'message': 'This answer is not associated with your account', 'status': 401, }
def logged_in_headers(mock_user_ad): """Pytest fixture that creates a valid token for a user.""" # This is the GUID for "testuser2" which is a member of "ADReset Users" guid = '10385a23-6def-4990-84a8-32444e36e496' user = User(ad_guid=guid) db.session.add(user) db.session.commit() token = create_access_token(identity={ 'guid': guid, 'username': '******' }) return { 'Authorization': f'Bearer {token}', 'Content-Type': 'application/json' }
def admin_logged_in_headers(mock_admin_ad): """Pytest fixture that creates a valid token for an admin.""" # This is the GUID for "testuser" which is a member of "ADReset Admins" guid = '5609c5ec-c0df-4480-a94b-b6eb0fc4c066' user = User(ad_guid=guid) db.session.add(user) db.session.commit() token = create_access_token(identity={ 'guid': guid, 'username': '******' }) return { 'Authorization': f'Bearer {token}', 'Content-Type': 'application/json' }
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