Stop unverified users from accessing third party app urls - 2FA

Continuing the discussion from Automatic Authentication Security:

I added two-factor authentication and have set django allauth to mandatory so users have to verify their email. However users can still enable 2FA in the meantime before they’ve verified their email.

I removed the 2FA link from my template but if users know the installed app url they can enable it. What if someone pre-registers other peoples emails and locks them out with 2FA without needing to verify.

I was wondering what the normal scenario is to handle this since it’s not built into the 2FA app and because it’s a third party app I can’t edit the .py files to enforce email verification. Is it assumed that you set up separate steps to prevent unverified user access? If so what are those steps?

I came up with some solutions:

  1. Dynamically add the urlpatterns in urls.py for the third party 2FA app depending if the user is verified or not, similar to checking if debug is enabled:
if settings.DEBUG:
      urlpatterns += [
          path(
              "400/",
              default_views.bad_request,
              kwargs={"exception": Exception("Bad Request!")},
        ),
        ...

However I’m not sure at what point you would check the email like:

email = EmailAddress.objects.get(user=self.request.user)
if email.verified == True:
	urlpatterns += [
		path('', include(tf_urls)) // the urls.py of the two factor installed app from pip install.

The user isn’t accessing the view yet to put the check in their but also the 3rd party views.py isn’t accessible to me, so there is no self.request.user.

  1. Add a decorator to the url, django alluth provides a decorator @verified_email_required. I found an old stack overflow post from 2012 for adding decorators to urls however while trying to implement it I thought this can’t be right, is everyone who sets user logins with django doing this to stop users accessing third party app urls?
    Is it possible to decorate include(...) in django urls with login_required? - Stack Overflow

  2. Clone the repository and install the two-factor app like one of my own apps and perform the email verified checks in the views.py, however then I don’t get access to updates anymore.

  3. I could edit the lib/python3.9/site-packages/two_factor/views/core.py of the installed app. I thought I might be confused later trying to install requirement.txt and having to keep track of changes outside the project folder, not sure if this is the best option.

For my own apps I put a check in the views.py to confirm the email is verified before displaying data but how can I achieve this with third party apps? Are any of the options I mentioned the standard way of doing it? Thanks

It depends upon what the mechanism is for the 2FA. If it’s an email confirmation, I don’t think you should allow any way of enabling 2FA without verifying the email. (That’s true with any 2FA mechanism. You don’t rely upon an SMS message without verifying the phone number, or a third-party authenticator without validating it.)

You can “wrap” views with your own “mini view”.
In your urls, define your view as the target of the POST for the login view. (e.g. path('accounts/login/', my_view, ...)
Your view receives the login form and checks the account. If the account is verified, pass it through to the original view. If not, handle it however you want.
Example:

def my_login_view(request, *args, **kwargs):
    # Code to check submitted username
    if validated_username:  # Whatever test is appropriate
        return original_login_view(request, *args, **kwargs)
    # Handle unvalidated user condition

I don’t know if this is in any way “standard”, “common”, or “typical”, but it’s the mechanism we use when we want to override or augment a third-party view.

I’m using QR codes for 2FA, the mini view works good for me. I ended up copying the url path from the third party app and put it in my main urls.py above the include of the third party app:

    path(
        'account/two_factor/setup/',
        mysetupView,
        name='setup',
    ),    
    path('', include(tf_urls)),

I can check the email verification now in my mini view, thank you.