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
/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.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?
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
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.
django.contrib.auth.authenticate
. This auth's the credentials but won't create a session.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})