Build and Deploy a REST API with Django and Docker — Part 2

vinod krishna
Nov 25, 2018 · 7 min read

In part 1 of this series, we discussed the project set up and created a hello world API. In this post, we will build the three APIs required for authentication. The below are the endpoints that we will create today.

  • POST /login
  • POST /login/refresh
  • POST /login/register

Let’s Migrate first!

A Migration is how Django applies your models and their changes to actual database schema. But what’s a model? It is just a Python class representing a table of your database schema. Django comes with a built-in ORM which provides a good number of convenient methods to operate on DB without worrying about the connectors or writing raw SQL queries. We will see more about how to use it a little later.

When you ran the server, you might have seen an error like this one

This is because Django comes with a few inbuilt models in auth and admin apps and these models need to be migrated in-order-to apply these changes to the actual schema. We can run the migration using the migrate command.

python manage.py migrate

POST /login/register

In this post call, we will accept a body with some required parameters like username and password and do some basic validations, then save the user to the database and respond back with an appropriate response. In this endpoint, we are going to use the default user model from django.contrib.auth.

Let's create a view class for our register endpoint and add a post method.

# View class to Register users
class Register(APIView):
def post(self,request):
data = request.data
return Response(request.data)

Add the /login/register path in urls.py and map it to this view class. Now your urlpatterns should look like below

urlpatterns = [
path('admin/', admin.site.urls),
path('hello_world', views.HelloWorld.as_view()),
# Paths for login
path('login/register', views.Register.as_view())
]

At this point, this endpoint does nothing but echo the request body.

Now that the structure is ready, let’s start building the actual logic. We will need username, password and email so let’s consider them as required parameters and store these keys to a list required_params. Our first step is to check if these are available and have valid values. To check the validity of required params we created a method validate_required_params which takes the param key, value and return value if it is valid else raises a ValidationError. We handled this exception to respond with a HTTP_400_BAD_REQUEST.

As we have validated the required parameters, We can create a new User object and set all the required fields directly from the variables. Similarly we will set optional parameters if available. Now all the values that the user passed are set to the new_user object, we call the save method on it, which will save it to database and the user registration is successful. So, we will just respond back with a success message. You can always find fullviews.py file here

Register API View

POST /login

This endpoint should basically take user credentials, validate them and return access and refresh tokens. Let’s start with creating an APIView with a post method. Credentials should always be passed in Authorization header of HTTP request, it should follow the Authorization: <type> <credentials> syntax. For this endpoint we will use Basic type which requires the credentials to be base64 encoded in <username>:<password> format.

To handle basic authentication requests, django rest framework has an inbuilt authentication class BasicAuthentication. This has to be added in the Login class along with the IsAuthenticated permission class. This makes the Login endpoint protected by basic authentication. At this point your Login class should look like this.

class Login(APIView):
authentication_classes = (BasicAuthentication,)
permission_classes = (IsAuthenticated,)

def post(self, request):
return Response("Authentication Successful")

Add a new path for login in urls.py. This time, we will use a regular expression in the url_patterns. To use regex, we should use re_path instead of path from django 2.0. Find an explanation for the login regex here.

urlpatterns = [
path('admin/', admin.site.urls),
path('hello_world', views.HelloWorld.as_view()),
# Paths for login
re_path(r'^login(?:\/)?$', views.Login.as_view()),
path('login/register', views.Register.as_view())
]

The logical next step is to generate token(s) for authenticated users. For this we will use Json Web Tokens (JWT) with the help of PyJWT. Basically, JWT is structured to have three parts separated by a dot(.). The first part is header which usually contain the type of the token and the hashing algorithm used. The second part is payload, where actual user claims are stored. The last part is the signature, which is basically a hash of header and payload generated using the algorithm specified in the header. You can learn more about jwt here.

Install PyJWT with pip by running this command in your terminal

pip install PyJWT

Our token payload should contain some recommended claims like issued time, issuer, audience, expiry etc. along with some user information. So, we will create a payload dictionary and sign the jwt with the SECRET_KEY which is generated by django in your app’s settings.py (to use this secret, you have to import settings from django.conf). We will also generate the refresh token in the same way but it will live longer than the access token. Finally send the response with these tokens and some other information. Our Login class will look like below.

class Login(APIView):
authentication_classes = (BasicAuthentication,)
permission_classes = (IsAuthenticated,)

def post(self, request):
utc_now = datetime.utcnow()
access_payload = {
'iat': utc_now,
'exp': utc_now + timedelta(minutes=60),
'nbf': utc_now,
'iss': "http://localhost:8000/login", # this has to be replaced with application domain after deploying
'username': request.user.username,
'email': request.user.email,
'type': 'access'
}

access_token = jwt.encode(access_payload, settings.SECRET_KEY, algorithm='HS256')

refresh_payload = {
'iat': utc_now,
'exp': utc_now + timedelta(days=2),
'nbf': utc_now,
'iss': "http://localhost:8000/login", # this has to be replaced with application domain after deploying
'username': request.user.username,
'type': 'refresh',
}

refresh_token = jwt.encode(refresh_payload, settings.SECRET_KEY, algorithm='HS256')

response = {
'access_token': access_token,
'expires_in': 3600,
'token_type': "bearer",
'refresh_token': refresh_token
}

return Response(response)

This way of signing a token with a hardcoded secret is not recommended, this is not at all meant to be used in production and only for understanding purposes. A more secure way of signing the token is using a RSA key pair but it is not discussed here to keep things simple.

Since we will use the entire token generation code in the refresh endpoint as well, we created a utils.py and created a generate_token method which takes User as argument and return access_token, refresh_token. So, the updated Login class will look like

class Login(APIView):
authentication_classes = (BasicAuthentication,)
permission_classes = (IsAuthenticated,)

def post(self, request):

access_token, refresh_token = utils.generate_tokens(request.user)

if access_token is None or refresh_token is None:
return Response({"error": "Something went wrong"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

response = {
'access_token': access_token,
'expires_in': 3600,
'token_type': "bearer",
'refresh_token': refresh_token
}

return Response(response)

And the utils.py file looks like

from datetime import datetime, timedelta
from django.contrib.auth.models import User
import jwt
from django.conf import settings


def generate_tokens(user):

if not (isinstance(user, User)):
return None

utc_now = datetime.utcnow()
access_payload = {
'iat': utc_now,
'exp': utc_now + timedelta(minutes=60),
'nbf': utc_now,
'iss': "http://localhost:8000/login", # this has to be replaced with application domain after deploying
'username': user.username,
'email': user.email,
'type': 'access'
}

access_token = jwt.encode(access_payload, settings.SECRET_KEY, algorithm='HS256')

refresh_payload = {
'iat': utc_now,
'exp': utc_now + timedelta(days=2),
'nbf': utc_now,
'iss': "http://localhost:8000/login", # this has to be replaced with application domain after deploying
'username': user.username,
'type': 'refresh',
}

refresh_token = jwt.encode(refresh_payload, settings.SECRET_KEY, algorithm='HS256')

return access_token, refresh_token

POST /login/refresh

This API is supposed to take refresh token and respond with a new set of access and refresh tokens if the refresh token is valid. We will create this endpoint similar to the login endpoint, but it will not need authentication.

For validating the refresh token, we will use PyJWT’s decode method. It will throw exceptions if the token is invalid for whatever reason. So, we need to handle these exceptions and respond back with an appropriate error message. After decoding we have to check the token’s type, if it is anything other than refresh, we will respond with a error. Then we will read the username from the token and use this to get the User object from the database. We pass this User object to generate_token method of utils to get access_token and refresh_token. If both of them are not none, we will respond back with tokens. The LoginRefresh class looks like this

LoginRefresh APIView

This kind of token based approach is only secure if all the communications are made over HTTPS. It fails big time if used over HTTP. For real world use you can always rely on OAUTH2 for token based authentication, there are some good packages available for django for implementing OAUTH2.

This marks the end of part-2, You can find the source code at this point here. In the next part we will implement the rest of the APIs and authenticate all the requests to these API’s using a custom authentication class. Feel free to post any of your questions or thoughts here.

vinod krishna

Written by

I’m a developer by profession and passion. I develop applications for multiple platforms including web and mobile.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade