JSON Web Tokens in django application
Recently I attended a workshop about web security. During these hours I learnt quite a lot but one thing got my attention- JWT.
What is JWT?
JWT aka JSON Web Tokens is a method of authentication. What it does: You as a user first send a request to the server saying: I want to login! And server gives you in response a long sequence of characters. As you get this sequence you can use it as a way to tell the server that you are the person you really are.
In a more technical sense: you send a request which logs you to service for example headers with login and password. In response, you got encrypted token. Then you want to get some info about another resource on the server that requires authentication. So to your request you add one more header with previously received token and that’s all! You are authenticated.
JSON Web Token looks like this:
HEADER.PAYLOAD.SIGNATURE
The header is a JSON that consists of a type of token (JWT) and which hash algorithm will be used (HMAC SHA256 or RSA). HMAC stands for keyed-Hash Message Authentication Code. Message Authentication Code (MAC) is used to confirm that message comes from the good sender and its integrity has not been changed. Keyed-Hash stands for hashing MAC in combination with a secret key.
The payload contains the claims. Claim store information user wants to transmit and server can use to properly handle authentication. There are a lot of registered claims but we will use only:
- “exp” (Expiration Time) Claim
- “nbf” (Not Before Time) Claim
- “iss” (Issuer) Claim
- “aud” (Audience) Claim
- “iat” (Issued At) Claim
Payload will look like this:
{
"exp": "1234567890",
"name": "Krzysztof Zuraw"
}
Last part is a signature. It is the sum of all previously mentioned parts encoded in base64 + secret.
How can you use JWT and why?
When you get your response back from a server with JSON Web Token you can use it in header like this:
Authorization: Bearer <JWT token>
In comparison with another method of authentication: SAML, JWT is more compact. JSON format is widely used in programming word so there is no problem with parsers for that format.
Overview of application
The main goal of this application is to create tasks. Each task has a title - string with a maximum length of 100 characters. Task also has a person to which it is bound (many to one relation - ForeginKey). The last thing that task have is date and time which given task is due to. The user can easily modify each of tasks so GET, POST, PUT and DELETE methods are supported.
As we know how the application is designed let’s jump into the code.
Application code
First, there is a need to create model for Task:
from django.db import models
from django.contrib.auth.models import User
class Task(models.Model):
title = models.CharField(max_length=100)
person = models.ForeignKey(User)
due_to = models.DateTimeField()
def __str__(self):
return 'Task with title: {}'.format(self.title)
The arguments of Task
correspond to what was written in the overview.
As we have models ready now it’s time to create serializers so data from database can be converted to stream of bytes:
from rest_framework import serializers
from .models import Task
from django.contrib.auth.models import User
class TaskSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Task
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
As you can see in TaskSerializer
I used HyperlinkedModelSerializer
as a type of serializer that I want to use - thanks to that response
from my application will have hyperlinks to resources instead of
primary keys that are used in UserSerializer
. In this serializer, I
use django User
as a source of data. I have to do this because Task
model has a reference to User
and without serialization of the second
one I cannot serialize the task.
Right now I have my models and serializers ready so it’s time to create some views and urls. For a typical usage of views, DRF gives you generic viewsets like ModelViewSet. ViewSet is a combination of the logic for a set of related views in a single class. How do views look like?
from rest_framework import viewsets
from .models import Task
from .serializers import TaskSerializer, UserSerializer
from django.contrib.auth.models import User
class TaskViewSet(viewsets.ModelViewSet):
queryset = Task.objects.all()
serializer_class = TaskSerializer
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
I created 2 viewsets. The only thing that I need to provide is queryset
and serializer_class
arguments so viewsets know which data they needed
to take and which serializer use. Right now there is only one thing
missing - urls:
from django.conf.urls import url, include
from django.contrib import admin
from tasks import views
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register(r'tasks', views.TaskViewSet)
router.register(r'users', views.UserViewSet)
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^', include(router.urls)),
]
Here I set up DefaultRouter
and hook TaskViewSet
and UserViewSet
to it. Router is a way of building common routes for a resource. To get
all tasks - I go to /tasks
uri. To retrieve first task I type
tasks/1
. I can write this in urlpatterns but the router is doing the
same for me automatically.
Right now I can try my application:
$ http GET 127.0.0.1:9000
HTTP/1.0 200 OK
Allow: GET, HEAD, OPTIONS
Content-Type: application/json
pubDate: Sun, 23 Oct 2016 08:36:23 GMT
Server: WSGIServer/0.2 CPython/3.5.1
Vary: Accept, Cookie
X-Frame-Options: SAMEORIGIN
{
"tasks": "http://127.0.0.1:9000/tasks/",
"users": "http://127.0.0.1:9000/users/"
}
$ http GET 127.0.0.1:9000/tasks/
HTTP/1.0 200 OK
Allow: GET, POST, HEAD, OPTIONS
Content-Type: application/json
pubDate: Sun, 23 Oct 2016 08:45:50 GMT
Server: WSGIServer/0.2 CPython/3.5.1
Vary: Accept, Cookie
X-Frame-Options: SAMEORIGIN
[
{
"due_to": "2016-10-18T19:12:01Z",
"person": "http://127.0.0.1:9000/users/1/",
"title": "First one",
"url": "http://127.0.0.1:9000/tasks/1/"
},
{
"due_to": "2016-10-18T19:12:10Z",
"person": "http://127.0.0.1:9000/users/1/",
"title": "Second one",
"url": "http://127.0.0.1:9000/tasks/2/"
}
]
JWT in Django Rest Framework
There are few packages on pypi that provide JWT support but as I am already using DRF I choose package called REST framework JWT Auth. It’s simple package and does it’s job well so I can recommend it to everyone. But you have to make sure that your application is behind SSL/TLS as JWT tokens generated are not signed. But enough writing- let’s jump into the code.
Implementing JWT in DRF application
First I added small change to my Task
model definition in models.py:
class Task(models.Model):
# rest of model
person = models.ForeignKey('auth.User', related_name='tasks')
# rest of model
It is the same model definition but written using string. The code in Django responsible for model lookup based on the string can be seen here.
Then I added an additional field to UserSerializer
- thanks to that
when getting info about the user I also get info about which tasks this
user has. It can be accomplished by:
class UserSerializer(serializers.ModelSerializer):
tasks = serializers.PrimaryKeyRelatedField(
many=True, queryset=Task.objects.all()
)
# rest of the code
As I got my models and serializers ready I need views:
from rest_framework import permissions
class TaskViewSet(viewsets.ModelViewSet):
# rest of the code
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
class UserViewSet(viewsets.ModelViewSet):
# rest of the code
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
I added permission_classes
to tell DRF that these views are read only
when the user is not authenticated. If I send a token ( or authenticate
in another way) I am able to modify data kept under this view. To
authenticate I needed a new endpoint so there’s a small change to
urls.py:
from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [
# rest of the code
url(r'^api-auth/', obtain_jwt_token),
]
Right now the user firsts need to authenticate using this endpoint. In return, endpoint gives back a token. Last thing to let this work is to tell Django Rest Framework that I want to use JWT as a basic type of authentication in settings.py:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
)
}
And that’s it! JWT should be working:
$ http GET 127.0.0.1:9000/tasks/
HTTP/1.0 200 OK
Allow: GET, POST, HEAD, OPTIONS
Content-Type: application/json
pubDate: Sat, 29 Oct 2016 13:10:52 GMT
Server: WSGIServer/0.2 CPython/3.5.1
Vary: Accept
X-Frame-Options: SAMEORIGIN
[
{
"due_to": "2016-10-18T19:12:01Z",
"person": "admin",
"title": "First one",
},
{
"due_to": "2016-10-18T19:12:10Z",
"person": "admin",
"title": "Second one",
}
]
$ cat create_task.json
{
"due_to": "2016-10-18T19:12:01Z",
"person": 1,
"title": "Next one",
}
$ http POST 127.0.0.1:9000/tasks/ < create_task.json
HTTP/1.0 401 Unauthorized
Allow: GET, POST, HEAD, OPTIONS
Content-Type: application/json
pubDate: Sun, 30 Oct 2016 08:38:41 GMT
Server: WSGIServer/0.2 CPython/3.5.1
Vary: Accept
WWW-Authenticate: JWT realm="api"
X-Frame-Options: SAMEORIGIN
{
"detail": "Authentication credentials were not provided."
}
To send POST you need:
$ http POST 127.0.0.1:9000/api-auth/ username=admin password=admin
HTTP/1.0 200 OK
Allow: POST, OPTIONS
Content-Type: application/json
pubDate: Sun, 30 Oct 2016 08:41:26 GMT
Server: WSGIServer/0.2 CPython/3.5.1
Vary: Accept
X-Frame-Options: SAMEORIGIN
{
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0Nzc4MTc4NTMsImVtYWlsIjoiYWRtaW5AZXhhbXBsZS5jb20iLCJ1c2VybmFtZSI6ImFkbWluIiwidXNlcl9pZCI6MX0.xWlhwgzzVjDwgTPp48AgAYDJnraGThlkGmBnJbKnA74"
}
$ http POST 127.0.0.1:9000/tasks/ < create_task.json 'Authorization: JWT eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0Nzc4MTc4NTMsImVtYWlsIjoiYWRtaW5AZXhhbXBsZS5jb20iLCJ1c2VybmFtZSI6ImFkbWluIiwidXNlcl9pZCI6MX0.xWlhwgzzVjDwgTPp48AgAYDJnraGThlkGmBnJbKnA74'
HTTP/1.0 201 Created
Allow: GET, POST, HEAD, OPTIONS
Content-Type: application/json
pubDate: Sun, 30 Oct 2016 08:53:30 GMT
Server: WSGIServer/0.2 CPython/3.5.1
Vary: Accept
X-Frame-Options: SAMEORIGIN
{
"due_to": "2016-10-18T19:12:01Z",
"id": 5,
"person": 1,
"title": "Next one"
}
That’s all for today! Feel free to comment and check repo for this blog post under this link.