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.
Create User Models and Forms¶
This section focuses on defining the user profile model
and creating forms
for user registration and profile updates.
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'
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 exampleField Type Description Example CharField A field for small- to large-sized strings. name = forms.CharField(max_length=100) EmailField A field for email addresses. email = forms.EmailField() IntegerField A field for integer values. age = forms.IntegerField() FloatField A field for floating-point numbers. price = forms.FloatField() BooleanField A field for boolean values. is_active = forms.BooleanField() DateField A field for date values. birth_date = forms.DateField() DateTimeField A field for date and time values. created_at = forms.DateTimeField() TimeField A field for time values. start_time = forms.TimeField() URLField A field for URL values. website = forms.URLField() SlugField A field for slug values. slug = forms.SlugField() ChoiceField A field for selecting a choice from a set of options. choice = forms.ChoiceField(choices=[('1', 'Option 1'), ('2', 'Option 2')]) MultipleChoiceField A field for selecting multiple choices from a set of options. choices = forms.MultipleChoiceField(choices=[('1', 'Option 1'), ('2', 'Option 2')]) FileField A field for uploading files. file = forms.FileField() ImageField A field for uploading image files. image = forms.ImageField() DecimalField A field for decimal values with fixed precision. price = forms.DecimalField(max_digits=10, decimal_places=2) DurationField A field for storing durations of time. duration = forms.DurationField() UUIDField A field for storing universally unique identifiers. uuid = forms.UUIDField() JSONField A field for storing JSON-encoded data. metadata = forms.JSONField() ModelChoiceField A field for selecting a single model instance. author = forms.ModelChoiceField(queryset=Author.objects.all()) ModelMultipleChoiceField A field for selecting multiple model instances. authors = forms.ModelMultipleChoiceField(queryset=Author.objects.all()) RegexField A field for validating input against a regular expression. zip_code = forms.RegexField(regex=r'^\d{5}$') IPAddressField A field for validating IPv4 addresses. ip_address = forms.IPAddressField() GenericIPAddressField A field for validating IPv4 and IPv6 addresses. ip_address = forms.GenericIPAddressField(protocol='both') TypedChoiceField A field like `ChoiceField`, but returns a value of a specific type. choice = forms.TypedChoiceField(choices=[('1', 'Option 1'), ('2', 'Option 2')], coerce=int) TypedMultipleChoiceField A field like `MultipleChoiceField`, but returns values of a specific type. choices = forms.TypedMultipleChoiceField(choices=[('1', 'Option 1'), ('2', 'Option 2')], coerce=int) SplitDateTimeField A field for entering date and time separately. appointment = forms.SplitDateTimeField() ComboField A field that combines multiple fields into one. combo = forms.ComboField(fields=[forms.CharField(max_length=10), forms.EmailField()]) MultiValueField A field that validates multiple values. multi = forms.MultiValueField(fields=[forms.CharField(), forms.CharField()]) SplitHiddenDateTimeField A field like `SplitDateTimeField`, but renders as hidden widgets. hidden = forms.SplitHiddenDateTimeField()
Create Views for User Authentication¶
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
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')), ..., ]
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.
Login and Logout Page¶
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. 'registration/login.html' 'users/login.html' authentication_form The form class to use for validating the login. AuthenticationForm MyCustomAuthenticationForm redirect_authenticated_user Whether to redirect authenticated users who access the login page. False True extra_context A dictionary of context data to add to the default context. {} {'next': 'somewhere'} redirect_field_name The name of a GET parameter that contains the URL to redirect to after login. 'next' 'next_page' success_url URL to redirect to after a successful login. None '/profile/'
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). None 'users/logout.html' next_page URL to redirect to after logging out. None '/login/' redirect_field_name The name of a GET parameter that contains the URL to redirect to after logout. 'next' 'next_page' extra_context A dictionary of context data to add to the default context. {} {'info': 'You have been logged out.'}
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 %}
Step 3: Add Conditional Statements in the Navigation Bar¶
Update your navigation bar template to show different options based on the user's authentication status.
<!-- Navbar template --> ... <div class="navbar-nav ml-auto"> {% if user.is_authenticated %} <a class="nav-item nav-link" href="{% url 'profile' %}">Profile</a> <a class="nav-item nav-link" href="{% url 'logout' %}">Logout</a> {% else %} <a class="nav-item nav-link" href="{% url 'login' %}">Login</a> <a class="nav-item nav-link" href="{% url 'register' %}">Register</a> {% endif %} </div> ...
- In Django, the
user
object, typically an instance ofdjango.contrib.auth.models.User
, has various attributes and methods that provide information about the user and enable interaction with user data.
Attributes (Parameters)¶
Attribute | Purpose | Example Usage |
---|---|---|
username | The username of the user. | {{ user.username }} |
first_name | The first name of the user. | {{ user.first_name }} |
last_name | The last name of the user. | {{ user.last_name }} |
The email address of the user. | {{ user.email }} | |
is_staff | True if the user is allowed to access the admin site. | {{ user.is_staff }} |
is_active | True if the user account is active. | {{ user.is_active }} |
is_superuser | True if the user has all permissions without explicitly assigning them. | {{ user.is_superuser }} |
last_login | The date and time of the user's last login. | {{ user.last_login }} |
date_joined | The date and time when the user account was created. | {{ user.date_joined }} |
groups | A related manager for handling the groups the user belongs to. | {{ user.groups.all }} |
user_permissions | A related manager for handling the permissions the user has. | {{ user.user_permissions.all }} |
Methods (Functions)¶
Method | Purpose | Example Usage |
---|---|---|
get_username() | Returns the username for the user. | {{ user.get_username }} |
get_full_name() | Returns the first name and the last name with a space in between. | {{ user.get_full_name }} |
get_short_name() | Returns the first name of the user. | {{ user.get_short_name }} |
set_password(raw_password) | Sets the user's password to the given raw string, taking care of the password hashing. | user.set_password('new_password') |
check_password(raw_password) | Returns True if the given raw string is the correct password for the user. | user.check_password('password') |
set_unusable_password() | Marks the user as having no password set. | user.set_unusable_password() |
has_usable_password() | Returns True if the user has a usable password. | user.has_usable_password() |
email_user(subject, message, from_email=None, **kwargs) | Sends an email to the user. | user.email_user('Subject', 'Message') |
get_group_permissions(obj=None) | Returns a list of permission strings that this user has through their groups. | user.get_group_permissions() |
get_all_permissions(obj=None) | Returns a list of permission strings that this user has, both through group and user permissions. | user.get_all_permissions() |
has_perm(perm, obj=None) | Returns True if the user has the specified permission. | user.has_perm('app_label.permission_code') |
has_perms(perms, obj=None) | Returns True if the user has each of the specified permissions. | user.has_perms(['app_label.perm1', 'perm2']) |
has_module_perms(app_label) | Returns True if the user has any permissions in the given app label. | user.has_module_perms('app_label') |
is_authenticated (property) | Always True for any user. This is a way to tell if the user has been authenticated. | {% if user.is_authenticated %}...{% endif %} |
is_anonymous (property) | Always False for any user. This is a way to tell if the user is not authenticated. | {% if user.is_anonymous %}...{% endif %} |
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'
Profile and Picture (Customize forms)¶
Install
pillow
for image processingpip install pillow
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/'
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'), ]
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')
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 %}
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()
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'))
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)
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 %}
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)
Additional Resource¶
Commonly Used Decorators¶
Decorator | Purpose | Use Example |
---|---|---|
@login_required | Ensures that the user is authenticated before accessing the view. | @login_required def profile(request): return render(request, 'profile.html') |
@permission_required | Ensures that the user has a specific permission before accessing the view. | @permission_required('app_label.permission_code') def my_view(request): ... |
@user_passes_test | Allows access to the view if a given test is passed (e.g., custom check). | @user_passes_test(lambda u: u.is_superuser) def my_view(request): ... |
@csrf_exempt | Exempts the view from CSRF verification. | @csrf_exempt def my_view(request): ... |
@require_http_methods | Restricts the allowed HTTP methods for the view. | @require_http_methods(["GET", "POST"]) def my_view(request): ... |
@require_GET | Restricts the view to handle only GET requests. | @require_GET def my_view(request): ... |
@require_POST | Restricts the view to handle only POST requests. | @require_POST def my_view(request): ... |
@cache_page | Caches the view’s response for a specified amount of time. | @cache_page(60 * 15) def my_view(request): ... |
@never_cache | Prevents caching of the view’s response. | @never_cache def my_view(request): ... |
@vary_on_headers | Varies the cache based on specific headers. | @vary_on_headers('User-Agent') def my_view(request): ... |
@method_decorator | Applies a decorator to a class-based view method. | @method_decorator(login_required, name='dispatch') class MyView(View): ... |
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 |