Using Django, DRF and Angular without cookies
Apps are often so well engineered with excellent examples it's easy to create one that "just works". Used as a starting point they can be used to quickly create and deliver something useful.
But what if you need to integrate more than one to deliver something more complex? Something where there's a requirement to deliver an app that doesn't rely on cookies?
I may be biased but Django and the related Django Rest Framework offer unparalleled flexibility to be able to quickly develop and deploy a sophisticated backend server with an elegant and enticing Angular/React frontend that doesn't rely on cookies.
This article offers an overview and code snippets for those aspects of Django, DRF and Angular that work well together to quickly deliver a flexible and modular app that doesn't rely on cookies for server state management.
By understanding the building blocks of the tools we use, why we use them, and how they fit together, it makes it easier to quickly build them into a cohesive and well engineered app. The examples are for Django and DRF with Angular but other SPAs such as React should work in a similar way.
What are the issues?
Applications often need to remember things, and cookies are still the most useful way to achieve this. Cookies have been abused in the past and, aside from moves to ban third party cookies, users are able to disable them. It's useful to find an alternative for when cookies are disabled, and this article offers one such alternative.
Cookies are different to Cross-origin resource sharing (CORS), a mechanism that allows restricted resources on a web page to be requested from another domain outside the domain from which the first resource was served.
What are the core components?
Django
Application logic and data store
Session management
DRF
API design
Angular
Interceptors
Although Django-Rest-Framework is used for the API between the server and Angular frontend, it doesn't need any modifications as Django handles session management, also only the session management aspect of Django needs the addition of a single piece of middleware to accomplish this.
This is important as the fewer parts that are affected, in this case the addition of one extra stage in processing sessions, ensures minimal risk. Equally, the Angular application has a single addition in the form of an interceptor, again ensuring minimal risk. Although there is no guarantee about the future, Django, Django-Rest-Framework and Angular have all gone through significant upgrades with no changes to the additional session handling code.
Session management
Although it's possible to run a Django site without cookies, many services such as Authentication and Messages require SessionMiddleware so it's hard to avoid them.
Single Page Apps without Cookies
So what needs to happen to ensure sessions are maintained when cookies are refused?
From the Django app, intercept outgoing cookies during middleware processing, then add an http header 'X-Session' to be consumed by the SPA. Within the Angular app, Interceptors are used to capture the incoming X-Session, store it, then add it to all outgoing requests.
Order is important so it's essential to ensure:
Django outgoing
After a cookie is set, capture the session id then set the X-Session header
Angular incoming
Capture and store the X-Session id
Angular outgoing
Add an X-Session HTTP header with the stored id
Django incoming
Capture the X-Session HTTP header and re-set the session cookie for use by the app. It's essential to restore the request session after SessionMiddleware.
By using and adapting normal Angular and Django behaviour there will be few if any surprises and applications will "Just Work". Although there is no guarantee about the future, Django, Django-Rest-Framework and Angular have all gone through significant upgrades with no changes to the additional session handling code.
HTTP headers
Why use X-Session as the header? There are many standardized headers such as the following:
Accept-Language: de; q=1.0, en; q=0.5
Non standard ones tend to be prefixed with X-, though this was deprecated in 2012.
Django Session Middleware
""" Session middleware to store a session in the header
This must be called after django.contrib.sessions.middleware.SessionMiddleware
Copyright (c) 2014-2021, Persistent Objects Ltd https://p-o.co.uk/ License: BSD
""" from importlib import import_module from django.conf import settings from django.core.exceptions import SuspiciousOperation from django.contrib.sessions.backends.base import UpdateError import logging logger = logging.getLogger(__name__) class SessionMiddleware: """Session middleware storing session key in headers, working alongside session middleware storing session key in a cookie. """ def __init__(self, get_response): # One-time configuration and initialization. self.get_response = get_response engine = import_module(settings.SESSION_ENGINE) self.SessionStore = engine.SessionStore msg = "{}.__init__ engine: {}".format(__name__, engine) logger.debug(msg) def __call__(self, request): if 'X-Session' in request.headers: session_key = request.headers.get('X-Session') request.session = self.SessionStore(session_key) response = self.get_response(request) if response.status_code != 500: try: request.session.save() except UpdateError: msg = "{}: Unable to save session".format(__name__) logger.warning(msg) raise SuspiciousOperation( "The request's session was deleted before the " "request completed. The user may have logged " "out in a concurrent request, for example." ) try: if not request.session.is_empty(): response['X-Session'] = request.session.session_key except AttributeError: pass return response
Angular interceptor
import { Injectable } from '@angular/core'; import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpResponse } from '@angular/common/http'; import { finalize, tap } from 'rxjs/operators'; import { MessageService } from '../message.service'; import { Observable } from 'rxjs'; /** Pass untouched request through to the next request handler. */ @Injectable() export class SessionInterceptor implements HttpInterceptor { // Static class variable to maintain the session id private SessionId; private SessionName = 'X-Session'; constructor( private messenger: MessageService, ) {} intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { if (this.SessionId) { request = request.clone({ headers: request.headers.set(this.SessionName, this.SessionId) }); } return next.handle(request) .pipe( tap(event => { if (event instanceof HttpResponse) { if (event.headers.get(this.SessionName)) { // If there is a session header, capture it so it can be set on future responses this.SessionId = event.headers.get(this.SessionName); } } }) ); } }