pythondjangodjango-rest-framework

Adding two factor authentication in Django/Django Rest


I know this topic has been widely discussed, but most of the examples are about two factor authentication in standard Django templates, while in my case i want to add two factor authentication to a project where Django is used as an API on the backend while the frontend is a native VueJS application.

For eveything authentication related, i'm using the built-in Django session authentication, since both frontend and backend are deployed on the same server.

My question is: how can i add two factor authentication (using google authenticator or yubikey) to a project where django is used as an API?

Here is the problem: the easiest way to do this would be to let the user login from the frontend, and once the user is logged in from /accounts/login (built-in django authentication view), submit a form where the user has to input their code. The problem with this approach is that once the user is logged in Django will create a session, so request.user.is_authenticated will return True even though the user didn't submit the Two Factor code yet, so everything would depend on the frontend. I don't like this approach because i'm afraid that someone might find a way to avoid submitting the two factor form and navigate on the rest of the site (since according to Django that session would be authenticated) without the two factor authentication

What i tried: I still have to write most of the code for this, because i want to understand how safe is it first. But here is my approach:

First approach

  1. User submits the login form
  2. Once the login form is submitted, a POST request with the credentials is sent to an endpoint called /authenticate in my Django app. This endpoint will use the Django built-in authenticate() method that will check if those credentials belong to a user without creating a session.
  3. If the credentials belong to a user, it will return True to the user. At this point the user will submit a form with the 2FA code, and if the code is right, the request is sent to /accounts/login which will check again password and email and actually login the user and create the session, this time.

Second approach Another approach, that would be even better, would be to override the Django-Allauth login view so that i can add a check for the token, so something like (WARNING: pseudo-code):

if provided_code == user_code:
   login()
   return HttpResponse({'Result': 'Logged in!'})
else:
   return HttpResponse({'Result': 'incorrect code'})

Where provided_code is the 2FA code provided by the user and user_code is the correct code that i will retrieve from another function like get_user_2fa_code(user).

I'm not a security expert, but i didn't come up with better approaches. How safe would it be? Is there a better way to add 2FA auth to a Django project where django works as a backend API?

Here is the login view:

class LoginView(
    RedirectAuthenticatedUserMixin, AjaxCapableProcessFormViewMixin, FormView
):
    form_class = LoginForm
    template_name = "account/login." + app_settings.TEMPLATE_EXTENSION
    success_url = None
    redirect_field_name = "next"

    @sensitive_post_parameters_m
    def dispatch(self, request, *args, **kwargs):
        return super(LoginView, self).dispatch(request, *args, **kwargs)

    def get_form_kwargs(self):
        kwargs = super(LoginView, self).get_form_kwargs()
        kwargs["request"] = self.request
        return kwargs

    def get_form_class(self):
        return get_form_class(app_settings.FORMS, "login", self.form_class)

    def form_valid(self, form):
        success_url = self.get_success_url()
        try:
            return form.login(self.request, redirect_url=success_url)
        except ImmediateHttpResponse as e:
            return e.response

    def get_success_url(self):
        # Explicitly passed ?next= URL takes precedence
        ret = (
            get_next_redirect_url(self.request, self.redirect_field_name)
            or self.success_url
        )
        return ret

    def get_context_data(self, **kwargs):
        ret = super(LoginView, self).get_context_data(**kwargs)
        signup_url = passthrough_next_redirect_url(
            self.request, reverse("account_signup"), self.redirect_field_name
        )
        redirect_field_value = get_request_param(self.request, self.redirect_field_name)
        site = get_current_site(self.request)

        ret.update(
            {
                "signup_url": signup_url,
                "site": site,
                "redirect_field_name": self.redirect_field_name,
                "redirect_field_value": redirect_field_value,
            }
        )
        return ret

Solution

  • This may help..I'm unsure. Most of the existing django 2fa projects work how you describe- I recommend checking how they do it for some better hints.

    1. First check username/password using django.contrib.auth.authenticate. This auth's the credentials but won't create a session.
    2. If the user has 2FA- route them to a page to do the verification step
    3. Once verification step is complete- log them in with django.contrib.auth.login

    For the login auth view and posting the credentials- I did something like this:

    class TwoFactorAwareLoginView(TemplateView):
        def post(self, request, *args, **kwargs):
            form = self.form_class(data=request.POST, request=request)
            if form.is_valid():
                username = form.cleaned_data.get("username")
                password = form.cleaned_data.get("password")
                user = authenticate(request, username=username, password=password)
                if user is not None:
                    devices = <something to get your 2FA device blobs>
                    if not devices: #no user devices- log them in
                        login(request, user)
                        return HttpResponseRedirect(reverse("cool-page"))
                    else: #2FA devices exist- route them to verification view and perform auth_login once complete
                        # add some context needed for verification view in session or context
                        return HttpResponseRedirect(reverse("two-factor:verify-login"))
            return render(request, self.template_name, {"form": form})