Using Django REST Framework to Quickly Get a Working API: part III

Fabricio Saavedra
NicaSource
Published in
10 min readJul 13, 2023

As a conclusion for this three-part series of articles on “how to use the DRF (Django Rest Framework)” to get a working API, we will focus on two crucial yet overlooked (mostly by beginners) concepts for your API, authentication and permissions, also referenced as authorization. As stated in the conclusion of part II, up until this point, our API accepts requests from any origin, and it allows unrestricted modification of the information stored in the database, which won’t be the case for most projects, so we will check some of the tools the DRF has to improve the resource administration.

Authentication and authorization as concepts

Before we do any Django REST Framework documentation reference, let’s establish what it means to have an authorized request in the context of a microservices architecture since that will help us better understand the implementation in our simple mechanic shop API project.

According to auth0 by Okta,

authentication is the process of verifying who a user is, while authorization is the process of verifying what they have access to.

It’s just like when you visit a hotel — you let them know you’ve arrived, and depending on your reservation and payment information, you may or may not be able to access the mini-bar.

How does this get translated into our project? First, we’d like to know who is trying to access our API, so we’ll see how we incorporate authentication using what the DRF provides us.

Authentication with DRF

According to the very efficient documentation of the DRF,

authentication is the mechanism of associating an incoming request with a set of identifying credentials, such as the user the request came from, or the token that it was signed with.

Without having to install any third-party package, the DRF has support for five types of authentication schemes:

  1. BasicAuthentication is generally only appropriate for testing.
  2. TokenAuthentication suits client-server setups and fits like a glove with what we are developing.
  3. SessionAuthentication is meant for AJAX clients running in the same session context as your website.
  4. RemoteUserAuthentication, which allows you to delegate authentication to your web server (APACHE or NGINX).
  5. Custom authentication (implemented by subclassing the BaseAuthentication class and overriding the authenticate method).

How do we specify the authentication scheme we want to use? We will always define them using a list of classes. Then the DRF will attempt to authenticate with each class in the list, and it will set request.user and request.auth using the return value of the first class that successfully authenticates.

Based on the nature of your project you can:

  1. Declare the list of authentication classes globally.
  2. Set that list of authentication classes.

Since the endpoints we have set are somewhat sensitive, we will set the authentication globally in the settings.py module:

A reminder for where settings.py is located.

In that module, we will add a piece of configuration. In this case, as we previously stated, the TokenAuthentication class is what we want, so your configuration will look like this:

Plus, this authentication scheme requires us to add the authtoken app to the INSTALLED_APPS configuration:

Having the rest_framework.authtoken app registered in the installed apps will provide some migration files to create a table to store the generated tokens, so you, as a very aware person, will know that we need to apply those migrations by running python manage.py migrate :

Migrations that were applied after adding the Token authentication config.

Before moving on, let’s attempt one of the requests we’ve made in part II to see what functional changes have been introduced by the additional configuration we’ve set:

Getting the vehicles list after adding TokenAuthentication.

We still are allowed to get the list of vehicles despite not providing any credentials that validate who we are. That’s because authentication by itself won’t allow or disallow an incoming request; it simply identifies the credentials with which the request was made. In this case, since we’re not providing information about who we are, the request.user variable will be set to an instance of django.contrib.auth.models.AnonymousUser, and request.auth will be set to None. With that cleared out, we will consider the options we have to generate the tokens that inform the API of our identity.

Generating a token

a. in Django, you can have ‘listeners’ that act before or after a certain action or process with instructions that fit your needs, called signals. For example, we can make sure that after saving a User instance, a Token instance is generated and saved for the newly created user. IMHO, using signals can become rather tangled pretty quickly for moderately complex projects, and if you’re a beginner with Django, you probably are better off without using them.

b. Exposing an endpoint: when using TokenAuthentication , generating a token using a username-password pair seems rather convenient, so the DRF provides a built-in view to provide it by using the obtain_auth_token function. obtain_auth_token will return a JSON response when valid username and password fields are POSTed to the view using form data or JSON.

c. Using Django admin: we didn’t mention this given the nature of our project, but Django has this useful interface that allows creating objects from models we defined using a built-in admin panel. Considering we’re building this API from a microservices standpoint, there’s not much use to it.

d. Using Django manage.py command: the CLI utility that gave us the ability to start the project and add apps to it, generating migration files and applying them, also makes it possible (for version ≥ 3.6.4) to generate a user token by running:

python manage.py drf_create_token <username>

which will return the API token for the requested user, generating one if it doesn’t exist yet.

The option that better fits our requirements, both didactically and functionality-wise, is generating the token with the built-in view obtain_auth_token. To reach that functionality, we need to specify a path for it in the URL patterns we have:

Then we will generate an ad-hoc user to get a token and present a general Django tool that I use quite frequently in my projects and find helpful, which is the “Django shell.” The Django shell allows us to open an interactive Python interpreter with the context of the Django project we’re in, so doing the following is totally valid:

Using the Django shell to create a user.

Notice that we’re leveraging the create_user method of the User model (another feat of Django), instead of directly using the create method. Otherwise, we wouldn’t get a hash of the password we specified, and our information would be compromised if anyone found a vulnerability in our system.

Ok, now let’s try to get a token for Salvatore:

Obtaining a token for Salvatore.

Yes! By obtaining a token, we can authenticate our requests to the API. As we noted before, we didn’t specify any authorization scheme yet, so our requests are still passing, but now the request.user will be correctly set to an instance of the User model (Salvatore in this case) once we include the token in our request headers, which will be fundamental to testing our permission classes.

To summarize, in a client-server architecture, we would:

  1. Obtain a token by making a POST request to /api/api-token-auth and retain or store that somewhere (when using a browser, we tend to store it in localStorage).
  2. Set that token in the header for all our subsequent requests as follows:
"Authorization": Token 81e584aebd6ccf9771ececd44234fb660aabc8a

3. Send the request we want or need.

Authorization (permissions)

Along with authentication and throttling (out of the scope of this series), permissions determine whether a request should be granted or denied access (even though what access is, is a relatively open definition).

Similar to authentication, permissions checks are always run before the main block of code of the view (usually the method handler for POST, GET, etc.) and will be typically associated with the identity established in the authentication process and stored in the request.user and/or request.auth objects

As stated in the documentation,

The simplest style of permission would be to allow access to any authenticated user, and deny access to any unauthenticated user. This corresponds to the IsAuthenticated class in REST framework.

So, similarly to authentication, we’re using classes to establish permission either globally or per view, and before running the main body of the view, every class in the list is checked. If any permission check fails, a PermissionDenied or NotAuthenticated exception will be raised, and the main block of code won’t run.

Back to our project, let’s suppose that for some reason, you can use the endpoint to list the vehicles just by verifying your identity, which in our project, is achieved by passing the token in the Authorization header. Having that in mind, we’re going to import and set the IsAuthenticated permission class to the permission_classes list in the VehiclesList class-based view:

Now, we are going to start the Django application and send a GET request to api/vehicles endpoint:

GET request after adding the IsAuthenticated permission class.

We got a 401 status code (Not Authorized) and a very clear message stating that we didn’t provide authentication credentials, which is true since we didn’t provide the Authorization header. So let’s try the same request after doing that:

Successful request after properly authenticating.

Your day ends better when things go that smoothly. Why did that work? Inspecting the IsAuthenticated class might help:

IsAuthentication class definition.

Simple and effective. As that simple line reads, having truthy values for request.user and request.user.is_authenticated is enough to have permission, thanks to the TokenAuthentication class.

Ok, you might say holding your chin with your fist, but this is some generous permission policy to apply to a serious project, how can we implement something more specific? Glad you ask!

You can define any logic you want in a custom permission class. To write your own permission class, you just need to subclass the BasePermission class and implement has_permission and/or has_object_permission with the following signatures:

  • has_permission(self, request, view)
  • has_object_permission(self, request, view, obj)

The implemented methods should return True if the permission to access the resource is granted, and False if not.

To give a colorful example, let’s assume that Salvatore is an exemplary employee and Mario is not, so we don’t want Mario snooping around. We will create a permission class that implements some logic to prevent Mario from accessing resources. But since Mario doesn’t exist yet in our database, we need to add him using the User model.

Adding Mario.

Now, we need to create our custom permission class. We will add a new permission_classes module in the customers app.

New permission classes module.

The logic we want to implement is the following:

  • If the request doesn’t have an authenticated user, reject it.
  • If the request is authenticated, but the user is Mario, reject it.
  • If the request is authenticated, and the user is not Mario, allow it.

So that will translate into:

You’ll notice that the last condition is exactly the same that is used in the IsAuthenticated class, so in theory, we could subclass it, and then call the super method, but to illustrate the custom class and avoid extending on inheritance, we will stick to our current approach.

To test our new class, we need to add it to the permission_classes list in the CustomersList class-based view:

A non-authenticated request will throw a classic 401, as expected:

Not authenticated request.

What will happen with a “Mario-authenticated” request? After obtaining a token for Mario and making a request to get the customers list, we get the following:

Authenticated request for Mario.

We got a 403 status code, which means the request is authenticated (we know Mario is behind it), but it’s not authorized (Mario is not allowed). Finally, we are going to attempt a request to the same endpoint but for Salvatore (notice that the token now belongs to him):

Authenticated request to api/customers for Salvatore.

Working as expected! Our custom permission class:

  1. Does not allow unauthenticated requests.
  2. Does not allow authenticated requests from Mario.
  3. Allows authenticated requests from anyone else.

Conclusion

Congrats! You made it through this series of articles. Hopefully, you now have a deeper understanding of how we can:

  1. leverage the DRF to keep out code DRY by using class-based views,
  2. represent information that resembles the structure of the models we defined,
  3. and lift a basic yet effective wall against curious people with a simple yet illustrative example project.

Next steps? Try some of these:

  • Look up how we can expose locally our API to have it listen to requests coming from a client (a frontend app).
  • Try to nest representations of information by using custom serializers as fields.
  • Connect an email server to send information from your Django app whenever a new user is created.

--

--