#StackBounty: #python #python-3.x #security #authentication #django Let's register that Django user

Bounty: 200

Short intro

So, I’ve been using Django for a while now and thought it would be nice to start a simple application. In an ideal World, each app must have a way of letting its users register and that’s what I’m gonna do below.


Description

I’ve used the latest Django version for this project -> because of this, I couldn’t use django-registration module as it’s incompatible with the above. Apart from this, I’ve used Python 3.6.5.

What I want to do it’s common along other web applications:

  1. The user goes to the registration page and fills in the following fields:
    • first_name
    • last_name
    • email
  2. The user submits the form and receives a confirmation email with an URL containing a unique token.

  3. When the user clicks on the received link, he’s redirected to a page where he’ll set his password.

  4. When done, he’s logged in to the dashboard page.


Code

models.py

from django.contrib.auth.models import AbstractBaseUser
from django.contrib.auth.models import BaseUserManager
from django.contrib.auth.models import PermissionsMixin
from django.db import models
from django.utils.translation import ugettext_lazy as _


class UserManager(BaseUserManager):
    """
    A custom user manager to deal with emails as unique identifiers for auth
    instead of usernames. The default that's used is "UserManager"
    """

    def _create_user(self, email, password, **extra_fields):
        """
        Creates and saves a User with the given email and password.
        """

        if not email:
            raise ValueError('The Email must be set')

        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)

        return user

    def create_user(self, email, password=None, **extra_fields):
        """
        Create and save a regular User with the given email and password.
        """

        extra_fields.setdefault('is_staff', False)
        extra_fields.setdefault('is_superuser', False)

        return self._create_user(email, password, **extra_fields)

    def create_superuser(self, email, password, **extra_fields):
        """
        Create and save a SuperUser with the given email and password.
        """

        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)
        extra_fields.setdefault('is_active', True)

        if extra_fields.get('is_staff') is not True:
            raise ValueError('Superuser must have is_staff=True.')

        if extra_fields.get('is_superuser') is not True:
            raise ValueError('Superuser must have is_superuser=True.')

        return self._create_user(email, password, **extra_fields)


class User(AbstractBaseUser, PermissionsMixin):
    email = models.EmailField(unique=True, null=True, max_length=64)
    first_name = models.CharField(max_length=64)
    last_name = models.CharField(max_length=64)

    is_staff = models.BooleanField(
        _('staff status'),
        default=False,
        help_text=_('Designates whether the user can log into this site.'),
    )
    is_active = models.BooleanField(
        _('active'),
        default=True,
        help_text=_(
            'Designates whether this user should be treated as active. '
            'Unselect this instead of deleting accounts.'
        ),
    )
    USERNAME_FIELD = 'email'
    objects = UserManager()

    def __str__(self):
        return self.email

    def get_full_name(self):
        return self.email

    def get_short_name(self):
        return self.email

forms.py

from django.contrib.auth import get_user_model
from django.forms import ModelForm

User = get_user_model()


class RegistrationForm(ModelForm):

    class Meta:
        model = User
        fields = ('email', 'first_name', 'last_name')

    def save(self, commit=True):
        user = super(RegistrationForm, self).save(commit=False)
        if commit:
            user.save()
        return user

views.py

from django.contrib import messages
from django.contrib.auth import update_session_auth_hash, get_user_model, authenticate, login
from django.contrib.auth.forms import AuthenticationForm, SetPasswordForm
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.views import LoginView, LogoutView
from django.contrib.sites.shortcuts import get_current_site
from django.core.mail import EmailMessage
from django.http import HttpResponseRedirect
from django.shortcuts import render
from django.template.loader import render_to_string
from django.utils.encoding import force_bytes, force_text
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.views.generic import TemplateView, View

from .forms import RegistrationForm
from .tokens import account_activation_token

User = get_user_model()


class HomeView(LoginRequiredMixin, TemplateView):
    template_name = 'base/home.html'


class CustomLoginView(LoginView):
    template_name = 'base/login.html'
    form_class = AuthenticationForm


class CustomLogoutView(LogoutView):
    template_name = 'base/login.html'


class RegistrationView(View):

    def get(self, request):
        registration_form = RegistrationForm()
        return render(request, 'base/register.html', {'form': registration_form})

    def post(self, request):
        registration_form = RegistrationForm(request.POST)

        if registration_form.is_valid():
            user = registration_form.save(commit=False)
            user.is_active = False
            user.set_unusable_password()
            user.save()

            current_site = get_current_site(request)
            mail_subject = 'Activate your account.'
            message = render_to_string('base/activation-email.html', {
                'user': user,
                'domain': current_site.domain,
                'uid': urlsafe_base64_encode(force_bytes(user.pk)).decode(),
                'token': account_activation_token.make_token(user),
            })
            to_email = registration_form.cleaned_data.get('email')
            email = EmailMessage(
                mail_subject, message, to=[to_email]
            )
            email.send()

            return render(request, 'base/confirm-email.html')


class ActivationView(View):

    def get(self, request, uidb64, token):
        try:
            uid = force_text(urlsafe_base64_decode(uidb64))
            user = User.objects.get(pk=uid)
        except(TypeError, ValueError, OverflowError, User.DoesNotExist):
            user = None

        if user is not None and account_activation_token.check_token(user, token):
            user.is_active = True
            user.save()
            login(request, user)

            form = SetPasswordForm(request.user)
            return render(request, 'base/password-set.html', {'form': form})
        else:
            return render(request, 'base/verification-failed.html')

    def post(self, request, uidb64, token):

        try:
            uid = force_text(urlsafe_base64_decode(uidb64))
            user = User.objects.get(pk=uid)
        except(TypeError, ValueError, OverflowError, User.DoesNotExist):
            user = None

        form = SetPasswordForm(user, request.POST)

        if form.is_valid():
            user = form.save()
            update_session_auth_hash(request, user)

            user = authenticate(username=user.email, password=form.cleaned_data['new_password1'])
            login(request, user)

            return HttpResponseRedirect('/')
        else:
            messages.error(request, form.errors)
            return render(request, 'base/password-set.html', {'form': form})

urls.py

from django.conf.urls import url
from django.views.generic import TemplateView

from .views import (
    CustomLoginView, CustomLogoutView, HomeView, 
    RegistrationView, ActivationView
)

app_name = 'apps.base'

urlpatterns = [
    url(r'^$', HomeView.as_view(), name='home'),

    url(r'^login/$', CustomLoginView.as_view(), name='login'),
    url(r'^logout/$', CustomLogoutView.as_view(), name='logout'),

    url(r'^register/$', RegistrationView.as_view(), name='register'),
    url(r'^confirm_email/$', TemplateView.as_view(template_name='base/confirm-email.html'), name='confirm_email'),
    url(r'^activate/(?P<uidb64>[0-9A-Za-z_-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
        ActivationView.as_view(), name='activate'),
]

tokens.py

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.is_active)
        )


account_activation_token = TokenGenerator()

Things that I’d like to be reviewed:

  • Django Design Patterns: Did I use the correct views / forms / models in my app? Should some of the logic be written somewhere else etc…
  • Security: Is my register system secure? If not, what can I do to make it so?
  • General feedback: Anything that comes to your mind is welcome (including criticism, ideas, improvements, cats…aliens..)


Get this bounty!!!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.