Configure URL Routing for Authentication Views
# apps/urls.py
from django.urls import path
from apps.views.account import login_view, logout_view, generate_captcha
urlpatterns = [
path('login/', login_view, name='login'),
path('logout/', logout_view, name='logout'),
path('captcha/', generate_captcha, name='captcha'),
]
Import Reuqired Modules in View Module
Create apps/views/account.py and include:
import io
from django.shortcuts import render, redirect, HttpResponse
from django import forms
from django.core.exceptions import ValidationError
from apps import models
from apps.utils.security import hash_password
from apps.utils.captcha import generate_verification_image
Define a Customm Login Form with Field Styling and Hashing
class AuthenticationForm(forms.Form):
username = forms.CharField(
label="Username",
max_length=32,
widget=forms.TextInput(),
required=True
)
secret = forms.CharField(
label="Password",
widget=forms.PasswordInput(render_value=True),
required=True
)
challenge = forms.CharField(
label="CAPTCHA",
widget=forms.TextInput(),
required=True
)
def clean_secret(self):
raw = self.cleaned_data.get("secret")
return hash_password(raw)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field_name, field in self.fields.items():
if not field.widget.attrs:
field.widget.attrs = {}
field.widget.attrs.update({
'class': 'form-control',
'placeholder': field.label
})
Implemant the Login Handler with Session Management and Validation
def login_view(request):
if request.method == 'GET':
return render(request, 'auth/login.html', {'form': AuthenticationForm()})
bound_form = AuthenticationForm(data=request.POST)
if not bound_form.is_valid():
return render(request, 'auth/login.html', {'form': bound_form})
user_input = bound_form.cleaned_data.copy()
submitted_captcha = user_input.pop('challenge')
expected_captcha = request.session.get('captcha_value')
if submitted_captcha.upper() != expected_captcha:
bound_form.add_error('challenge', 'Invalid CAPTCHA')
return render(request, 'auth/login.html', {'form': bound_form})
# Extend session lifetime to 7 days
request.session.set_expiry(60 * 60 * 24 * 7)
authenticated_user = models.Admin.objects.filter(**user_input).first()
if authenticated_user:
request.session['user_profile'] = {
'id': authenticated_user.id,
'display_name': authenticated_user.name
}
return redirect('admin_dashboard')
else:
bound_form.add_error('secret', ValidationError('Credentials do not match'))
return render(request, 'auth/login.html', {'form': bound_form})
return render(request, 'auth/login.html', {'form': bound_form})
Build the Logout Endpoint
def logout_view(request):
request.session.flush()
return redirect('login')
Serve Dynamic CAPTCHA Images
def generate_captcha(request):
image, text = generate_verification_image()
request.session['captcha_value'] = text
request.session.set_expiry(60) # 1-minute expiry
buffer = io.BytesIO()
image.save(buffer, format='PNG')
return HttpResponse(buffer.getvalue(), content_type='image/png')
Render the Login Template with Bootstrap Styling
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Sign In</title>
<link rel="stylesheet" href="{% static 'vendor/bootstrap/css/bootstrap.min.css' %}">
<style>
.auth-card { width: 420px; margin: 150px auto; }
</style>
</head>
<body>
<div class="container auth-card">
<div class="panel panel-default">
<div class="panel-heading text-center"><h3>Account Access</h3></div>
<div class="panel-body">
<form method="post">
{% csrf_token %}
<div class="form-group">
<label>{{ form.username.label }}</label>
{{ form.username }}
<span class="text-danger">{{ form.username.errors.0 }}</span>
</div>
<div class="form-group">
<label>{{ form.secret.label }}</label>
{{ form.secret }}
<span class="text-danger">{{ form.secret.errors.0 }}</span>
</div>
<div class="form-group">
<label>{{ form.challenge.label }}</label>
<div class="row">
<div class="col-sm-7">{{ form.challenge }}</div>
<div class="col-sm-5">
<img src="{% url 'captcha' %}" alt="CAPTCHA" id="captcha-img" />
</div>
</div>
<span class="text-danger">{{ form.challenge.errors.0 }}</span>
</div>
<button type="submit" class="btn btn-success btn-block">Log In</button>
</form>
</div>
</div>
</div>
<script>
document.getElementById('captcha-img').onclick = function () {
this.src = '{% url "captcha" %}?' + Math.random();
};
</script>
</body>
</html>