Building an API with Django REST Framework 2: User Authentication

In this tutorial, we shall learn how to secure your API using Authentication, Authorization, & Permissions, as well as, implement user authentication, endpoints for user login, registration, logout, password reset etc. If you haven’t yet created your basic Django project, follow Part 1 of this tutorial, since this is a continuation to that one.

First let us customize our user model to have some custom fields, and register it.

Custom User model

Django provides a built-in basic user model, which we shall extend to include more fields, to fulfil our requirements. In the previous tutorial of this series, we created a blank extended user model. We shall customise it a little.

Add the following code in userapi/models.py

from django.db import models
from django.contrib.auth.models import AbstractUser
from django.utils.translation import ugettext_lazy as _
from django.conf import settings

class User(AbstractUser):
    username = models.CharField(blank=True, null=True, max_length=50)
    email = models.EmailField(_('email address'), unique=True)

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['username', 'first_name', 'last_name']

    def __str__(self):
        return "{}".format(self.email)
class UserProfile(models.Model):
    user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='profile')
    title = models.CharField(max_length=5)
    dob = models.DateField()
    address = models.CharField(max_length=255)
    country = models.CharField(max_length=50)
    city = models.CharField(max_length=50)
    zip = models.CharField(max_length=5)

Here we have redefined the email and username fields, in order to make email the default mandatory unique identifier instead of username. Username still has to be defined because otherwise Django complains while creating superuser.

Check api/settings.py , and make sure the following line is added there.

AUTH_USER_MODEL = 'userapi.User'

And then register this model to admin panel, userapi/admin.py so we can add/delete/update users.

from django.contrib import admin
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from .models import User, UserProfile

class UserProfileInline(admin.StackedInline):
  model = UserProfile
  can_delete = False

@admin.register(User)
class UserAdmin(BaseUserAdmin):
  fieldsets = (
    (None, {'fields': ('email', 'password')}),
    (_('Personal info'), {'fields': ('first_name', 'last_name')}),
    (_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser',
    'groups', 'user_permissions')}),
    (_('Important dates'), {'fields': ('last_login', 'date_joined')}),
    )
  add_fieldsets = (
    (None, {
    'classes': ('wide',),
    'fields': ('email', 'password1', 'password2'),
    }),
  )
  list_display = ('email', 'first_name', 'last_name', 'is_staff')
  search_fields = ('email', 'first_name', 'last_name')
  ordering = ('email',)
  inlines = (UserProfileInline, )

Let’s apply the migrations by running:

python3 manage.py makemigrations
python3 manage.py migrate

If you have been following this tutorial from Part 1, you would already have a superuser. If not, you can easily create one by running:

python3 manage.py createsuperuser

Now let us test our user model. Run the server by entering this command

python3 manage.py runserver

and open http://127.0.0.1:8000/admin and login with your superuser credentials. Click on add user and you can see the form where you can create instances of your new User model!

Now that we have created the API interface that allows to create, retrieve, update or delete users, registering a new user can be done by sending a POST request to the user endpoint. Let us create serializer and routes for that.

. . .

Serializers

In short, Serialisers take data sets and prepares them for easy conversion to JSON/XML formats, and vice versa. We need to create a serializer file for every django-app in your project that needs it.

Create a file serializers.py in the userapi folder, and add the following code.

from rest_framework import serializers
from userapi.models import User, UserProfile

class UserProfileSerializer(serializers.ModelSerializer):

class Meta:
model = UserProfile
fields = ('title', 'dob', 'address', 'country', 'city', 'zip')

class UserSerializer(serializers.HyperlinkedModelSerializer):
profile = UserProfileSerializer(required=True)

class Meta:
model = User
fields = ('url', 'email', 'first_name', 'last_name', 'password', 'profile')
extra_kwargs = {'password': {'write_only': True}}

def create(self, validated_data):
profile_data = validated_data.pop('profile')
password = validated_data.pop('password')
user = User(**validated_data)
user.set_password(password)
user.save()
UserProfile.objects.create(user=user, **profile_data)
return user

def update(self, instance, validated_data):
profile_data = validated_data.pop('profile')
profile = instance.profile

instance.email = validated_data.get('email', instance.email)
instance.save()

profile.title = profile_data.get('title', profile.title)
profile.dob = profile_data.get('dob', profile.dob)
profile.address = profile_data.get('address', profile.address)
profile.country = profile_data.get('country', profile.country)
profile.city = profile_data.get('city', profile.city)
profile.zip = profile_data.get('zip', profile.zip)
profile.save()

return instance

Next, to create the User Viewset, add these lines in userapi/views.py

from django.shortcuts import render
from rest_framework import viewsets

from userapi.models import User
from userapi.serializers import UserSerializer

class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer

To setup the API endpoints for the user model, add these lines in userapi/urls.py. The DefaultRouter class will define the standard REST (GET, POST, PUT, DELETE) endpoints for our User resource.

from rest_framework import routers
from userapi.views import UserViewSet

router = routers.DefaultRouter()
router.register(r'users', UserViewSet)

urlpatterns = [
    #other paths
    path(r'', include(router.urls)),
]

. . .

Authentication, Authorization, & Permissions

Authentication is the process that identifies the credentials that the request was made with. After the user is authenticated, Django will check if the user is authorized to access the resource they are requesting. Permissions determine whether a request should be granted or denied access. They are used to grant or deny access to different classes of users to different parts of the API.

Why JWT?

Django REST Framework comes with an inbuilt token-based authentication system which can generate a token for the user. That works fine, but each time it needs to make database calls to determine the user associated with the token it receives. This extra work is eliminated by JWT since JWT is encoded JSON data. As long as any web-service has access to the secret used in signing the data, it can also decode and read the embedded data. It doesn’t need any database calls & you don’t need to save the token in a database. You can generate the token from one service and other services can read and verify it just fine. Therefore, we will use JWT in our project because JWT is more efficient and simply scales better.

. . .

We shall be continuing with the userapi app that we created in Part 1 of this tutorial. If you haven’t followed that, just visit and follow the last section to create an app named userapi, and that’s it.

Setting up JWT Authentication

Let’s install the package.

pip3 install djangorestframework-jwt

That will install the package. Now we need to add rest_framework_jwt.authentication.JSONWebTokenAuthentication to the default authentication classes in the settings file. Go ahead and open api/settings.py file, and add this.

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
        'rest_framework.authentication.SessionAuthentication', 
        'rest_framework.authentication.BasicAuthentication',    
    ],
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated' 
    ],
}

Also, add these in the same file. This is a dictionary with JWT settings. Although not necessary, we add it because this assists in defining the defaults to use for JWT.

import datetime

#your other code here

JWT_AUTH = {
    # If the secret is wrong, it will raise a jwt.DecodeError telling you as such. You can still get at the payload by setting the JWT_VERIFY to False.
    'JWT_VERIFY': True,

    # You can turn off expiration time verification by setting JWT_VERIFY_EXPIRATION to False.
    # If set to False, JWTs will last forever meaning a leaked token could be used by an attacker indefinitely.
    'JWT_VERIFY_EXPIRATION': True,

    # This is an instance of Python's datetime.timedelta. This will be added to datetime.utcnow() to set the expiration time.
    # Default is datetime.timedelta(seconds=300)(5 minutes).
    'JWT_EXPIRATION_DELTA': datetime.timedelta(hours=1),

    'JWT_ALLOW_REFRESH': True,
    'JWT_AUTH_HEADER_PREFIX': 'JWT',
}

With this, you are done setting up JWT in your project and you have also set the global authentication settings. If needed you can override this on view level.

Now that JWT is all set up, let us include the paths for creating, verifying, and refreshing tokens, provided by the JWT package.

api/urls.py

from rest_framework_jwt.views import obtain_jwt_token
from rest_framework_jwt.views import refresh_jwt_token
from rest_framework_jwt.views import verify_jwt_token

urlpatterns = [
    # ... your other URLs
    path(r'api-token-auth/', obtain_jwt_token),
    path(r'api-token-refresh/', refresh_jwt_token),
    path(r'api-token-verify/', verify_jwt_token),
]

That’s it. Now we can test all of the 3 token services.

If you’re using Mac, just go to your terminal, make sure django server is running, and post this command, using the username and password of your superuser account that you created for admin panel. It should return a token string. If wrong credentials are provided, it should return an error.

curl -X POST -d "username=admin&password=admin" http://127.0.0.1:8000/api-token-auth/

. . .

Set Authentication endpoints

For our authentication endpoints, we need to install this package

pip3 install django-rest-auth

Go to api/settings.py and add these 2 apps to your INSTALLED_APPS array

'rest_framework.authtoken',
'rest_auth',

and in the same file add  this value

REST_USE_JWT = True

Add the following URL to the urlpatterns array in userapi/urls.py

path(r'auth/', include('rest_auth.urls')),

and to your main project urls.py file (api/urls.py) add this path

path(r'', include('django.contrib.auth.urls')),

Finally apply migrations and run your server

python3 manage.py migrate
python3 manage.py runserver

That’s it. Go to http://localhost:8000/userapi/auth/ and you can see the list of authentication endpoints added. Try out your newly introduced login functionality by visiting http://localhost:8000/userapi/auth/login/ (You can ignore the username field). If you login successfully then you will receive a login JSON response with the JSON web token.

For using the API endpoints in your website/app, you must append ?format=json to the endpoint URL to get raw JSON formatted response.

Permissions

At this stage, everyone can access the users api. We will now create a permission model that will allow only the admin to view all and delete users, and everyone to register or create a user. Additionally, logged in users will be able to retrieve or update their profiles.

Create a new file api/permissions.py and add the following code

from rest_framework import permissions

class IsLoggedInUserOrAdmin(permissions.BasePermission):

    def has_object_permission(self, request, view, obj):
        return obj == request.user or request.user.is_staff

class IsAdminUser(permissions.BasePermission):

    def has_permission(self, request, view):
        return request.user and request.user.is_staff

    def has_object_permission(self, request, view, obj):
        return request.user and request.user.is_staff

Now go to api/views.py and add the following lines

from rest_framework.permissions import AllowAny
from api.permissions import IsLoggedInUserOrAdmin, IsAdminUser

#other code
#in the end of class UserViewSet add the following

  def get_permissions(self):
    permission_classes = []
    if self.action == 'create':
      permission_classes = [AllowAny]
    elif self.action == 'retrieve' or self.action == 'update' or self.action == 'partial_update':
      permission_classes = [IsLoggedInUserOrAdmin]
    elif self.action == 'list' or self.action == 'destroy':
      permission_classes = [IsAdminUser]
    return [permission() for permission in permission_classes]

That’s all. Run your server and try out!


Also published on Medium.

By |2019-04-04T05:17:32+00:00March 29th, 2019|Categories: Django|Tags: , , , , |0 Comments

Leave A Comment