Unit 6: Cookies and Sessions, users and authentication

INT253 — Web Development In Python Using Django 9 min read

I. Orientation: State on a Stateless Protocol

HTTP is stateless: each request arrives with no memory of the last one. Cookies (standardised as RFC 6265) solved this by letting a server ask the browser to store a small key–value pair and return it on every subsequent request to that domain. Django wraps this raw mechanism in two contributed applications shipped since Django 1.0 (2008): django.contrib.sessions for anonymous server-side state, and django.contrib.auth for identity, permissions and password handling.

  • Where state is read: HttpRequestrequest.COOKIES (a plain dict of strings), request.session (a dict-like SessionBase object), request.user (a User or AnonymousUser).
  • Where state is written: HttpResponse — cookies are set on the response (response.set_cookie(...)), never on the request.
  • Middleware order matters: in settings.MIDDLEWARE, SessionMiddleware must precede AuthenticationMiddleware, because request.user is resolved from a user id stored inside the session.
  • Required apps: django.contrib.sessions and django.contrib.auth (plus django.contrib.contenttypes, which permissions depend on) in INSTALLED_APPS.
  • Default cookie names: sessionid for the session key, csrftoken for CSRF protection.
  • Trust convention: cookie data is client-controlled and untrusted; session data lives on the server and only the opaque session key travels over the wire.

II. Cookies and Sessions — client-side storage versus server-side storage

A. Definitions and the underlying mechanism

A cookie is a name/value string pair plus attributes, transported by the Set-Cookie response header and the Cookie request header; a session is a server-side dictionary keyed by a random session key that is itself delivered as a cookie.

  • Cookie attributes: max_age (seconds), expires (datetime), path, domain, secure (HTTPS only), httponly (invisible to JavaScript), samesite ('Lax', 'Strict', 'None').
  • Size limit: roughly 4 KB per cookie — one reason bulk data belongs in a session, not a cookie.
  • Session key: a 32-character random string; the payload is serialised (JSON by default) and stored by the configured engine, so the browser learns nothing about its contents.

B. Creating Cookies and sessions in Django

Both are created through ordinary view code; the difference is where the value ends up.

1. Cookies (client-side).

PYTHON
def set_pref(request):
    response = HttpResponse("theme saved")
    response.set_cookie('theme', 'dark',
                        max_age=60 * 60 * 24 * 30,   # 30 days
                        httponly=True, samesite='Lax')
    return response

def read_pref(request):
    theme = request.COOKIES.get('theme', 'light')    # always a string
    return HttpResponse(theme)
  • Deleting: response.delete_cookie('theme') — implemented by re-sending the cookie with an expiry in the past; path/domain must match those used when setting it.
  • Tamper-evident cookies: response.set_signed_cookie('cart', '3', salt='cart') appends an HMAC derived from SECRET_KEY; reading uses request.get_signed_cookie('cart', salt='cart', max_age=3600), which raises BadSignature if altered or KeyError if absent. Signing proves integrity, not confidentiality — the value is still readable.
  • Type caveat: everything returns as str, so int(request.COOKIES['count']) is needed for arithmetic.

2. Sessions (server-side).

PYTHON
def cart_add(request, item):
    cart = request.session.get('cart', [])
    cart.append(item)
    request.session['cart'] = cart      # marks session modified -> saved
    return HttpResponse(len(cart))
  • Dict-like API: get(), pop(), setdefault(), keys(), del request.session['cart'], plus flush() (delete data and key, issue a new one), cycle_key() (keep data, new key) and set_expiry().
  • Mutation detection: Django saves only when session.modified is True. Editing a nested object in place (request.session['cart'].append(x)) is not detected — either reassign the key or set request.session.modified = True.
  • Serialisation: the default JSONSerializer accepts only JSON-safe values, so model instances must be reduced to ids.

C. Session engines, expiry and security settings

The engine decides the storage medium; expiry settings decide the lifetime of both the record and the cookie.

  • SESSION_ENGINE options: db (default, table django_session), cache, cache_db (write-through cache), file, signed_cookies (data in the cookie itself — no server storage, but no true invalidation either).
  • Lifetime: SESSION_COOKIE_AGE defaults to 1209600 seconds (2 weeks). request.session.set_expiry(0) makes the cookie expire at browser close; set_expiry(None) restores the global default.
  • Sliding vs fixed window: SESSION_SAVE_EVERY_REQUEST = True re-saves and refreshes expiry on every request (sliding); the default False counts from the last write only.
  • Hardening: SESSION_COOKIE_SECURE = True, SESSION_COOKIE_HTTPONLY = True (default), SESSION_COOKIE_SAMESITE = 'Lax' (default).
  • Housekeeping: expired database rows are not removed automatically — run python manage.py clearsessions on a schedule.

III. Users and Authentication — the auth application

A. The User model and the authentication pipeline

django.contrib.auth.models.User is the default identity record, and authenticate() is the only sanctioned way to verify credentials.

  • Core fields: username, password (hash string), email, first_name, last_name, is_active, is_staff, is_superuser, last_login, date_joined.
  • Password storage format: <algorithm>$<iterations>$<salt>$<hash>, produced by the default PBKDF2-SHA256 hasher; hashes are never reversible.
  • Backends: AUTHENTICATION_BACKENDS defaults to ['django.contrib.auth.backends.ModelBackend'], which checks the password and rejects users whose is_active is False.
  • authenticate(request, username=..., password=...) returns a User on success and None on failure — it does not create a session.

B. Creating and Managing Users in Django

Users must be created through the manager or a form, never by assigning to password directly.

PYTHON
from django.contrib.auth import get_user_model
User = get_user_model()

u = User.objects.create_user('asha', 'asha@example.com', 'S3cret!pass')
u.set_password('newS3cret!')        # hashes in place
u.save()
u.check_password('newS3cret!')      # True
  • Manager methods: create_user() hashes the password; create_superuser() additionally sets is_staff=True and is_superuser=True.
  • Why not User(password='x'): that stores plaintext; set_password() applies the hasher.
  • Management commands: manage.py createsuperuser, manage.py changepassword <username>.
  • Deactivating rather than deleting: set is_active = False to block login while preserving foreign-key history.
  • Forms: UserCreationForm (username + two password fields, runs validators from AUTH_PASSWORD_VALIDATORS) and UserChangeForm for admin-style editing.
  • Custom user models:
    • AbstractUser: subclass to add fields while keeping username/password machinery.
    • AbstractBaseUser: subclass for a different identifier (e.g. email), defining USERNAME_FIELD and a custom manager.
    • Point AUTH_USER_MODEL = 'accounts.User' before the first migration, and always refer to the model via get_user_model() or settings.AUTH_USER_MODEL.

C. Permissions, groups and profile data

Authorisation is separate from authentication and is checked per action.

  • Auto-created permissions: for every model, four codenames — add_<model>, change_<model>, delete_<model>, view_<model>.
  • Checking: user.has_perm('shop.add_order'); superusers return True for every check.
  • Assignment: user.user_permissions.add(perm) for direct grants, user.groups.add(group) for role-based grants; Group bundles permissions for many users.
  • Extra attributes: attach a OneToOneField(settings.AUTH_USER_MODEL) profile model rather than editing User.

IV. Login, Logout and View-level Protection

A. The built-in class-based auth views

django.contrib.auth.views supplies LoginView, LogoutView, PasswordChangeView, PasswordResetView and their *Done/*Confirm partners, all rendering templates under a registration/ directory.

  • Default templates: registration/login.html for LoginView, registration/logged_out.html for LogoutView.
  • Context in login.html: a bound AuthenticationForm as form, plus next for post-login redirection.

B. Login and Logout URLs in Django

One include wires the whole set of authentication URLs; settings control where users land afterwards.

PYTHON
# project/urls.py
urlpatterns = [
    path('accounts/', include('django.contrib.auth.urls')),
]
  • Names created: login, logout, password_change, password_change_done, password_reset, password_reset_done, password_reset_confirm, password_reset_complete — referenced in templates as {% url 'login' %}.
  • Key settings: LOGIN_URL (default '/accounts/login/', the redirect target for unauthenticated access), LOGIN_REDIRECT_URL (default '/accounts/profile/'), LOGOUT_REDIRECT_URL (default None, which renders the logged-out template instead). All three accept URL names.
  • The next parameter: ?next=/orders/ on the login URL overrides LOGIN_REDIRECT_URL; Django validates it against allowed hosts to prevent open redirects.
  • Logout must be POST in current Django versions, so the template needs a form, not a link:
HTML
<form method="post" action="{% url 'logout' %}">{% csrf_token %}
  <button type="submit">Log out</button>
</form>
  • Overriding: path('accounts/login/', LoginView.as_view(template_name='shop/login.html'), name='login') placed before the include.

C. Using Django Login in Views

For custom flows, drive the session directly with the three functions from django.contrib.auth.

PYTHON
from django.contrib.auth import authenticate, login, logout

def sign_in(request):
    if request.method == 'POST':
        user = authenticate(request,
                            username=request.POST['username'],
                            password=request.POST['password'])
        if user is not None:
            login(request, user)          # writes user id + backend to session
            return redirect(request.GET.get('next', 'dashboard'))
        messages.error(request, 'Invalid credentials')
    return render(request, 'registration/login.html')

def sign_out(request):
    logout(request)                       # flushes the session entirely
    return redirect('login')
  • login(request, user): stores _auth_user_id, _auth_user_hash and _auth_user_backend in the session and calls cycle_key(), which rotates the session key to defeat session fixation.
  • logout(request): flushes session data and resets request.user to AnonymousUser; it is safe to call when nobody is logged in.
  • Identity checks: request.user.is_authenticated is a property, not a method — {% if user.is_authenticated %} in templates.
  • Password change side effect: changing a password invalidates other sessions because the stored _auth_user_hash no longer matches; call update_session_auth_hash(request, user) to keep the current one alive.

D. Restricting access, and the limits of the system

Protection is applied per view, and the framework deliberately stops short of some responsibilities.

  • Function views: @login_required redirects anonymous users to LOGIN_URL with ?next= appended; @permission_required('shop.add_order', raise_exception=True) returns HTTP 403 instead of redirecting; @user_passes_test(lambda u: u.is_staff) for arbitrary predicates.
  • Class-based views: LoginRequiredMixin and PermissionRequiredMixin, listed before the base view class in the MRO.
  • Limitations: no registration view is provided (build one with UserCreationForm); permissions are model-level, not per-object; and cookies deleted or blocked by the browser silently break both sessions and login, so any state built on them must degrade gracefully.