Chapter 8: Authentication, Authorization, and User Management - IndianTechnoEra
Latest update Android YouTube

Chapter 8: Authentication, Authorization, and User Management

Almost every web application needs users - registering, logging in, managing permissions, and controlling access. Django provides a robust authentication system out of the box. In this chapter, you'll learn how to implement user authentication, permissions, groups, and extend Django's user model for your custom needs.

8.1 Django's Built-in User Model and AbstractUser

Django comes with a default User model that includes common fields: username, password, email, first_name, last_name, is_active, is_staff, is_superuser, and date_joined. However, you often need additional fields like bio, profile picture, or phone number.

# Option 1: Using the default User model (simplest)
from django.contrib.auth.models import User

# Accessing user fields
user = User.objects.create_user(
    username='john',
    email='john@example.com',
    password='securepassword123',
    first_name='John',
    last_name='Doe'
)

# Check properties
if user.is_authenticated:
    print(f"Welcome {user.username}")
if user.is_staff:
    print("User can access admin")
if user.is_superuser:
    print("User has all permissions")

# Option 2: Extending User with OneToOneField (Profile model)
from django.db import models
from django.contrib.auth.models import User

class UserProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
    bio = models.TextField(max_length=500, blank=True)
    location = models.CharField(max_length=100, blank=True)
    birth_date = models.DateField(null=True, blank=True)
    profile_picture = models.ImageField(upload_to='profiles/', blank=True)
    phone_number = models.CharField(max_length=15, blank=True)
    
    def __str__(self):
        return f"{self.user.username}'s profile"

# Access profile
user = User.objects.get(username='john')
profile = user.profile
profile.bio = "Django developer"
profile.save()

# Option 3: Custom User Model using AbstractUser (RECOMMENDED for new projects)
# Create a custom user model from the start - even if you don't need extra fields yet
# models.py
from django.contrib.auth.models import AbstractUser
from django.db import models

class CustomUser(AbstractUser):
    # Add custom fields
    bio = models.TextField(max_length=500, blank=True)
    location = models.CharField(max_length=100, blank=True)
    birth_date = models.DateField(null=True, blank=True)
    profile_picture = models.ImageField(upload_to='profiles/', blank=True)
    phone_number = models.CharField(max_length=15, blank=True)
    
    # Add custom methods
    def get_full_name_with_bio(self):
        return f"{self.get_full_name()} - {self.bio[:50]}"
    
    def __str__(self):
        return self.email  # Use email instead of username for display
    
    class Meta:
        ordering = ['-date_joined']
# settings.py - Tell Django to use your custom user model
AUTH_USER_MODEL = 'myapp.CustomUser'  # 'app_name.ModelName'

# IMPORTANT: Set AUTH_USER_MODEL BEFORE running migrations
# If you already have migrations, you'll need to start over or do a complex migration

Interview Insight: "Why should you use a custom user model from the start?" Even if you don't need extra fields now, changing the user model later is extremely difficult (requires database restructuring). Start with `AbstractUser` for maximum flexibility.

Pro Tip: Always use `create_user()` instead of `create()` - it properly hashes the password. Never store passwords as plain text.

8.2 User Registration, Login, Logout

Implementing authentication views and templates.

# views.py
from django.shortcuts import render, redirect
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from .forms import UserRegistrationForm, UserLoginForm
from .models import CustomUser

def register_view(request):
    if request.method == 'POST':
        form = UserRegistrationForm(request.POST)
        if form.is_valid():
            user = form.save(commit=False)
            user.set_password(form.cleaned_data['password'])  # Hash password
            user.save()
            
            # Auto-login after registration
            login(request, user)
            messages.success(request, 'Registration successful! Welcome!')
            return redirect('home')
    else:
        form = UserRegistrationForm()
    
    return render(request, 'accounts/register.html', {'form': form})

def login_view(request):
    if request.method == 'POST':
        username = request.POST['username']
        password = request.POST['password']
        
        # Authenticate user
        user = authenticate(request, username=username, password=password)
        
        if user is not None:
            login(request, user)
            messages.success(request, f'Welcome back, {user.username}!')
            
            # Redirect to next parameter or home
            next_url = request.GET.get('next', 'home')
            return redirect(next_url)
        else:
            messages.error(request, 'Invalid username or password')
    
    return render(request, 'accounts/login.html')

def logout_view(request):
    logout(request)
    messages.info(request, 'You have been logged out')
    return redirect('home')

@login_required
def profile_view(request):
    return render(request, 'accounts/profile.html', {'user': request.user})

@login_required
def profile_edit_view(request):
    if request.method == 'POST':
        user = request.user
        user.first_name = request.POST.get('first_name')
        user.last_name = request.POST.get('last_name')
        user.email = request.POST.get('email')
        
        # Handle profile picture
        if request.FILES.get('profile_picture'):
            user.profile_picture = request.FILES['profile_picture']
        
        user.save()
        messages.success(request, 'Profile updated successfully!')
        return redirect('profile')
    
    return render(request, 'accounts/profile_edit.html', {'user': request.user})
# forms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm
from .models import CustomUser

class UserRegistrationForm(forms.ModelForm):
    password = forms.CharField(widget=forms.PasswordInput, label='Password')
    confirm_password = forms.CharField(widget=forms.PasswordInput, label='Confirm Password')
    
    class Meta:
        model = CustomUser
        fields = ['username', 'email', 'first_name', 'last_name']
    
    def clean_confirm_password(self):
        password = self.cleaned_data.get('password')
        confirm = self.cleaned_data.get('confirm_password')
        
        if password and confirm and password != confirm:
            raise forms.ValidationError("Passwords don't match")
        return confirm
    
    def clean_email(self):
        email = self.cleaned_data.get('email')
        if CustomUser.objects.filter(email=email).exists():
            raise forms.ValidationError("Email already registered")
        return email
<!-- templates/accounts/register.html -->
{% extends "base.html" %}

{% block content %}
<div class="auth-container">
    <h2>Create an Account</h2>
    
    <form method="post">
        {% csrf_token %}
        
        {% for field in form %}
            <div class="form-group">
                {{ field.label_tag }}
                {{ field }}
                {% if field.errors %}
                    <div class="error">
                        {% for error in field.errors %}
                            <small>{{ error }}</small>
                        {% endfor %}
                    </div>
                {% endif %}
                {% if field.help_text %}
                    <small class="help">{{ field.help_text }}</small>
                {% endif %}
            </div>
        {% endfor %}
        
        <button type="submit">Register</button>
    </form>
    
    <p>Already have an account? <a href="{% url 'login' %}">Login here</a></p>
</div>
{% endblock %}
<!-- templates/accounts/login.html -->
{% extends "base.html" %}

{% block content %}
<div class="auth-container">
    <h2>Login to Your Account</h2>
    
    <form method="post">
        {% csrf_token %}
        
        <div class="form-group">
            <label>Username or Email:</label>
            <input type="text" name="username" required>
        </div>
        
        <div class="form-group">
            <label>Password:</label>
            <input type="password" name="password" required>
        </div>
        
        <button type="submit">Login</button>
    </form>
    
    <p><a href="{% url 'password_reset' %}">Forgot Password?</a></p>
    <p>Don't have an account? <a href="{% url 'register' %}">Register here</a></p>
</div>
{% endblock %}
# urls.py - Include Django's built-in auth URLs
from django.urls import path, include
from . import views

urlpatterns = [
    path('register/', views.register_view, name='register'),
    path('login/', views.login_view, name='login'),
    path('logout/', views.logout_view, name='logout'),
    path('profile/', views.profile_view, name='profile'),
    path('profile/edit/', views.profile_edit_view, name='profile_edit'),
    
    # Django's built-in password reset URLs
    path('password-reset/', 
         include('django.contrib.auth.urls')),
]

# django.contrib.auth.urls provides:
# password_reset/, password_reset/done/, reset/<uidb64>/<token>/, reset/done/

Pro Tip: Use `@login_required` decorator to protect views that require authentication. Add `LOGIN_URL = 'login'` in settings.py to specify where unauthenticated users are redirected.

8.3 Password Reset, Change, and Email Integration

Django provides built-in password reset views that handle token generation and email sending.

# settings.py - Configure email for password reset
# For development - print emails to console
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

# For production - use SMTP
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = 'your-email@gmail.com'
EMAIL_HOST_PASSWORD = 'your-app-password'
DEFAULT_FROM_EMAIL = 'noreply@yourdomain.com'

# Customize password reset email
PASSWORD_RESET_TIMEOUT = 86400  # 24 hours (in seconds)
<!-- templates/registration/password_reset_form.html -->
{% extends "base.html" %}

{% block content %}
<h2>Reset Password</h2>
<p>Enter your email address and we'll send you a link to reset your password.</p>

<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Send Reset Link</button>
</form>
{% endblock %}
<!-- templates/registration/password_reset_email.html -->
{% autoescape off %}
Hello {{ user.get_username }},

You requested to reset your password for your account at {{ site_name }}.

Click the link below to reset your password:
{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}

This link will expire in 24 hours.

If you didn't request this, please ignore this email.

Thanks,
The {{ site_name }} Team
{% endautoescape %}
# Custom password change view
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.forms import PasswordChangeForm

@login_required
def change_password_view(request):
    if request.method == 'POST':
        form = PasswordChangeForm(request.user, request.POST)
        if form.is_valid():
            user = form.save()
            # Important: Update session to prevent logout
            update_session_auth_hash(request, user)
            messages.success(request, 'Your password was successfully updated!')
            return redirect('profile')
        else:
            messages.error(request, 'Please correct the error below.')
    else:
        form = PasswordChangeForm(request.user)
    
    return render(request, 'accounts/change_password.html', {'form': form})

Common Mistake: Not calling `update_session_auth_hash()` after password change. Without it, the user gets logged out immediately.

8.4 Permissions: Creating, Assigning, and Checking Permissions

Django has a granular permission system. Each model gets three default permissions: add, change, delete, and view.

# models.py - Custom permissions
class Post(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    author = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
    is_published = models.BooleanField(default=False)
    
    class Meta:
        permissions = [
            ("can_publish_post", "Can publish/unpublish posts"),
            ("can_archive_post", "Can archive old posts"),
            ("can_view_draft", "Can view draft posts"),
        ]

# Creating permissions programmatically
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType

content_type = ContentType.objects.get_for_model(Post)
permission = Permission.objects.create(
    codename='can_moderate_comments',
    name='Can moderate comments',
    content_type=content_type,
)

# Assigning permissions to users and groups
from django.contrib.auth.models import User, Group, Permission

# Assign to individual user
user = CustomUser.objects.get(username='john')
permission = Permission.objects.get(codename='can_publish_post')
user.user_permissions.add(permission)
user.user_permissions.remove(permission)
user.user_permissions.clear()

# Assign to group (better for role-based access)
editors_group, created = Group.objects.get_or_create(name='Editors')
editors_group.permissions.add(permission)

# Add user to group
user.groups.add(editors_group)

# Checking permissions in views
from django.contrib.auth.decorators import permission_required

@permission_required('blog.can_publish_post', raise_exception=True)
def publish_post(request, post_id):
    post = Post.objects.get(id=post_id)
    post.is_published = True
    post.save()
    return redirect('post_detail', pk=post_id)

# Checking in templates
{% if perms.blog.can_publish_post %}
    <a href="{% url 'publish_post' post.id %}">Publish</a>
{% endif %}

# Checking in code
if request.user.has_perm('blog.can_publish_post'):
    # User has permission
    pass

# Check if user is in group
if request.user.groups.filter(name='Editors').exists():
    pass

Pro Tip: Use groups for role-based access control (RBAC). Create groups like 'Authors', 'Editors', 'Admins' and assign permissions to groups, not individual users.

8.5 Groups and Custom Permission Logic

Complex authorization often requires custom logic beyond simple permissions.

# Custom permission checking in views
from django.core.exceptions import PermissionDenied

def edit_post(request, post_id):
    post = Post.objects.get(id=post_id)
    
    # Custom logic: Allow author OR editor OR superuser
    if (post.author == request.user or 
        request.user.has_perm('blog.can_edit_any_post') or 
        request.user.is_superuser):
        # Edit post
        pass
    else:
        raise PermissionDenied("You don't have permission to edit this post")

# Decorator with custom logic
from django.contrib.auth.decorators import user_passes_test

def is_author_or_editor(user, post_id):
    from .models import Post
    post = Post.objects.get(id=post_id)
    return user == post.author or user.has_perm('blog.can_edit_any_post')

@user_passes_test(lambda u: u.is_staff)
def admin_dashboard(request):
    return render(request, 'admin_dashboard.html')

# Custom template tag for complex checks
# templatetags/auth_extras.py
from django import template
from blog.models import Post

register = template.Library()

@register.filter
def can_edit(user, post):
    return user == post.author or user.has_perm('blog.can_edit_any_post')

# In template
{% if user|can_edit:post %}
    <a href="{% url 'edit_post' post.id %}">Edit</a>
{% endif %}

# Class-based view permissions
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin, UserPassesTestMixin
from django.views.generic import UpdateView

class PostUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UserPassesTestMixin, UpdateView):
    model = Post
    fields = ['title', 'content']
    permission_required = 'blog.change_post'
    
    def test_func(self):
        post = self.get_object()
        return self.request.user == post.author
    
    def handle_no_permission(self):
        from django.shortcuts import redirect
        return redirect('post_list')

Interview Insight: "What's the difference between permissions and groups?" Permissions are individual actions (can_publish, can_edit). Groups are collections of permissions for role-based access. Use groups to assign multiple permissions at once (e.g., 'Editor' group has publish, edit, delete permissions).

8.6 Social Authentication with django-allauth

Allow users to login with Google, GitHub, Facebook, etc.

# Install
# pip install django-allauth

# settings.py
INSTALLED_APPS = [
    'django.contrib.sites',
    'allauth',
    'allauth.account',
    'allauth.socialaccount',
    'allauth.socialaccount.providers.google',
    'allauth.socialaccount.providers.github',
]

SITE_ID = 1

AUTHENTICATION_BACKENDS = [
    'django.contrib.auth.backends.ModelBackend',
    'allauth.account.auth_backends.AuthenticationBackend',
]

# Provider specific settings
SOCIALACCOUNT_PROVIDERS = {
    'google': {
        'APP': {
            'client_id': 'your-google-client-id',
            'secret': 'your-google-secret',
            'key': ''
        },
        'SCOPE': ['profile', 'email'],
        'AUTH_PARAMS': {'access_type': 'online'},
    },
    'github': {
        'APP': {
            'client_id': 'your-github-client-id',
            'secret': 'your-github-secret',
        }
    }
}

LOGIN_REDIRECT_URL = '/'
ACCOUNT_LOGOUT_REDIRECT_URL = '/'
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_USERNAME_REQUIRED = False
ACCOUNT_AUTHENTICATION_METHOD = 'email'

# urls.py
urlpatterns = [
    path('accounts/', include('allauth.urls')),
]

# Template
<a href="{% provider_login_url 'google' %}">Login with Google</a>
<a href="{% provider_login_url 'github' %}">Login with GitHub</a>

8.7 Custom User Models (Extending vs Substituting)

Three ways to customize the user model, each with trade-offs.

# Method 1: Proxy Model (No new database table)
class Organizer(AbstractUser):
    class Meta:
        proxy = True
        permissions = [
            ("can_manage_events", "Can manage events"),
        ]
    
    def manage_event(self):
        # Custom method
        pass

# Method 2: AbstractUser (Add fields to existing user table)
class CustomUser(AbstractUser):
    bio = models.TextField(blank=True)
    website = models.URLField(blank=True)
    # These fields are added to the user table

# Method 3: AbstractBaseUser (Full control, most complex)
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin

class CustomUserManager(BaseUserManager):
    def create_user(self, email, password=None, **extra_fields):
        if not email:
            raise ValueError('Email is required')
        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save()
        return user
    
    def create_superuser(self, email, password=None, **extra_fields):
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)
        return self.create_user(email, password, **extra_fields)

class CustomUser(AbstractBaseUser, PermissionsMixin):
    email = models.EmailField(unique=True)
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    is_active = models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)
    date_joined = models.DateTimeField(auto_now_add=True)
    
    objects = CustomUserManager()
    
    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['first_name', 'last_name']
    
    def get_full_name(self):
        return f"{self.first_name} {self.last_name}"
    
    def __str__(self):
        return self.email

Recommendation:

  • New projects: Use `AbstractUser`
  • Existing projects with default User: Use `OneToOneField` Profile model
  • Complete custom auth (email login only): Use `AbstractBaseUser`

Authentication Best Practices Checklist

  • ✅ Use `AbstractUser` for custom user models in new projects
  • ✅ Always hash passwords with `set_password()`
  • ✅ Use `@login_required` and `@permission_required` decorators
  • ✅ Set `LOGIN_URL` and `LOGIN_REDIRECT_URL` in settings
  • ✅ Use HTTPS in production (always!)
  • ✅ Implement rate limiting on login attempts
  • ✅ Use strong password validation
  • ✅ Never log passwords or sensitive data
  • ✅ Implement session timeout for sensitive applications
  • ✅ Use `update_session_auth_hash()` after password change

Summary

In this chapter, we covered:

  • Django's built-in User model and extending with AbstractUser
  • User registration, login, and logout implementation
  • Password reset and change functionality with email integration
  • Permissions system - creating, assigning, and checking permissions
  • Groups and custom permission logic for complex authorization
  • Social authentication with django-allauth
  • Different approaches to custom user models

Your application now has a complete authentication system! In the next chapter, we'll explore Django's powerful admin panel customization.

إرسال تعليق

Feel free to ask your query...
Cookie Consent
We serve cookies on this site to analyze traffic, remember your preferences, and optimize your experience.
Oops!
It seems there is something wrong with your internet connection. Please connect to the internet and start browsing again.
AdBlock Detected!
We have detected that you are using adblocking plugin in your browser.
The revenue we earn by the advertisements is used to manage this website, we request you to whitelist our website in your adblocking plugin.
Site is Blocked
Sorry! This site is not available in your country.