Non-user authentication in Django and Django Rest Framework (DRF)
Let’s imagine that our application needs to be used by some particular users, who are not real users on our database backend. For example, let’s assume we have a medical app used by doctors and patients. And then we need to provide certain access to Hospital administrators to get an overview of their doctors’ and patients’ activities. These hospital administrators are not and cannot become system users.
The problem becomes complex since Django’s authentication flow revolves around the concept of a user, and plenty of assumptions rely on the user being available.
How would you approach authentication and authorization in that case? How would this non-system users access the app, and how would the app handle them?
Authentication and authorization
But first let’s see an overview of authentication and authorization in Django/DRF. It’s important to understand the request flow in order to come up with a good solution. The overview here mentions those Django components that are relevant to authentication.
Authentication consists of the synergy between middleware and view functionality.
Django’s SessionMiddleware
Django’s SessionMiddleware
reads the session cookie for all incoming requests and sets it for all outgoing requests that don't have it. When reading the session cookie on an incoming request, the middleware sets the request.session
parameter to refer to a SessionStore
object which wraps the session.
The SessionMiddleware
runs for all incoming requests. Even anonymous requests get a session that refers to AnonymousUser
. Even requests to an API that uses Token-based authentication, and are therefore anonymous from a session perspective, do get the request.session
object as well as the response cookie.
Django’s AuthenticationMiddleware
Django’s AuthenticationMiddleware
sets the request.user
parameter to refer to the User
instance that the SessionStore
object pointed to by request.session
, refers to.
DRF View
- Request class
One of the very first things a DRF view does is wrap the request object around its very own Request
class. Among others, this overrides the request.user
object, essentially cancelling out the AuthenticationMiddleware
's earlier action, and rendering that middleware useless.
Therefore, as far as API requests go, the AuthenticationMiddleware
is entirely redundant.
2. Authentication
The view performs authentication by simply invoking request.user
. This, essentially calls the authenticate
method on the necessary authentication classes and sets the value of request.user
to the User
instance returned by the authenticator class.
In other words, one authentication class sets the request.user
object to the User
instance that made the request. The user instance could, for example, be the one that the Token corresponds to.
It’s worth mentioning that authentication classes are either defined project-wide using the REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES']
parameter, or on class level, using the authentication_classes
parameter.
3. Permissions
The view checks permissions, ensuring that the request.user
has the necessary access permissions or role to proceed.
Solution
Now that we have a solid understanding of authentication and authorization flow in Django/DRF, let’s figure out a solution to the problem mentioned earlier.
It should be obvious by now, that most of our logic should live in the authentication class, whose goal in general is to pair the incoming request with a user. In our case, of course, there is no such thing as a user, but rather a hospital.
Let’s assume that the Hospital
is indeed one of the data models in our application. The solution that I've come up with, involves an extension of the AnonymousUser
class that can refer to a Hospital
instance. This way we get an in-memory User
instance without littering the database, and the instance still gets to refer to the Hospital
that the request is made on behalf of.
This way, any time the authenticate
method runs successfully, the incoming request will get the request.user
object that contains the request.user.hospital
parameter which indicates the Hospital
instance that talks to our backend.
From that point on, request.user.hospital
may be used throughout the request life cycle.
Conclusions
As you can see the view’s querying logic is based on the concept of Hospital
and we've accomplished this with quite a few benefits:
- Without littering the database with dummy users.
- Without changing our data model to accommodate new types of users (hospital administrators).
- Using the regular Django/DRF authentication flow.
- We are able to plug in our authentication and permission classes into any view and work as we always do.