def add_jwt_claims(identity): """ Verify the user is authorized and add the role (admin or user) to the JWT. :param dict identity: a dictionary with user's GUID stored """ ad = adreset.ad.AD() ad.service_account_login() claims = {} if ad.check_admin_group_membership(identity['guid']): claims['roles'] = ['admin'] elif ad.check_user_group_membership(identity['guid']): # Make sure there are enough questions configured for the application to be usable total_questions = db.session.query(func.count(Question.question)).scalar() if total_questions < current_app.config['REQUIRED_ANSWERS']: log.error('There are {0} questions configured. There must be at least {1}.' .format(total_questions, current_app.config['REQUIRED_ANSWERS'])) raise ValidationError('The administrator has not finished configuring the application') else: claims['roles'] = ['user'] else: raise Unauthorized('You don\'t have access to use this application') username = ad.get_sam_account_name(identity['guid']) claims['username'] = username return claims
def add_jwt_claims(identity): """ Verify the user is authorized and add the role (admin or user) to the JWT. :param dict identity: a dictionary with user's GUID stored """ ad = adreset.ad.AD() ad.service_account_login() claims = {} if ad.check_admin_group_membership(identity['guid']): claims['roles'] = ['admin'] elif ad.check_user_group_membership(identity['guid']): # Make sure there are enough questions configured for the application to be usable total_questions = db.session.query(func.count( Question.question)).scalar() if total_questions < current_app.config['REQUIRED_ANSWERS']: log.error( 'There are {0} questions configured. There must be at least {1}.' .format(total_questions, current_app.config['REQUIRED_ANSWERS'])) raise ValidationError( 'The administrator has not finished configuring the application' ) else: claims['roles'] = ['user'] else: raise Unauthorized('You don\'t have access to use this application') username = ad.get_sam_account_name(identity['guid']) claims['username'] = username return claims
def get_id_from_ad_username(username, ad=None): """ Query Active Directory to find the user's ID in the database. :param str username: the user's sAMAccountName :kwarg adreset.ad.AD ad: an optional Active Directory session that is logged in with the service account :return: the user's ID in the database :rtype: int or None """ if not ad: ad = adreset.ad.AD() ad.service_account_login() try: user_guid = ad.get_guid(username) except adreset.error.ADError: return None return db.session.query(User.id).filter_by(ad_guid=user_guid).scalar()
def account_status(username): """ Get general information about the account in the context of the domain. :rtype: flask.Response """ if current_app.config['ACCOUNT_STATUS_ENABLED'] is False: raise NotFound() ad = adreset.ad.AD() ad.service_account_login() status = ad.get_account_status(username) if not status: raise NotFound('The user was not found') # Convert all the datetime objects to ISO 8601 strings for key, value in status.items(): if isinstance(value, datetime): status[key] = value.strftime('%Y-%m-%dT%H:%M:%S%z') return jsonify(status)
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