14. User Authentication Tutorial#
Django
provides a robust authentication system that handles user accounts, groups, permissions, and cookie-based user sessions.In this tutorial, we will learn how to create an
authentication system
for a Django application. This involves setting up user registration, login, and logout functionality, and restricting access to certain pages.
14.1. Set up User Authentication app#
14.1.1. Register app#
First, create a new app named
users
:python manage.py startapp users
Add the app to
INSTALLED_APPS
indjango_project/settings.py
:INSTALLED_APPS = [ "users.apps.UsersConfig", ... ]
14.2. Create User Models and Forms#
This section focuses on defining the user profile model
and creating forms
for user registration and profile updates.
14.2.1. Setup Crispy environment#
install the required libraries
pip install django-crispy-forms pip install crispy-bootstrap4
Add the app to
INSTALLED_APPS
indjango_project/settings.py
:INSTALLED_APPS = [ "crispy_forms", "crispy_bootstrap4", ... ]
Set up the crispy environment in
django_project/settings.py
:CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap4" CRISPY_TEMPLATE_PACK = 'bootstrap4'
14.2.2. Create forms in users/forms.py
#
Since
User
is a build-in model indjango.contrib.auth
, we do not need to construct our own model again.Meta
Class: TheMeta
class is used to specify which model the form is associated with (User
) and which fields should be included in the form.
from django import forms
from django.contrib.auth.models import User
from django.contrib.auth.forms import UserCreationForm
from .models import UserProfile
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit
class UserRegisterForm(UserCreationForm):
email = forms.EmailField()
class Meta:
model = User
fields = ['username', 'email', 'password1', 'password2']
Here is a table listing the
supported fields
inDjango forms
along with a brief description and an example
Field Type |
Description |
Example |
---|---|---|
CharField |
A field for small- to large-sized strings. |
|
EmailField |
A field for email addresses. |
|
IntegerField |
A field for integer values. |
|
FloatField |
A field for floating-point numbers. |
|
BooleanField |
A field for boolean values. |
|
DateField |
A field for date values. |
|
DateTimeField |
A field for date and time values. |
|
TimeField |
A field for time values. |
|
URLField |
A field for URL values. |
|
SlugField |
A field for slug values. |
|
ChoiceField |
A field for selecting a choice from a set of options. |
|
MultipleChoiceField |
A field for selecting multiple choices from a set of options. |
|
FileField |
A field for uploading files. |
|
ImageField |
A field for uploading image files. |
|
DecimalField |
A field for decimal values with fixed precision. |
|
DurationField |
A field for storing durations of time. |
|
UUIDField |
A field for storing universally unique identifiers. |
|
JSONField |
A field for storing JSON-encoded data. |
|
ModelChoiceField |
A field for selecting a single model instance. |
|
ModelMultipleChoiceField |
A field for selecting multiple model instances. |
|
RegexField |
A field for validating input against a regular expression. |
|
IPAddressField |
A field for validating IPv4 addresses. |
|
GenericIPAddressField |
A field for validating IPv4 and IPv6 addresses. |
|
TypedChoiceField |
A field like |
|
TypedMultipleChoiceField |
A field like |
|
SplitDateTimeField |
A field for entering date and time separately. |
|
ComboField |
A field that combines multiple fields into one. |
|
MultiValueField |
A field that validates multiple values. |
|
SplitHiddenDateTimeField |
A field like |
|
14.3. Create Views for User Authentication#
14.3.1. Create register
view#
If
request.method == POST
, we get the POST data and check the validation, if passed, we useform.save
to push data into our database.If it’s a
GET
request, we just render theregister.html
templatefrom django.contrib import messages from .forms import UserRegisterForm # Create your views here. def register(request): if request.method == 'POST': form = UserRegisterForm(request.POST) if form.is_valid(): form.save() username = form.cleaned_data.get('username') messages.success(request, f'Account created for {username}!') return redirect('blog-home') else: form = UserRegisterForm() return render(request, 'users/register.html', {'form': form})
Django's messages framework
allows you to store messages in one request and retrieve them for display in a subsequent request. It’s often used to provide feedback to the user.Example in Views
# Example in a view messages.success(request, 'Your message here')
Handling Messages in Templates:
{% for message in messages %} <div class="alert alert-{{ message.tags }}"> {{ message }} </div> {% endfor %}
Message Levels:
messages.debug
messages.info
messages.success
messages.warning
messages.error
14.3.2. Setting Up URL Patterns#
Define URLs in
users/urls.py
:from django.urls import path from . import views urlpatterns = [ path('register/', views.register, name='register'), # path('profile/', views.profile, name='profile'), ]
Include users URLs in your project’s
django_project/urls.py
:urlpatterns = [ ..., path('users/', include('users.urls')), ..., ]
14.3.3. Creating Templates#
Create
register.html
inusers/templates/users/register.html
:{% extends "blog/main.html" %} {% load crispy_forms_tags %} {% block main-content %} <div class="container"> <h2>Join Today</h2> {% for message in messages %} <div class="alert alert-{{ message.tags }}"> {{ message }} </div> {% endfor %} <form method="POST"> {% csrf_token %} {{ form|crispy }} <div class="d-flex justify-content-end"> <button type="submit" class="btn btn-primary me-2">Register</button> <a href="{% url 'blog-home' %}" class="btn btn-danger">Cancel</a> </div> </form> <div class="border-top mt-3"> <small class="text-muted"> Already Have An Account? <a class="ml-2" href="#">Sign In</a> </small> </div> </div> {% endblock main-content %}
Button Alignment: The
d-flex justify-content-end
class places both buttons on the right side of the form row.Button Spacing: The
me-2
class adds a margin to the right of the Register button, creating space between the buttons.Cancel Button: The cancel button is styled as a danger button (
btn btn-danger
) to differentiate it from the primary action.
14.4. Login and Logout Page#
14.4.1. Step 1: Construct the URL Patterns#
First, you’ll need to define the URL patterns for the login and logout views using Django’s built-in views.
# users/urls.py from django.urls import path from django.contrib.auth.views import LoginView, LogoutView urlpatterns = [ ... path('login/', LoginView.as_view(template_name='users/login.html'), name='login'), path('logout/', LogoutView.as_view(template_name='users/logout.html'), name='logout'), ... ]
LoginView
is used to handle user login. It displays a login form and processes the login request.
Parameter |
Description |
Default Value |
Example Value |
---|---|---|---|
template_name |
The name of the template to use for displaying the login form. |
|
|
authentication_form |
The form class to use for validating the login. |
|
|
redirect_authenticated_user |
Whether to redirect authenticated users who access the login page. |
|
|
extra_context |
A dictionary of context data to add to the default context. |
|
|
redirect_field_name |
The name of a GET parameter that contains the URL to redirect to after login. |
|
|
success_url |
URL to redirect to after a successful login. |
|
|
LogoutView
is used to handle user logout. It logs the user out and redirects them to a specified URL.
Parameter |
Description |
Default Value |
Example Value |
---|---|---|---|
template_name |
The name of the template to use for displaying a post-logout message (if any). |
|
|
next_page |
URL to redirect to after logging out. |
|
|
redirect_field_name |
The name of a GET parameter that contains the URL to redirect to after logout. |
|
|
extra_context |
A dictionary of context data to add to the default context. |
|
|
14.4.2. Step 2: Create the Templates for Login and Logout Pages#
Next, create the templates for the login and logout pages. Use Django’s template inheritance and crispy forms for better styling.
users/templates/users/login.html
{% extends "blog/main.html" %} {% load crispy_forms_tags %} {% block main-content %} <div class="container"> <h2>Log In</h2> {% for message in messages %} <div class="alert alert-{{ message.tags }}">{{ message }}</div> {% endfor %} <form method="POST"> {% csrf_token %} {{ form|crispy }} <div class="d-flex justify-content-end"> <button type="submit" class="btn btn-primary me-2 mt-2">Login</button> <a href="{% url 'blog-home' %}" class="btn btn-danger mt-2">Cancel</a> </div> </form> <div class="border-top mt-3"> <small class="text-muted"> Need an account? <a class="ml-2" href="{% url 'register' %}">Sign Up Now</a> </small> </div> </div> {% endblock main-content %}
users/templates/users/logout.html
{% extends "blog/main.html" %} {% block main-content %} <div class="container"> <h2>You have been logged out</h2> <a href="{% url 'login' %}" class="btn btn-primary mt-2">Log In Again</a> </div> {% endblock main-content %}
14.4.4. Step 4: Configure Settings#
Set the login redirect URL and the login URL in your
django_project/setting.py
LOGIN_URL
: This setting defines the URL where users will be redirected if they need to log in. For instance, if a user tries to access a page that requires authentication without being logged in, they will be redirected to this URL.LOGIN_REDIRECT_URL
: This setting defines the URL where users will be redirected after successfully logging in.# settings.py LOGIN_REDIRECT_URL = 'profile' LOGIN_URL = 'login'
14.5. Profile and Picture (Customize forms)#
Install
pillow
for image processingpip install pillow
14.5.1. Step 1: Create UserProfile Model and configure Media settings#
The
UserProfile
model is an extension of the default DjangoUser
model.It allows us to store additional information about the
user
, such as a bio, profile picture, and birth date.## users/models.py from django.db import models from django.contrib.auth.models import User class UserProfile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) bio = models.TextField(default="This user does not have a bio", blank=True) image = models.ImageField(default='default.png', upload_to='profile_pics') birth_date = models.DateField(null=True, blank=True) def __str__(self): return f'{self.user.username} Profile'
Define where uploaded media files (like profile pictures) are stored and how they are accessed.
# django_project/settings.py import os ... MEDIA_ROOT = os.path.join(BASE_DIR, 'media') MEDIA_URL = '/media/'
14.5.2. Step 2: Construct the URL Pattern#
Define the URL pattern to access the user’s profile page.
## users/urls.py from django.urls import path from . import views urlpatterns = [ ..., path('profile/', views.profile, name='profile'), ]
14.5.3. Step 3: Create Profile View#
The view handles the logic of rendering the user profile page.
It ensures that only
authenticated users
can access the profile page.## users/views.py from django.contrib.auth.decorators import login_required from django.shortcuts import render ... @login_required def profile(request): return render(request, 'users/profile.html')
14.5.4. Step 4: Construct the Template#
It displays the user’s information and provides a form for updating the profile.
<!-- templates/users/profile.html --> {% extends "blog/main.html" %} {% load crispy_forms_tags %} {% block main-content %} <h2>User Profile</h2> <div class="content-section"> <div class="row"> <div class="col-md-3"> <img class="rounded-circle account-img" src="{{ user.userprofile.image.url }}" /> </div> <div class="col-md-9"> <h2 class="account-heading">{{ user.username }}</h2> <p class="text-secondary">{{ user.email }}</p> <ul class="list-group"> <li class="list-group-item"> <strong>Bio:</strong> {{ user.userprofile.bio }} </li> <li class="list-group-item"> <strong>Birth Date:</strong> {{ user.userprofile.birth_date|date:"F d, Y" }} </li> </ul> </div> </div> <!-- FORM HERE --> </div> {% endblock main-content %}
14.5.5. Step 5: Create Signals#
Use
Django signals
to automatically create and save aUserProfile
instance whenever a new User instance is created.This ensures that every user has a profile.
# users/signals.py from django.db.models.signals import post_save from django.contrib.auth.models import User from django.dispatch import receiver from .models import UserProfile @receiver(post_save, sender=User) def create_profile(sender, instance, created, **kwargs): if created: UserProfile.objects.create(user=instance) @receiver(post_save, sender=User) def save_profile(sender, instance, **kwargs): instance.userprofile.save()
14.5.6. Step 6: Construct Two Forms (User and UserProfile)#
Purpose: Create forms for updating user information and user profile information.
These forms will be used to handle the input from the profile page.
# users/forms.py from django import forms from django.contrib.auth.models import User from .models import UserProfile from crispy_forms.helper import FormHelper from crispy_forms.layout import Submit ... class UserUpdateForm(forms.ModelForm): email = forms.EmailField() class Meta: model = User fields = ['username', 'email'] class UserProfileForm(forms.ModelForm): class Meta: model = UserProfile fields = ['bio', 'image', 'birth_date'] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.helper = FormHelper() self.helper.add_input(Submit('update', 'Update'))
14.5.7. Step 7: Update the Profile View#
Modify the profile view to handle POST requests for form submission and update user and user profile data.
# users/views.py from django.contrib import messages from django.shortcuts import redirect ... @login_required def profile(request): if request.method == 'POST': u_form = UserUpdateForm(request.POST, instance=request.user) p_form = UserProfileForm(request.POST, request.FILES, instance=request.user.userprofile) if u_form.is_valid() and p_form.is_valid(): u_form.save() p_form.save() messages.success(request, f'Your account has been updated') return redirect('profile') else: u_form = UserUpdateForm(instance=request.user) p_form = UserProfileForm(instance=request.user.userprofile) context = { 'u_form': u_form, 'p_form': p_form } return render(request, 'users/profile.html', context=context)
14.5.8. Step 8: Update the Template#
Modify the template to include forms for updating the user and user profile information.
<!-- users/templates/users/profile.html --> {% extends "blog/main.html" %} {% load crispy_forms_tags %} {% block main-content %} <h2>{{ user.username }}'s Profile</h2> <div class="content-section"> <div class="media"> <img class="rounded-circle account-img" src="{{ user.userprofile.image.url }}" /> <div class="media-body"> <h2 class="account-heading">{{ user.username }}</h2> <p class="text-secondary">{{ user.email }}</p> <ul class="list-group"> <li class="list-group-item"> <strong>Bio:</strong> {{ user.userprofile.bio }} </li> <li class="list-group-item"> <strong>Birth Date:</strong> {{ user.userprofile.birth_date|date:"F d, Y" }} </li> </ul> </div> </div> <form method="POST" enctype="multipart/form-data"> {% csrf_token %} <fieldset class="form-group"> <legend class="border-bottom mb-4">Update Profile</legend> {{ u_form|crispy }} {{ p_form|crispy }} </fieldset> <div class="form-group"> <button class="btn btn-outline-info" type="submit">Update</button> </div> </form> </div> {% endblock main-content %}
14.5.9. Step 9: Modify the UserProfile Model to Resize Images#
Update the UserProfile model to resize profile images to a maximum of 300x300 pixels to ensure uniformity and optimize storage space.
# users/models.py from django.db import models from django.contrib.auth.models import User from PIL import Image class UserProfile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) bio = models.TextField(default="This user does not have a bio", blank=True) image = models.ImageField(default='default.png', upload_to='profile_pics') birth_date = models.DateField(null=True, blank=True) def __str__(self): return f'{self.user.username} Profile' def save(self, *args, **kwargs): super().save(*args, **kwargs) # Resize uploaded image img = Image.open(self.image.path) if img.height > 300 or img.width > 300: output_size = (300, 300) img.thumbnail(output_size) img.save(self.image.path)
14.6. Additional Resource#
14.6.1. Commonly Used Decorators#
Decorator |
Purpose |
Use Example |
---|---|---|
@login_required |
Ensures that the user is authenticated before accessing the view. |
|
@permission_required |
Ensures that the user has a specific permission before accessing the view. |
|
@user_passes_test |
Allows access to the view if a given test is passed (e.g., custom check). |
|
@csrf_exempt |
Exempts the view from CSRF verification. |
|
@require_http_methods |
Restricts the allowed HTTP methods for the view. |
|
@require_GET |
Restricts the view to handle only GET requests. |
|
@require_POST |
Restricts the view to handle only POST requests. |
|
@cache_page |
Caches the view’s response for a specified amount of time. |
|
@never_cache |
Prevents caching of the view’s response. |
|
@vary_on_headers |
Varies the cache based on specific headers. |
|
@method_decorator |
Applies a decorator to a class-based view method. |
|
14.6.2. Commonly used Signals#
Signal | Purpose | Example |
---|---|---|
post_save | Sent after a model’s save() method is called. |
Automatically create a profile for new users:from django.db.models.signals import post_save from django.contrib.auth.models import User from django.dispatch import receiver from .models import UserProfile |
pre_save | Sent before a model’s save() method is called. |
Auto-populate a field before saving:from django.db.models.signals import pre_save from django.dispatch import receiver from .models import MyModel |
post_delete | Sent after a model’s delete() method is called. |
Clean up related records after deletion:from django.db.models.signals import post_delete from django.dispatch import receiver from .models import MyModel |
pre_delete | Sent before a model’s delete() method is called. |
Perform actions before deletion:from django.db.models.signals import pre_delete from django.dispatch import receiver from .models import MyModel |
m2m_changed | Sent when a ManyToManyField is changed. |
Log changes to many-to-many relationships:from django.db.models.signals import m2m_changed from django.dispatch import receiver from .models import MyModel |
request_started | Sent when Django starts processing an HTTP request. |
Initialize per-request data:from django.core.signals import request_started from django.dispatch import receiver |
request_finished | Sent when Django finishes processing an HTTP request. |
Clean up after request processing:from django.core.signals import request_finished from django.dispatch import receiver |
user_logged_in | Sent when a user logs in. |
Track user login activities:from django.contrib.auth.signals import user_logged_in from django.dispatch import receiver |
user_logged_out | Sent when a user logs out. |
Track user logout activities:from django.contrib.auth.signals import user_logged_out from django.dispatch import receiver |
user_signup | Custom signal, often used to handle user sign-up events. |
Handle additional sign-up logic:from django.dispatch import Signal |