'django-otp implementation using custom Views

I have been trying to implement the django-otp with qrcode using custom Forms and Views. The problem is I am a little caught up on whether my implementation is correct or not. As the documentation states that a request.user.is_verified() attribute is added to users who have been OTP verified, I am actually not able to get it right. Have created confirmed TOTP device for user with the QR code setup using Microsoft Authenticator app.

I was able to successfully implement the default Admin Site OTP verification without any issues. Below is the files for the custom implementation.

urls.py

from django.conf.urls import url
from account.views import AccountLoginView, AccountHomeView, AccountLogoutView

urlpatterns = [
    url(r'^login/$', AccountLoginView.as_view(), name='account-login',),
    url(r'^home/$', AccountHomeView.as_view(), name='account-home',),
    url(r'^logout/$', AccountLogoutView.as_view(), name='account-logout',)
]

views.py

from django.contrib.auth import authenticate, login as auth_login
from django.views.generic.base import TemplateView
from django.views.generic.edit import FormView
from django_otp.forms import OTPAuthenticationForm

class AccountLoginView(FormView):

    template_name = 'login.html'
    form_class = OTPAuthenticationForm
    success_url = '/account/home/'

    def form_invalid(self, form):
        return super().form_invalid(form)

    def form_valid(self, form):

        # self.request.user returns AnonymousUser
        # self.request.user.is_authenticated returns False
        # self.request.user.is_verified() returns False

        username = form.cleaned_data.get('username')
        password = form.cleaned_data.get('password')
        otp_token = form.cleaned_data.get('otp_token')
        otp_device = form.cleaned_data.get('otp_device')

        user = authenticate(request=self.request, username=username, password=password)

        if user is not None:

            device_match = match_token(user=user, token=otp_token)

            # device_match returns None

            auth_login(self.request, user)

            # self.request.user returns [email protected]
            # self.request.user.authenticated returns True
            # self.request.user.is_verified returns AttributeError 'User' object has no attribute 'is_verified'
            # user.is_verified returns AttributeError 'User' object has no attribute 'is_verified'

        return super().form_valid(form)

class AccountHomeView(TemplateView):
    template_name = 'account.html'

    def get(self, request, *args, **kwargs):

        # request.user.is_authenticated returns True
        # request.user.is_verified() returns False

        return super(AccountHomeView, self).get(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['data'] = 'This is secured text'
        return context

login.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Login</title>
    </head>
    <body>
        <form action="." method="post">

            {% csrf_token %}

            {{ form.non_field_errors }}

            <div class="fieldWrapper">
                {{ form.username.errors }}
                <label for="{{ form.username.id_for_label }}">{{ form.username.label_tag }}</label>
                {{ form.username }}
            </div>

            <div class="fieldWrapper">
                {{ form.password.errors }}
                <label for="{{ form.password.id_for_label }}">{{ form.password.label_tag }}</label>
                {{ form.password }}
            </div>

            {% if form.get_user %}
                <div class="fieldWrapper">
                    {{ form.otp_device.errors }}
                    <label for="{{ form.otp_device.id_for_label }}">{{ form.otp_device.label_tag }}</label>
                    {{ form.otp_device }}
                </div>
            {% endif %}

            <div class="fieldWrapper">
                {{ form.otp_token.errors }}
                <label for="{{ form.otp_token.id_for_label }}">{{ form.otp_token.label_tag }}</label>
                {{ form.otp_token }}
            </div>

            <input type="submit" value="Log In" />

            {% if form.get_user %}
                <input type="submit" name="otp_challenge" value="Get Challenge" />
            {% endif %}

        </form>
    </body>
</html>

Could anyone please let me know what is that I am missing. I want to be able to authenticate and verify them using my own views by using the existing OTP form classes.

Please advice.



Solution 1:[1]

What exactly is match_token? You don't need a device field, rather you try and a device for a user.

Here's my implementation for that.

from django_otp import devices_for_user
from django_otp.plugins.otp_totp.models import TOTPDevice


def get_user_totp_device(user, confirmed=None):
    devices = devices_for_user(user, confirmed=confirmed)
    for device in devices:
        if isinstance(device, TOTPDevice):
            return device


def create_device_topt_for_user(user):
    device = get_user_totp_device(user)
    if not device:
        device = user.totpdevice_set.create(confirmed=False)
    return device.config_url


def validate_user_otp(user, data):
    device = get_user_totp_device(user)
    serializer = otp_serializers.TokenSerializer(data=data)

    if not serializer.is_valid():
        return dict(data='Invalid data', status=status.HTTP_400_BAD_REQUEST)
    elif device is None:
        return dict(data='No device registered.', status=status.HTTP_400_BAD_REQUEST)
    elif device.verify_token(serializer.data.get('token')):
        if not device.confirmed:
            device.confirmed = True
            device.save()
            return dict(data='Successfully confirmed and saved device..', status=status.HTTP_201_CREATED)
        else:
            return dict(data="OTP code has been verified.", status=status.HTTP_200_OK)
    else:
        return dict(
            data=
            dict(
                statusText='The code you entered is invalid',
                status=status.HTTP_400_BAD_REQUEST
            ),
            status=status.HTTP_400_BAD_REQUEST
        )

And then in a view, you can just do something like

create_device_topt_for_user(user=request.user)

validate_user_otp(request.user, request.data)

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Simeon Aleksov