Selectively Require MFA

What I’m trying to do:
Selectively require MFA by user. I want to enforce MFA for users who have configured MFA (i.e. they have something in the mfa column of the users table).

What I’ve tried and what’s not working:
It seems the only option to selectively enforce MFA is to use a server function to replace login_with_email that checks the mfa column of the user table and then sets up the mfa login if something’s there.

However, login_with_email(email, password) can still be called from the client code with no errors unless I check off this:

login_with_email returns an MFARequired exception if it is called without an mfa object and the “Require 2-factor authentication” is checked.

So it seems that there isn’t a secure way to do this unless there’s a change in the app configuration that enforces selective MFA. Something like “Raise MFARequired exception if user has configured MFA”.

Are there any other options other than a feature request to add this to the User’s service?

1 Like

Does this even work if the setting isn’t checked? I mean, have you tried entering the wrong totp and see if you actually get a MFAException or AuthenticationFailed exception?

I have a suspicion that it might not be checking the MFA and only using email and password if that setting isn’t checked, unless you are checking it manually and calling logout if it fails.

So I went about this backwards and solved it. I simply kept the MFA required in the User settings, and created a server function to override that if there’s no MFA object defined for that user row:

@anvil.server.callable
def login_with_email_mfa(email, password):
    """Try to log user in without MFA. Return exception if user has MFA configured."""
    import bcrypt
    user = app_tables.users.get(email=email)
    if user:
        if user['n_password_failures'] >= 10:
            raise anvil.users.TooManyPasswordFailures('You have reached your limit of password attempts. Please reset your password.')
        elif user['mfa'] is not None:
            raise anvil.users.MFARequired('User needs to enter MFA credentials.')
        elif user['confirmed_email'] != True:
            raise anvil.users.EmailNotConfirmed('Please confirm your email before logging in.')
        elif bcrypt.checkpw(password.encode('utf-8'), user['password_hash'].encode('utf-8')):
            anvil.users.force_login(user, remember=True)
            user['n_password_failures'] = 0
            return user
        else:
            user['n_password_failures'] += 1
            raise anvil.users.AuthenticationFailed('Email or password is incorrect.')
    else:
        raise anvil.users.AuthenticationFailed('Email or password is incorrect.')
3 Likes