Django’da Parolasız Oturum Açma(Passwordless Authentication) Mekanizması Nasıl Uygulanır?

Sızdırılan parolaların durumları ortada hal böyle olunca alternatif oturum açma mekanizmaları gündeme gelmeye başladı, bunlardan bizimde zamanında kullandığımız yöntemi artık açıklama zamanı geldi. Bu sistemi hali hazırda kullanmayı bıraktık, elimde kalan halini sizlerle paylaşıyorum.

Öncelikli olarak bu sistemde uygulanan yapıda girişler suistimal edilmemesi amacıyla hem tek kullanımlık URL hemde giriş sırasında mail gatewaylerin linkin geçerliliğini devre dışı bırakmaması için hCaptcha ekledik. Tabii bu durum kullanıcıların canını biraz sıktı diyebiliriz, ama güvenlikten ödün vermedik.

Önce kullanıcının sisteme girebilmesi amacıyla bizim belirli bir link oluşturmamız gerekiyor, daha sonra oluşturulan bu link mail aracılığı ile kullanıcıya gidecek oluşturacağımız bu linkin ise belirli bir geçerlilik süresi olacak aynı zamanda link tıklandığında geçerliliğini güvenlik için yitirecek.

class IsLogin(FormView):
    http_method_names = ["get", "post"]
    template_name = "account/login.html"
    email_template_name = "account/mails/login_email.html"
    form_class = UserForm
    success_url = settings.LOGIN_REDIRECT_URL

    def form_valid(self, form):
        try:
            user = Users.objects.get(email=form.cleaned_data["email"], customer__is_active=True)
        except Users.DoesNotExist:
            messages.success(self.request, _("If an account matches the username %s, you should receive an email with magic link." % form.cleaned_data["email"]))
        else:
            current_site = get_current_site(self.request)
            mail_subject = _("Login your account.")
            mail_message = render_to_string(
                self.email_template_name,
                {
                    "user": user,
                    "domain": current_site.domain,
                    "uid": urlsafe_base64_encode(force_bytes(user.pk)),
                    "token": account_activation_token.make_token(user),
                },
            )
            email = EmailMessage(from_email='noreply@mertcangokgoz.com', to=[user.email], subject=mail_subject, body=mail_message)
            email.content_subtype = "html"
            email.send()
            messages.success(self.request, _("If an account matches the username %s, you should receive an email with magic link." % form.cleaned_data["email"]))
        return render(self.request, self.template_name, {"form":self.get_form()})

    def form_invalid(self, form):
        return render(
            self.request, self.template_name, {'form':self.get_form()}
        )

Sisteme giriş yapmış bir kullanıcı tekrar bir şekilde istekte bulunmasın diyede ayrıca kontrol ediyoruz giriş yapmışsa tekrar ilgili alana yönlendiriyoruz.

    def get(self, request, *args, **kwargs):
        if self.request.user.is_authenticated:
            return HttpResponseRedirect(resolve_url(self.success_url))
        return super().get(request, *args, **kwargs)

Eposta oluşturduğumuz ve gerekli bilgileri girdiğimiz noktada ise kullanıcıya özel bir token oluşturuyoruz. Ve bu tokenle birlikte bağlantılı olan verileri ekliyoruz.

mail_message = render_to_string(
self.email_template_name,
 {
    "user": user,
    "domain": current_site.domain,
    "uid": urlsafe_base64_encode(force_bytes(user.pk)),
    "token": account_activation_token.make_token(user),
 },
)

Tokeni ise kullanıcıya göre aşağıdaki şekilde oluşturuyoruz. Burada önemli olan magic_link_secret_key değerinin giriş yapıldıktan sonra otomatik olarak değişmesi olacak.

from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.utils import six


class TokenGenerator(PasswordResetTokenGenerator):
    def _make_hash_value(self, user, timestamp):

        return (
            six.text_type(user.pk)
            + six.text_type(timestamp)
            + six.text_type(user.magic_link_secret_key)
        )


account_activation_token = TokenGenerator()

Hem OTP hemde Captcha için kullanacağımız sayfamızın ise kodları şu şekilde olacak.

class OTPView(View):
    template_name = "account/otp_login.html"

    def get(self, *args, **kwargs):
        try:
            uid = force_text(urlsafe_base64_decode(self.kwargs.get("uidb64")))
            user = Users.objects.get(pk=uid)
        except:
            messages.error(self.request, _("Link is invalid!"))
            return redirect("/")

        form = OtpCodeForm()
        otp = False

        if not account_activation_token.check_token(user, self.kwargs.get("token")):
            messages.error(self.request, _("Link is invalid!"))
            return redirect("login")

        if not user.otp_active and settings.OTP_IS_ACTIVE and user.is_2fa:
            if account_activation_token.check_token(user, self.kwargs.get("token")):
                return redirect("otp_activation", uidb64 = self.kwargs.get("uidb64"), token = self.kwargs.get("token"))
            else:
                return HttpResponse(_("Link is invalid!"))

        if user.is_2fa:
            if account_activation_token.check_token(user, self.kwargs.get("token")):
                otp = True

        return render(
            self.request, self.template_name, {"form": form, "hcaptcha_sitekey":settings.HCAPTCHA_SITEKEY, "otp":otp}
        )


    def post(self, *args, **kwargs):
        try:
            uid = force_text(urlsafe_base64_decode(self.kwargs.get("uidb64")))
            user = Users.objects.get(pk=uid)
        except:
            messages.error(self.request, _("Link is invalid!"))
            return redirect("/")
        form = OtpCodeForm(data=self.request.POST)
        if not self.request.POST.get("h-captcha-response"):
            messages.warning(self.request, _("Please enter captcha!"))
            return redirect("activate", uidb64 = self.kwargs.get("uidb64"), token = self.kwargs.get("token"))
        payload = { 'secret': settings.HCAPTCHA_SECRET, 'response': self.request.POST["h-captcha-response"] }
        r = requests.post("https://hcaptcha.com/siteverify", data=payload)
        result = json.loads(r.text)
        if not result['success']:
            messages.error(self.request, _("Incorrect Captcha"))
            return redirect("activate", uidb64 = self.kwargs.get("uidb64"), token = self.kwargs.get("token"))

        if user.is_2fa:
            if account_activation_token.check_token(user, self.kwargs.get("token")):
                if form.is_valid():
                    code = form.cleaned_data["code"]
                    totp = TOTP(user.secret_key)
                    if totp.verify(code) and user.customer.is_active:
                        if account_activation_token.check_token(user, self.kwargs.get("token")):
                            login(self.request, user)
                            user.magic_link_secret_key = random_base32()
                            user.save()
                            return redirect("/")
                        else:
                            return redirect("login")
                    else:
                        return HttpResponse(status=401)
                else:
                    messages.error(self.request, _("Incorrect code!"))
            else:
                messages.error(self.request, _("Link is invalid!"))
            return redirect("activate", uidb64 = self.kwargs.get("uidb64"), token = self.kwargs.get("token"))
        elif result['success']:
            if account_activation_token.check_token(user, self.kwargs.get("token")) and user.customer.is_active:
                login(self.request, user)
                user.magic_link_secret_key = random_base32()
                user.save()
                return redirect("/")
            else:
                return redirect("login")
        else:
            messages.error(self.request, str(result))
            return redirect("activate", uidb64 = self.kwargs.get("uidb64"), token = self.kwargs.get("token"))

Kodumuzun tamamı ise şu şekilde oluyor haliyle

from django.conf import settings
from django.contrib import messages
from django.contrib.auth import login, logout
from django.contrib.sites.shortcuts import get_current_site
from django.core.mail import EmailMessage
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import redirect, render, resolve_url
from django.template.loader import render_to_string
from django.utils.encoding import force_bytes, force_text
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
from django.utils.translation import gettext_lazy as _
from django.views.generic import FormView, View

from pyotp import TOTP, random_base32
from .utils.token import account_activation_token

class IsLogin(FormView):
    http_method_names = ["get", "post"]
    template_name = "account/login.html"
    email_template_name = "account/mails/login_email.html"
    form_class = UserForm
    success_url = settings.LOGIN_REDIRECT_URL

    def get(self, request, *args, **kwargs):
        if self.request.user.is_authenticated:
            return HttpResponseRedirect(resolve_url(self.success_url))
        return super().get(request, *args, **kwargs)

    def form_valid(self, form):
        try:
            user = Users.objects.get(email=form.cleaned_data["email"], customer__is_active=True)
        except Users.DoesNotExist:
            messages.success(self.request, _("If an account matches the username %s, you should receive an email with magic link." % form.cleaned_data["email"]))
        else:
            current_site = get_current_site(self.request)
            mail_subject = _("Login your account.")
            mail_message = render_to_string(
                self.email_template_name,
                {
                    "user": user,
                    "domain": current_site.domain,
                    "uid": urlsafe_base64_encode(force_bytes(user.pk)),
                    "token": account_activation_token.make_token(user),
                },
            )
            email = EmailMessage(from_email='noreply@mertcangokgoz.com', to=[user.email], subject=mail_subject, body=mail_message)
            email.content_subtype = "html"
            email.send()
            messages.success(self.request, _("If an account matches the username %s, you should receive an email with magic link." % form.cleaned_data["email"]))
        return render(self.request, self.template_name, {"form":self.get_form()})

    def form_invalid(self, form):
        return render(
            self.request, self.template_name, {'form':self.get_form()}
        )

class OTPView(View):
    template_name = "account/otp_login.html"

    def get(self, *args, **kwargs):
        try:
            uid = force_text(urlsafe_base64_decode(self.kwargs.get("uidb64")))
            user = Users.objects.get(pk=uid)
        except:
            messages.error(self.request, _("Link is invalid!"))
            return redirect("/")

        form = OtpCodeForm()
        otp = False

        if not account_activation_token.check_token(user, self.kwargs.get("token")):
            messages.error(self.request, _("Link is invalid!"))
            return redirect("login")

        if not user.otp_active and settings.OTP_IS_ACTIVE and user.is_2fa:
            if account_activation_token.check_token(user, self.kwargs.get("token")):
                return redirect("otp_activation", uidb64 = self.kwargs.get("uidb64"), token = self.kwargs.get("token"))
            else:
                return HttpResponse(_("Link is invalid!"))

        if user.is_2fa:
            if account_activation_token.check_token(user, self.kwargs.get("token")):
                otp = True

        return render(
            self.request, self.template_name, {"form": form, "hcaptcha_sitekey":settings.HCAPTCHA_SITEKEY, "otp":otp}
        )


    def post(self, *args, **kwargs):
        try:
            uid = force_text(urlsafe_base64_decode(self.kwargs.get("uidb64")))
            user = Users.objects.get(pk=uid)
        except:
            messages.error(self.request, _("Link is invalid!"))
            return redirect("/")
        form = OtpCodeForm(data=self.request.POST)
        if not self.request.POST.get("h-captcha-response"):
            messages.warning(self.request, _("Please enter captcha!"))
            return redirect("activate", uidb64 = self.kwargs.get("uidb64"), token = self.kwargs.get("token"))
        payload = { 'secret': settings.HCAPTCHA_SECRET, 'response': self.request.POST["h-captcha-response"] }
        r = requests.post("https://hcaptcha.com/siteverify", data=payload)
        result = json.loads(r.text)
        if not result['success']:
            messages.error(self.request, _("Incorrect Captcha"))
            return redirect("activate", uidb64 = self.kwargs.get("uidb64"), token = self.kwargs.get("token"))

        if user.is_2fa:
            if account_activation_token.check_token(user, self.kwargs.get("token")):
                if form.is_valid():
                    code = form.cleaned_data["code"]
                    totp = TOTP(user.secret_key)
                    if totp.verify(code) and user.customer.is_active:
                        if account_activation_token.check_token(user, self.kwargs.get("token")):
                            login(self.request, user)
                            user.magic_link_secret_key = random_base32()
                            user.save()
                            return redirect("/")
                        else:
                            return redirect("login")
                    else:
                        return HttpResponse(status=401)
                else:
                    messages.error(self.request, _("Incorrect code!"))
            else:
                messages.error(self.request, _("Link is invalid!"))
            return redirect("activate", uidb64 = self.kwargs.get("uidb64"), token = self.kwargs.get("token"))
        elif result['success']:
            if account_activation_token.check_token(user, self.kwargs.get("token")) and user.customer.is_active:
                login(self.request, user)
                user.magic_link_secret_key = random_base32()
                user.save()
                return redirect("/")
            else:
                return redirect("login")
        else:
            messages.error(self.request, str(result))
            return redirect("activate", uidb64 = self.kwargs.get("uidb64"), token = self.kwargs.get("token"))

Sistem Uzmanı, Linux Hacısı, El-Kernel

Yorum yapın