Example #1
0
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
Example #2
0
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
Example #3
0
    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()
Example #4
0
    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()
Example #5
0
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)
Example #6
0
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
Example #7
0
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