When I started this series I have got one comment from my co-worker that instead of authentication JWT can be used to sign one time links. After reading through the documentation I found that can be a great idea so I decided to write a blog post about it.
Table of Contents:
Nowadays when a user creates an account he or she has to confirm identity. It is done by sending an email with the link to confirm and activate an account.
As this link has to expire and be safe this is a good use case for using JSON Web Tokens. Such tokens can be generated for every user and set to expire for example after two hours. How can it be done in Django? Let's jump into the code.
First I change the previous code from series and made special django app just for users. But the first user has to register - that's why I made new endpoint in urls.py:
from users.views import UserViewSet, CreateUserView,
urlpatterns = [
# rest of url patterns
url('^api-register/$', CreateUserView.as_view()),
]
CreateUserView looks as follows:
from rest_framework import status
from rest_framework.generics import CreateAPIView
from rest_framework.response import Response
from rest_framework_jwt.settings import api_settings
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
class CreateUserView(CreateAPIView):
model = User.objects.all()
permission_classes = [
permissions.AllowAny # Or anon users can't register
]
serializer_class = UserSerializer
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
user = self.model.get(username=serializer.data['username'])
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
return Response(
{
'confirmation_url': reverse(
'activate-user', args=[token], request=request
)
},
status=status.HTTP_201_CREATED, headers=headers
)
In this view, I simply add few additional lines for creating JWT. Rest of this is a standard code from DRF. First I created payload by adding user to JWT creation process, then I created the token from payload by calling jwt_encode_handler. At the end instead of returning user data, I return confirmation_url for the end user to enter and activate the account. By default django make every user active so I have to write my own create method for UserSerializer:
from django.contrib.auth.models import User
from rest_framework import serializers
from tasks.models import Task
class UserSerializer(serializers.ModelSerializer):
tasks = serializers.PrimaryKeyRelatedField(
many=True, queryset=Task.objects.all()
)
class Meta:
model = User
fields = ('username', 'password', 'tasks', 'email')
def create(self, validated_data):
user = User(
email=validated_data['email'],
username=validated_data['username']
)
user.set_password(validated_data['password'])
user.is_active = False
user.save()
return user
It is simply for setting user as inactive during the process of account creation. Right now when user wants to create an account he/she has to send the following request:
$ http POST 127.0.0.1:9000/api-register/ username=krzysiek password=krzysiek email=[email protected]
HTTP/1.0 201 Created
Allow: POST, OPTIONS
Content-Type: application/json
Date: Sun, 13 Nov 2016 15:16:33 GMT
Server: WSGIServer/0.2 CPython/3.5.2
Vary: Accept
X-Frame-Options: SAMEORIGIN
{
"confirmation_url": "http://127.0.0.1:9000/api-activate/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6ImtyenlzenRvZkBrei5jb20iLCJ1c2VyX2lkIjoyNSwidXNlcm5hbWUiOiJrcnp5c2llayIsImV4cCI6MTQ3OTA1MDQ5M30.CMcW8ZtU6AS9LfVvO-PoLyqcwi6cOK1VzI2o7pEPX2k/"
}
How this confirmation_url works? I made additional urlpattern:
from users.views import ActivateUser
urlpatterns = [
# rest of url patterns
url(
'^api-activate/(?P<token>.+?)/$',
ActivateUser.as_view(),
name='activate-user'
),
]
and in ActivateUser:
class ActivateUser(APIView):
def get(self, request, *args, **kwargs):
token = kwargs.pop('token')
try:
payload = jwt_decode_handler(token)
except jwt.ExpiredSignature:
msg = _('Signature has expired.')
raise exceptions.AuthenticationFailed(msg)
except jwt.DecodeError:
msg = _('Error decoding signature.')
raise exceptions.AuthenticationFailed(msg)
except jwt.InvalidTokenError:
raise exceptions.AuthenticationFailed()
user_to_activate = User.objects.get(id=payload.get('user_id'))
user_to_activate.is_active = True
user_to_activate.save()
return Response(
{'User Activated'},
status=status.HTTP_200_OK
)
This is generic APIView so I write get method for handling GET requests. I was wondering if it's a good idea to activate user in GET request or do it in PUT. If you have some thoughts about this I will be happy to hear them. In get I simply take the token from kwargs and perform validation on that token - if it's valid or expired. This part of code usually lies in authentication backend but in such class I don't have access to url of a request so in this case, I have to implement this in such a way. If you have other ways of handling such a case please let me know! So if everything looks good I activate user:
$ http GET http://127.0.0.1:9000/api-activate/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6ImtyenlzenRvZkBrei5jb20iLCJ1c2VyX2lkIjoyNSwidXNlcm5hbWUiOiJrcnp5c2llayIsImV4cCI6MTQ3OTA1MDQ5M30.CMcW8ZtU6AS9LfVvO-PoLyqcwi6cOK1VzI2o7pEPX2k/
HTTP/1.0 200 OK
Allow: GET, HEAD, OPTIONS
Content-Type: application/json
Date: Sun, 13 Nov 2016 15:17:37 GMT
Server: WSGIServer/0.2 CPython/3.5.2
Vary: Accept
X-Frame-Options: SAMEORIGIN
[
"User Activated"
]
$ http GET http://127.0.0.1:9000/api-activate/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6ImtyenlzenRvZkBrei5jb20iLCJ1c2VyX2lkIjoyNSwidXNlcm5hbWUiOiJrcnp5c2llayIsImV4cCI6MTQ3OTA1MDQ5M30.CMcW8ZtU6AS9LfVvO-PoLyqcwi6cOK1VzI2o7pEPX2k/
HTTP/1.0 401 Unauthorized
Allow: GET, HEAD, OPTIONS
Content-Type: application/json
Date: Sun, 13 Nov 2016 15:28:00 GMT
Server: WSGIServer/0.2 CPython/3.5.2
Vary: Accept
WWW-Authenticate: JWT realm="api"
X-Frame-Options: SAMEORIGIN
{
"detail": "Signature has expired."
}
By default django rest framework jwt sets token expiry time to 5 minutes. If you want to change that add following lines in settings.py:
JWT_AUTH = {
'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=7)
}
That's all for today! Feel free to comment and check repo for this blog post under this link.
Special thanks to Kasia for being editor for this post. Thank you.
Cover picture by Lalmch under CC0 Public Domain.
As we have working application now it's high time to make it more secure by authenticating users. To do this I will use JSON Web Tokens.
Table of Contents:
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.
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
Date: 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
Date: 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
Date: 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
Date: 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.
Special thanks to Kasia for being editor for this post. Thank you.
Cover picture by Lalmch under CC0 Public Domain.
In this blog post, I will deal with creating simple Django application for creating tasks using django rest framework.
From this blog post, you can learn how to setup basic DRF application.
Table of Contents:
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.
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 just 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
Date: 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
Date: 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/"
}
]
That's all for today! In the next post, I will make authentication with JWT. Stay tuned!
Code for this blog post is under this github.
Special thanks to Kasia for being editor for this post. Thank you.
Cover picture by Lalmch under CC0 Public Domain.