Published on

Custom Two-factor with Qrcode in django

Authors
  • avatar
    Name
    Lif
    Twitter

Notes about Google authentication installation

Getting started

In django, I use django-two-factor-auth to setup my google(or other authenticator) qr code and use django-otp to verify my code.

pip install django-two-factor-auth
pip install django-otp

Request by Frontend

Using Vue/React to post request to django-two-factor-auth.

Actually, I only need the put the qr code on my front end and scan it, then use the original api to accomplish the flow.

Look at the source of django-two-factor-auth .

// setup.html
<p><img src="{{ QR_URL }}" alt="QR Code" class="bg-white"/></p>
<p>{% blocktrans trimmed %}Alternatively you can use the following secret to
    setup TOTP in your authenticator or password manager manually.{% endblocktrans %}</p>
<p>{% translate "TOTP Secret:" %} <a href="{{ otpauth_url }}">{{ secret_key }}</a></p>
<p>{% blocktrans %}Then, enter the token generated by the app.{% endblocktrans %}</p>

QR_URL is given by backend. Search QR_URL in .py files.

Then got this:

	def get_context_data(self, form, **kwargs):
        context = super().get_context_data(form, **kwargs)
        if self.steps.current == 'generator':
            key = self.get_key('generator')
            rawkey = unhexlify(key.encode('ascii'))
            b32key = b32encode(rawkey).decode('utf-8')
            issuer = get_current_site(self.request).name
            username = self.request.user.get_username()
            otpauth_url = get_otpauth_url(username, b32key, issuer)
            self.request.session[self.session_key_name] = b32key
            context.update({
                # used in default template
                'otpauth_url': otpauth_url,
                'QR_URL': reverse(self.qrcode_url),
                'secret_key': b32key,
                # available for custom templates
                'issuer': issuer,
                'totp_digits': totp_digits(),
            })
        elif self.steps.current == 'validation':
            context['device'] = self.get_device()
        context['cancel_url'] = resolve_url(settings.LOGIN_REDIRECT_URL)
        return context

The process of generating qr_code is key->rawkey->b32key->otpauth_url->reverse().

I find the generation of key is here:

    def get_key(self, step):
        self.storage.extra_data.setdefault('keys', {})
        if step in self.storage.extra_data['keys']:
            return self.storage.extra_data['keys'].get(step)
        key = random_hex(20)
        self.storage.extra_data['keys'][step] = key
        return key

Now, let's focus on the source code:

# In django.views.generic.base.py I found the usage of get_context_data
# It returns all data to frontend via the TemplateResponse format
class TemplateView(TemplateResponseMixin, ContextMixin, View):
    """
    Render a template. Pass keyword arguments from the URLconf to the context.
    """

    def get(self, request, *args, **kwargs):
        context = self.get_context_data(**kwargs)
        return self.render_to_response(context)
        

class TemplateResponseMixin:
    """A mixin that can be used to render a template."""

    template_name = None
    template_engine = None
    response_class = TemplateResponse
    content_type = None

    def render_to_response(self, context, **response_kwargs):
        """
        Return a response, using the `response_class` for this view, with a
        template rendered with the given context.

        Pass response_kwargs to the constructor of the response class.
        """
        response_kwargs.setdefault("content_type", self.content_type)
        return self.response_class(
            request=self.request,
            template=self.get_template_names(),
            context=context,
            using=self.template_engine,
            **response_kwargs,
        )

在这里插入图片描述

The process from QR code generation to saving is just like this: 在这里插入图片描述

Write my code

After realize all process of this. Now I can write code to cover this process without source code using my own view.

from base64 import b32encode
from binascii import unhexlify

import qrcode

from django.utils.module_loading import import_string
from django.contrib.sites.shortcuts import get_current_site
from two_factor.utils import get_otpauth_url, totp_digits


class QRSetup(View):
    default_qr_factory = "qrcode.image.svg.SvgPathImage"

    def get(self, request, *args, **kwargs):
        key = random_hex(20)
        print(f"key is: {key}")
        rawkey = unhexlify(key.encode("ascii"))
        b32key = b32encode(rawkey).decode("utf-8")

        # Get data for qrcode
        image_factory_string = getattr(
            settings, "TWO_FACTOR_QR_FACTORY", self.default_qr_factory
        )
        image_factory = import_string(image_factory_string)
        content_type = "image/svg+xml; charset=utf-8"
        try:
            username = get_user_name()
        except Exception as e:
            username = "lif"
        issuer = get_current_site(self.request).name

        otpauth_url = get_otpauth_url(
            accountname=username, issuer=issuer, secret=b32key, digits=totp_digits()
        )

        # Make QR code
        img = qrcode.make(otpauth_url, image_factory=image_factory)
        resp = HttpResponse(content_type=content_type)
        img.save(resp)
        return resp


class QRCreateListView(generics.ListCreateAPIView):
    queryset = TOTPDevice.objects.all()
    serializer_class = QrCodeSerializer
    permission_classes = (permissions.AllowAny,)
    
    def post(self, request):
        key = 0
        tolerance = 1
        t0 = 0
        step = 30
        drift = 0
        digits = totp_digits()
        user = None
        try:
            request_body = request.data
        except Exception as e:
            print(e)
        key = request_body.get("key")
        user = User.objects.filter(username=request_body.get("user"))[0]

        try:
            TOTPDevice.objects.create(user=user, key=key,
                                            tolerance=tolerance, t0=t0,
                                            step=step, drift=drift,
                                            digits=digits,
                                            name='default')
            return HttpResponse("Success")
        except Exception as e:
            print(f"{e}")
            return HttpResponse("fail")

Then add url to urls.py

    path(
        "api/v2/test/", QRSetup.as_view(), name="testOtp"
    ),
    
    path(
        "api/v2/save/", QRCreateListView.as_view(), name="testOstp"
    )

Now, we can use Apifox to make request.

在这里插入图片描述

Notice to use the key generated by key = random_hex(20). 在这里插入图片描述 And I got success: 在这里插入图片描述