Executando verificação de segurança...
0

[DJANGO] Crie seu próprio two factors authenticator.

Eai pessoal, vamos criar nosso próprio fator de autenticação?

settings.py

SITE_ID = 1

EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
EMAIL_HOST_USER = "no-repy@{domain}"

INSTALLED_APPS = [
 ...
 "django.contrib.sites",
 ...
]

models.py

from django.contrib.auth import get_user_model
from django.core.validators import MaxValueValidator
from django.utils.timezone import datetime, timedelta

Account = get_user_model()

class AuthToken(models.Model):
    account = models.OneToOneField(Account, on_delete=models.CASCADE)
    attempts = models.PositiveIntegerField(
        default=0, validators=[MaxValueValidator(3, "Max attempts.")]
    )
    token = models.CharField(max_length=6)
    date_expire = models.DateTimeField(default=datetime.now() + timedelta(days=5))

    def __str__(self) -> str:
        return "The %s expire at %s " % (self.token, self.date_expire)

Lembrar de criar seu template de emails, ou utilizar o descrito abaixo.

templates/emails/authentication_factor.html

{% autoescape off %}
    {{subject}}
    {{token}}
    {{support}}
{% endautoescape %}

utils/emails.py

import random

from django.utils import timezone
from django.template.loader import render_to_string
from django.core.mail import send_mail
from django.contrib.sites.models import Site
from django.conf import settings


def mail_authenticate_token(cls):
    domain = Site.objects.get_current()

    data = {
        "subject": "Authenticate token",
        "token": random.randint(100000, 999999),
        "support": settings.EMAIL_HOST_USER.format(domain=domain),
    }

    token = AuthToken.objects.filter(
        account=cls,
    )

    if token.exists():
        token.first().delete()

    AuthToken.objects.create(account=cls, token=data["token"])

    body = render_to_string("emails/authenticate_factor.html", data)

    send_mail(
        subject=data.get("subject"),
        message=body,
        from_email=settings.EMAIL_HOST_USER.format(domain=domain),
        recipient_list=[cls.email],
        fail_silently=False,
    )
    
def validate_token(cls, token, form):
    get_token = AuthToken.objects.filter(account_id__exact=cls.pk)
    if not get_token.exists():
        return False

    authtoken = get_token.first()

    if token != authtoken.token:
        authtoken.attempts += 1
        authtoken.save(
            update_fields=[
                "attempts",
            ]
        )

        if (authtoken.attempts + 1) >= 4:
            authtoken.delete()
            form.add_error("token", "We are sent to you a new token, check your email.")
            mail_authenticate_token(cls)
        else:
            form.add_error(
                "token", "You have more %s attempts." % (3 - authtoken.attempts)
            )

        return False

    if authtoken.date_expire <= timezone.now():
        authtoken.delete()
        form.add_error("token", "We are sent to you a new token, check your email.")
        mail_authenticate_token(cls)
        return False
    return True

views.py

from django.urls import reverse_lazy
from django.contrib.auth import login, authenticate
fro django.shortcuts import get_object_or_404

from apps.accounts.utils.emails import mail_authenticate_token
from apps.accounts.forms import LoginForm, AuthenticationTokenForm


def get_or_create_session(request):
    session = request.session.session_key
    if not session:
        return request.session.create()
    return session


class LoginView(FormView, AuthenticatedRedirectMixin):
    template_name = "login.html"
    form_class = LoginForm
    success_url = reverse_lazy("authentication_token")

    def form_valid(self, form):
        email, password = form.cleaned_data.values()

        auth = authenticate(self.request, **dict(email=email, password=password))
        if not auth:
            form.add_error("email", "Cannot authenticate with this credentials.")
            return super().form_invalid(form)

        mail_authenticate_token(auth)
        get_or_create_session(self.request)

        self.request.session["auth_id"] = auth.id
        return super().form_valid(form)


class AuthenticationTokenView(FormView):
    template_name = "authentication_token.html"
    form_class = AuthenticationTokenForm
    success_url = "/"

    def dispatch(self, request, *args, **kwargs):
        if not request.session.get("auth_id"):
            return redirect("/")
        return super().dispatch(request, *args, **kwargs)

    def form_valid(self, form):
        token = form.cleaned_data.get("token")
        auth_id = self.request.session.get("auth_id")
        user = get_object_or_404(Account, id=auth_id)

        if not validate_token(user, token, form):
            return super().form_invalid(form)

        login(self.request, user)

        return super().form_valid(form)

forms.py

class LoginForm(forms.Form):
    """A form to authenticate an account, increased
    all fields necessary.
    """

    email = forms.CharField(
        widget=forms.EmailInput(
            attrs={
                "placehoder": "[email protected]",
            }
        )
    )
    password = forms.CharField(
        widget=forms.PasswordInput(attrs={"placeholder": "*********"})
    )

    def clean(self):
        cleaned_data = super().clean()
        email = cleaned_data.get("email")
        password = cleaned_data.get("password")

        if not email:
            self.add_error("email", "Email was required.")

        if not password:
            self.add_error("password", "Password was required")

        acc = Account.objects.filter(email__iexact=email)

        if not acc.exists():
            self.add_error("email", "Wrong credentials, try again.")

        if not acc.exists():
            return self.add_error("password", "Credentials mismatch.")

        return cleaned_data


class AuthenticationTokenForm(forms.Form):
    token = forms.CharField(max_length=6)

    def clean(self):
        cleaned_data = super().clean()
        token = cleaned_data.get("token")
        if not token:
            self.add_error("token", "Token is wanted to continue")

        return cleaned_data

Carregando publicação patrocinada...