This page looks best with JavaScript enabled

Django User Registration and Authentication

 ·  🎃 kr0m

User registration and authentication are very common aspects of web applications. In this article, we will see step by step how Django makes all these tasks easier for us through the django.contrib.auth app. All interaction with the database users will be transparent, we will simply have to generate the templates so that the website has the desired appearance.

Before we begin, it is recommended to read the previous articles on Django as they are the previous steps to this article:


As always, activate the project’s venv:

cd rxWod
source bin/activate
cd rxWodProject/

The fastest way to create users is through the Python CLI:

python manage.py shell

>>> from django.contrib.auth.models import User  
>>> user = User.objects.create_user('kr0m2', 'kr0m2@alfaexploit.com')

We can change parameters through the user’s attributes:

>>> user.last_name = 'Awesome'  
>>> user.is_staff=True  
>>> user.set_password('PASSWORD12345')  
>>> user.save()

We can check the available attributes with the following command:

>>> print(user._meta.fields)  
<django.db.models.fields.AutoField: id>, <django.db.models.fields.CharField: password>, <django.db.models.fields.DateTimeField: last_login>, <django.db.models.fields.BooleanField: is_superuser>, <django.db.models.fields.CharField: username>, <django.db.models.fields.CharField: first_name>, <django.db.models.fields.CharField: last_name>, <django.db.models.fields.EmailField: email>, <django.db.models.fields.BooleanField: is_staff>, <django.db.models.fields.BooleanField: is_active>, <django.db.models.fields.DateTimeField: date_joined>

We can see the newly created user in the web interface:
http://localhost:8000/admin/auth/user/

The fastest way to change a password is through the CLI:

python manage.py changepassword USERNAME

Django comes with all the components enabled in the settings.py file to authenticate users, let’s check that this is the case:

vi rxWodProject/settings.py

INSTALLED_APPS = [
    ...
    'django.contrib.auth',
    'django.contrib.contenttypes',
    ...
]

MIDDLEWARE = [
    ...
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    ...
]

To register users, we will require the introduction of an email that will be validated by the user. This way, we ensure that the user and only the user can reset the password in the future.

To be able to send the email, we must configure Django with the SMTP parameters. In my case, I’m going to use a Gmail account. First, we must enable insecure access through the following link .

And we will configure the firewall rules if necessary ( manual IPFW ):

$cmd 00401 allow tcp from me to any 587 out via $wanif  
$cmd 00401 allow tcp from any 587 to me in via $wanif

The SMTP parameters will be configured in the project’s settings file, but we will externalize it to be able to commit the settings file:

vi rxWodProject/settings.py

# Email configuration
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'

email_auth_path = str(Path(__file__).resolve().parent) + '/email_auth.txt'
f = open(email_auth_path, "r")
lines = f.readlines()
f.close()

EMAIL_HOST = lines[0].strip().split(' ')[0]
EMAIL_USE_TLS = lines[1].strip().split(' ')[0]
EMAIL_PORT = lines[2].strip().split(' ')[0]
EMAIL_HOST_USER = lines[3].strip().split(' ')[0]
EMAIL_HOST_PASSWORD = lines[4].strip().split(' ')[0]

We create the file with the credentials:

vi rxWodProject/email_auth.txt

smtp.gmail.com  
True  
587  
rxworkofday@gmail.com  
PASSWORD

NOTE: In my case, emails will be sent very punctually. If the website starts to have traffic or if many emails are sent for whatever reason, we will probably have to talk to Gmail to start paying for a business account.

We check from the project’s shell that the sending works:

python manage.py shell

>>> from django.core.mail import send_mail  
>>> send_mail('Subject here', 'Here is the message.', 'rxworkofday@gmail.com',  ['kr0m@alfaexploit.com'], fail_silently=False,)  
1

The received email is the following:

Now that we have the email sending system working, we can focus on our usersAuth application.

The login URL is defined by django.contrib.auth, and we define the URL to redirect to upon successful authentication:

vi rxWodProject/settings.py

LOGIN_URL='/accounts/login'
LOGIN_REDIRECT_URL='/'

To register users, we can use the django.contrib.auth.forms(UserCreationForm) form, which will automatically perform certain validations on the form fields.

The only drawback is that it does not require the user to enter an email account for registration. Therefore, we will create our own form that extends UserCreationForm. This way, we will inherit the previous validations but can modify the required fields and perform the validations we consider appropriate.

In our case, we will check that no account has been registered with the indicated email, thus avoiding multiple accounts being registered with the same email.

vi usersAuth/forms.py

from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from django import forms

class RegisterUserForm(UserCreationForm):
    class Meta:
        model = User
        fields =  ['username', 'email', 'password1', 'password2']

    # Check unique email
    # Email exists && account active -> email_already_registered
    # Email exists && account not active -> delete previous account and register new one
    def clean_email(self):
        email_passed = self.cleaned_data.get("email")
        email_already_registered = User.objects.filter(email = email_passed).exists()
        user_is_active = User.objects.filter(email = email_passed, is_active = 1)
        if email_already_registered and user_is_active:
            #print('email_already_registered and user_is_active')
            raise forms.ValidationError("Email already registered.")
        elif email_already_registered:
            #print('email_already_registered')
            User.objects.filter(email = email_passed).delete()

        return email_passed

Now, in the register view, we import our RegisterUserForm form. The code will behave differently depending on certain conditions:

  • The user is already authenticated: We redirect them to the index.
  • The request is GET: We instantiate the user registration form and pass it to the template via context to display it.
  • The request is POST: We instantiate the user registration form, passing the POST parameters for validation.
    • If it passes the validations: We save the user as inactive, send the email with the confirmation link, and redirect them to the login window.
    • If it does not pass the validations: We generate the messages associated with the errors and redirect them to the register window, where we will display the messages generated by the view.

On the other hand, we have the activate view, which will be used to confirm the registration of users who click on the registration email link. The logic is as follows:

  • If the UUID is valid: We activate the user, generate the message, and redirect them to the login window, where we will display the messages.
  • If the UUID is invalid: We return ‘Activation link is invalid’.
vi usersAuth/views.py
from django.shortcuts import render
from django.http import HttpResponse, HttpResponseRedirect
from django.urls import reverse
from django.contrib.sites.shortcuts import get_current_site
from django.contrib import messages
from django.template.loader import render_to_string
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.utils.encoding import force_bytes
from django.contrib.auth.tokens import default_token_generator
from django.contrib.auth.models import User
from django.contrib.auth import get_user_model
UserModel = get_user_model()
from django.core.mail import send_mail
from .forms import RegisterUserForm

def register(request):
    if request.user.is_authenticated:
        print('Already authenticated')
        return HttpResponseRedirect(reverse('rxWod:index'))
    else:
        if request.method == 'POST':
            form = RegisterUserForm(request.POST)
            # RegisterUserForm is created from User model, all model field restrictions are checked to considerate it a valid form
            if form.is_valid():
                print('Valid form')
                # Save user to database but with is_active = False
                user = form.save(commit=False)
                user.is_active = False
                user.save()

                # Send confirmation email
                current_site = get_current_site(request)
                subject = 'Activate Your ' + current_site.domain + ' Account'
                message = render_to_string('usersAuth/email_confirmation.html',
                    {
                        "domain": current_site.domain,
                        "user": user,
                        "uid": urlsafe_base64_encode(force_bytes(user.pk)),
                        "token": default_token_generator.make_token(user),
                    },
                )
                to_email = form.cleaned_data.get('email')
                send_mail(subject, message, 'rxworkofday@gmail.com', [to_email])

                # Redirect user to login
                messages.success(request, 'Please Confirm your email to complete registration before Login.')
                return HttpResponseRedirect(reverse('login'))
            else:
                #print('Invalid form: %s' % form.errors.as_data())
                #print(type(form.errors.as_data()))
                if form.errors:
                    #messages.info(request, 'Input field errors:')
                    for key, values in form.errors.as_data().items():
                        #print("Bad value: %s - %s" % (key, values))
                        if key == 'username':
                            messages.info(request, 'Error input fields')
                            break
                        else:
                            for error_value in values:
                                print(error_value)
                                #print(type(error_value))
                                messages.info(request, '%s' % (error_value.message))

                return HttpResponseRedirect(reverse('usersAuth:register'))
        else:
            form = RegisterUserForm()

            context = {
                'form': form
            }
            return render(request, 'usersAuth/register.html', context)

def activate(request, uidb64, token):
    try:
        uid = urlsafe_base64_decode(uidb64).decode()
        user = UserModel._default_manager.get(pk=uid)
    except(TypeError, ValueError, OverflowError, User.DoesNotExist):
        user = None

    if user is not None and default_token_generator.check_token(user, token):
        user.is_active = True
        user.save()
        # Redirect user to login
        messages.success(request, 'Successful email confirmation, you can proceed to login.')
        return HttpResponseRedirect(reverse('login'))
    else:
        return HttpResponse('Activation link is invalid!')

NOTE: When we redirect to the login page, we don’t do it through HttpResponseRedirect(reverse(‘usersAuth:login’)) because the login URL is not provided by our usersAuth App but by django.contrib.auth, so we use HttpResponseRedirect(reverse(’login’)) instead.

We register the register and activate URLs:

vi usersAuth/urls.py

from django.urls import path
from . import views

app_name = 'usersAuth'

urlpatterns = [
    path('register/', views.register, name='register'),
    path('activate/<uidb64>/<token>/',views.activate, name='activate'),
]

For registration, we need several templates, the registration one and the one that will be used when we send the confirmation link to the client.

We create the directory where we will store the templates:

mkdir -p usersAuth/templates/usersAuth

We generate the templates:

vi usersAuth/templates/usersAuth/register.html

<form action="{% url 'usersAuth:register' %}" method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <input type="submit" class="btn btn-success" id="Register" value="Register"></input>
    {% for message in messages %}
        <p class="item" id="messages">{{ message }}</p>
    {% endfor%}
</form>
vi usersAuth/templates/usersAuth/email_confirmation.html
{% autoescape off %}
Hi {{ user.username }},

Please click on the link below to confirm your registration:

http://{{ domain }}{% url 'usersAuth:activate' uidb64=uid token=token %}
{% endautoescape %}

The rest of the authentication functionalities are provided by Django by default .


Through a cat we can see the paths and names of the views associated with each functionality provided by django.contrib.auth:

cat /home/kr0m/rxWod/lib/python3.7/site-packages/django/contrib/auth/urls.py

urlpatterns = [
    path('login/', views.LoginView.as_view(), name='login'),
    path('logout/', views.LogoutView.as_view(), name='logout'),

    path('password_change/', views.PasswordChangeView.as_view(), name='password_change'),
    path('password_change/done/', views.PasswordChangeDoneView.as_view(), name='password_change_done'),

    path('password_reset/', views.PasswordResetView.as_view(), name='password_reset'),
    path('password_reset/done/', views.PasswordResetDoneView.as_view(), name='password_reset_done'),
    path('reset/<uidb64>/<token>/', views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
    path('reset/done/', views.PasswordResetCompleteView.as_view(), name='password_reset_complete'),
]

We import the URLs from django.contrib.auth:

vi rxWodProject/urls.py

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    # django.contrib.auth URLs
    path('accounts/', include('django.contrib.auth.urls')),
    path('', include('rxWod.urls')),
    path('', include('usersAuth.urls')),
    path('admin/', admin.site.urls),
]

We create the directory where it will look for the templates:

mkdir -p usersAuth/templates/registration

If we don’t create the template, it will use the admin interface’s by default, although this is usually not the desired behavior:
http://127.0.0.1:8000/accounts/password_reset/

We create the templates, in this link we can see the default name of each template:

  • login: registration/login.html
  • logout: registration/logged_out.html
  • password_change: registration/password_change_form.html
  • password_change_done: registration/password_change_done.html
  • password_reset: registration/password_reset_form.html
  • password_reset_done: registration/password_reset_done.html
  • password_reset_confirm: registration/password_reset_confirm.html
  • password_reset_complete: registration/password_reset_complete.html
vi usersAuth/templates/registration/login.html
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <input type="submit" class="btn btn-success" id="Login" value="Login"></input>
    {% for message in messages %}
        <p class="item" id="messages">{{ message }}</p>
    {% endfor%}
</form>
vi usersAuth/templates/registration/logged_out.html
<h1>Logout with success</h1>
{% for message in messages %}
    <p class="item" id="messages">{{ message }}</p>
{% endfor%}
vi usersAuth/templates/registration/password_change_form.html
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <input type="submit" class="btn btn-success" id="Change-password" value="Change password"></input>
    {% for message in messages %}
        <p class="item" id="messages">{{ message }}</p>
    {% endfor%}
</form>
vi usersAuth/templates/registration/password_change_done.html
<h1>Password successfully changed</h1>
{% for message in messages %}
    <p class="item" id="messages">{{ message }}</p>
{% endfor%}
vi usersAuth/templates/registration/password_reset_form.html
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <input type="submit" class="btn btn-success" id="Send-email" value="Send email"></input>
    {% for message in messages %}
        <p class="item" id="messages">{{ message }}</p>
    {% endfor%}
</form>
vi usersAuth/templates/registration/password_reset_done.html
<h1>Password reset sent</h1>
<p>We've emailed you instructions for setting your password, if an account exists with the email you entered. You should receive them shortly.<br>If you don't receive an email, please make sure you've entered the address you registered with, and check your spam folder.</p>
vi usersAuth/templates/registration/password_reset_confirm.html
<h1>Password Reset Confirm</h1>
<p>Please enter your new password.</p>
<form method="POST">
    {% csrf_token %}
    {{ form.as_p }}                    
    <button class="btn btn-primary" type="submit">Reset password</button>
</form>
vi usersAuth/templates/registration/password_reset_complete.html
<h1>Password reset complete</h1>
<p>Your password has been set. You may go ahead and log in now.</p>   

To create a user, we access the /register URL:
http://127.0.0.1:8000/register

We fill in the data and click on Register:

It will redirect us to http://127.0.0.1:8000/accounts/login/ indicating that we must confirm the email link:

Whenever we access the Django administration interface, we must do it from an anonymous browser session to avoid interference with the tests we are performing with the test user. If we do not do it this way, when we log in to the administration interface, the code will detect that we are logged in and will not show the correct content.

We check the status of the new user and see that it is not active yet since they have not confirmed their email:


The user will have received an email with the confirmation link:

When the user clicks on the email link, they will be redirected to the login page where a message will be displayed indicating that their account has been confirmed and they can proceed to log in:

If we check the administration interface, we will see that the user has become active:

When the user logs in, they will see the created routines. Remember that for now, there are no filters when displaying the routines, all of them are shown regardless of the user:
http://127.0.0.1:8000/accounts/login

The page it redirects us to once authenticated is /, as defined in the settings:

LOGIN_REDIRECT_URL='/'

If we log out, we will see the following page:
http://127.0.0.1:8000/accounts/logout

If we change the password after logging out, it will ask us to log in first:
http://127.0.0.1:8000/accounts/password_change

And as soon as we log in, it will show us the password change form:

After changing the password, it will display:

If we reset the password, it will ask us for the associated email:
http://127.0.0.1:8000/accounts/password_reset

It indicates that we must follow the instructions received via email:

The user will receive an email with the following content:

By clicking on the link, the user can change the password:

The user is informed of the change:

Now they can log in with the new password.

NOTE: At the moment, we are not concerned with translations. We will cover this section in detail in later articles.

The URLs of django.contrib.auth are protected by default requiring login, but our views will be exposed. We must keep in mind that there are views that will require authentication while others will not. For example, rxWod:index should request credentials for access, while the views usersAuth:register and usersAuth:activate do not need to, as they should be accessible to the user before registering an account.

Through the login_required decorator, we can control access.

vi rxWod/views.py

from django.shortcuts import render
from django.http import HttpResponse
from .models import Routine
from django.contrib.auth.decorators import login_required

@login_required()
def index(request):
    routines = Routine.objects.order_by('id')
    context = {
        'routines': routines,
    }
    return render(request, 'rxWod/index.html', context)

If we log out and try to access the index, it will ask us for access credentials:
http://localhost:8000/accounts/logout/

We access the index and see that we go through login first:
http://localhost:8000


With this, we only require login, we do not control the content displayed. Once authenticated, the user can see all routines, not just their own. We can control this by filtering by user in the view query.

vi rxWod/views.py

from django.shortcuts import get_object_or_404, get_list_or_404, render, redirect
from django.http import HttpResponse
from .models import Routine
from django.contrib.auth.decorators import login_required

@login_required()
def index(request):
    #print('username: %s' % request.user.username)
    routines = Routine.objects.filter(user=request.user)
    context = {
        'routines': routines,
    }
    return render(request, 'rxWod/index.html', context)

If we create a new routine from the administration interface and assign it to the user jjivarspoquet:

We log out/login with jjivarspoquet to verify that they can now only see their routines:
http://127.0.0.1:8000/accounts/logout
http://127.0.0.1:8000/accounts/login

Another way to filter is through conditionals in the template, but we should always filter as early as possible, that is, if we can filter in the view, we should not wait to do it in the template.

There is a small problem in user validation, as a registered but unvalidated user will remain in the database forever. These records can accumulate, take up storage space, and ultimately degrade database performance.

To solve the problem, we will create a custom command that will clean up old users:

mkdir -p usersAuth/management/commands
vi usersAuth/management/commands/delete_limbo_users.py

# delete_limbo_users.py
import datetime
from django.core.management.base import BaseCommand
from django.contrib.auth.models import User

class Command(BaseCommand):
    help = 'Delete registered but unactive users older than the given number of days.'

    def add_arguments(self, parser):
        parser.add_argument('days', type=int, help='Retention period in days.')

    def handle(self, *args, **options):
        days = options['days']

        print('-------------------------------------------')
        print('| Checking limbo users older than: %s days |' % days)
        print('-------------------------------------------')

        retention_period = datetime.timedelta(days)
        expiry_date = datetime.date.today() - retention_period

        deleted_users = 0
        unactive_users = User.objects.filter(is_active = 0)
        for user in unactive_users:
            if user.date_joined.date() < expiry_date:
                print('>> Deleting user: : %s -- %s' % (user.username, user.date_joined))
                user.delete()
                deleted_users = deleted_users + 1
        
        if deleted_users == 0:
            print('>> No users to be deleted')

To test it, we will edit the status of the user “jjivarspoquet” and set it to inactive:

This user was registered more than 2 days ago:

If we tell the script to delete all users older than 2 days, it should delete it:

python manage.py delete_limbo_users 2

-------------------------------------------  
| Checking limbo users older than: 2 days |  
-------------------------------------------  
>> Deleting user: : jjivarspoquet -- 2021-03-01 18:58:16+00:00

If we go to the user section of the administration interface, we will see that the user no longer exists:

To schedule it with crontab, we write the following script:

vi .scripts/delete_limbo_users.sh

#!/usr/local/bin/bash
cd /home/kr0m/rxWod/
source bin/activate
cd rxWodProject/
python manage.py delete_limbo_users 2
chmod 700 .scripts/delete_limbo_users.sh

To run it every night at 12, we would set the Cron as follows:

crontab -e

* 00 * * * /home/kr0m/.scripts/delete_limbo_users.sh 2>&1
If you liked the article, you can treat me to a RedBull here