Skip to content

Commit

Permalink
feature(jwt):configure jwt on login/registration
Browse files Browse the repository at this point in the history
-return a token on login/registration
-write tests to check for the token

[starts #162949210]
  • Loading branch information
kwanj-k committed Jan 15, 2019
1 parent ca538e6 commit c0f1906
Show file tree
Hide file tree
Showing 9 changed files with 139 additions and 29 deletions.
3 changes: 1 addition & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ before_script:
script:
- python manage.py makemigrations
- python manage.py migrate
- python manage.py test
- coverage run --source=authors/ manage.py test
- pytest --cov-report term-missing --cov=authors/apps/authentication -p no:warnings
- coverage report

# Generate coverage report
Expand Down
30 changes: 22 additions & 8 deletions authors/apps/authentication/backends.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
# import jwt
#
# from django.conf import settings
#
# from rest_framework import authentication, exceptions
#
# from .models import User
"""
JWT configuration
"""
from rest_framework_jwt.settings import api_settings

"""Configure JWT Here"""

"""
By default, django-restframework--jwt generates a token
here we create a function to manually create a token
with user data of our choosing
Here the token is passed the user email
All the the jwt configurations are defined globally in the project settings.py
"""


def get_jwt_token(user):
"""
Creates a token manually
more info: https://getblimp.github.io/django-rest-framework-jwt/
"""
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
payload = jwt_payload_handler(user)
return jwt_encode_handler(payload)
2 changes: 0 additions & 2 deletions authors/apps/authentication/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,5 +116,3 @@ def get_short_name(self):
the user's real name, we return their username instead.
"""
return self.username


19 changes: 12 additions & 7 deletions authors/apps/authentication/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from rest_framework import serializers

from .models import User
from .backends import get_jwt_token


class RegistrationSerializer(serializers.ModelSerializer):
Expand All @@ -15,15 +16,17 @@ class RegistrationSerializer(serializers.ModelSerializer):
min_length=8,
write_only=True
)
token = serializers.SerializerMethodField()

# The client should not be able to send a token along with a registration
# request. Making `token` read-only handles that for us.
def get_token(self, obj):
token = get_jwt_token(obj)
return token

class Meta:
model = User
# List all of the fields that could possibly be included in a request
# or response, including fields specified explicitly above.
fields = ['email', 'username', 'password']
fields = ['email', 'username', 'password', 'token']

def create(self, validated_data):
# Use the `create_user` method we wrote earlier to create a new user.
Expand All @@ -34,7 +37,7 @@ class LoginSerializer(serializers.Serializer):
email = serializers.CharField(max_length=255)
username = serializers.CharField(max_length=255, read_only=True)
password = serializers.CharField(max_length=128, write_only=True)

token = serializers.CharField(read_only=True)

def validate(self, data):
# The `validate` method is where we make sure that the current
Expand Down Expand Up @@ -84,7 +87,10 @@ def validate(self, data):
# The `validate` method should return a dictionary of validated data.
# This is the data that is passed to the `create` and `update` methods
# that we will see later on.
data['token'] = get_jwt_token(user)
token = list(data.values())[2]
return {
'token': token,
'email': user.email,
'username': user.username,

Expand All @@ -94,7 +100,7 @@ def validate(self, data):
class UserSerializer(serializers.ModelSerializer):
"""Handles serialization and deserialization of User objects."""

# Passwords must be at least 8 characters, but no more than 128
# Passwords must be at least 8 characters, but no more than 128
# characters. These values are the default provided by Django. We could
# change them, but that would create extra work while introducing no real
# benefit, so let's just stick with the defaults.
Expand All @@ -112,11 +118,10 @@ class Meta:
# specifying the field with `read_only=True` like we did for password
# above. The reason we want to use `read_only_fields` here is because
# we don't need to specify anything else about the field. For the
# password field, we needed to specify the `min_length` and
# password field, we needed to specify the `min_length` and
# `max_length` properties too, but that isn't the case for the token
# field.


def update(self, instance, validated_data):
"""Performs an update on a User."""

Expand Down
55 changes: 55 additions & 0 deletions authors/apps/authentication/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""
Module to test if token is returned during login_url/registration
"""
from rest_framework import status
from django.test import TestCase, Client
from django.urls import reverse
import json


# initialize the APIClient app
client = Client()


class TestTokenGeneration(TestCase):
""" Test user registration/login_url returns token """

def setUp(self):
self.user_data = {
'user': {
'username': 'fooo',
'email': 'cake@foo.com',
'password': 'yertg234D'
}
}

def test_token_gen_on_signup(self):
"""
Test if a token is returned after registration
"""
response = client.post(
reverse('authentication:signup_url'),
data=json.dumps(self.user_data),
content_type='application/json'
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
# assert token is in the response
self.assertIn('token', response.data)

def test_login_url_returns_token(self):
"""
Test if token is returned after login_url
"""
client.post(
reverse('authentication:signup_url'),
data=json.dumps(self.user_data),
content_type='application/json'
)
response = client.post(
reverse('authentication:login_url'),
data=json.dumps(self.user_data),
content_type='application/json'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
# assert token is in the response
self.assertIn('token', response.data)
11 changes: 6 additions & 5 deletions authors/apps/authentication/urls.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@

from django.urls import path
from .views import (
LoginAPIView, RegistrationAPIView, UserRetrieveUpdateAPIView
)

""" Django 2.0 requires the app_name variable set when using include namespace"""
"""
Django 2.0 requires the app_name variable set when using include namespace
"""
app_name = 'authentication'

urlpatterns = [
path('user/', UserRetrieveUpdateAPIView.as_view()),
path('users/', RegistrationAPIView.as_view(), name = "signup_url"),
path('users/login/', LoginAPIView.as_view(), name = "login_url"),
path('user/', UserRetrieveUpdateAPIView.as_view(), name="deatails"),
path('users/', RegistrationAPIView.as_view(), name="signup_url"),
path('users/login/', LoginAPIView.as_view(), name="login_url"),
]
2 changes: 0 additions & 2 deletions authors/apps/authentication/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ def post(self, request):
serializer = self.serializer_class(data=user)
serializer.is_valid(raise_exception=True)
serializer.save()

return Response(serializer.data, status=status.HTTP_201_CREATED)


Expand Down Expand Up @@ -72,4 +71,3 @@ def update(self, request, *args, **kwargs):
serializer.save()

return Response(serializer.data, status=status.HTTP_200_OK)

41 changes: 38 additions & 3 deletions authors/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import os
import dj_database_url

import datetime

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
Expand Down Expand Up @@ -147,8 +147,43 @@
'NON_FIELD_ERRORS_KEY': 'error',

'DEFAULT_AUTHENTICATION_CLASSES': (
# 'authors.apps.authentication.backends.JWTAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
),
}

#Jwt configuration
JWT_AUTH = {
'JWT_ENCODE_HANDLER':
'rest_framework_jwt.utils.jwt_encode_handler',

'JWT_DECODE_HANDLER':
'rest_framework_jwt.utils.jwt_decode_handler',

'JWT_PAYLOAD_HANDLER':
'rest_framework_jwt.utils.jwt_payload_handler',

'JWT_PAYLOAD_GET_USER_ID_HANDLER':
'rest_framework_jwt.utils.jwt_get_user_id_from_payload_handler',

'JWT_RESPONSE_PAYLOAD_HANDLER':
'rest_framework_jwt.utils.jwt_response_payload_handler',

'JWT_SECRET_KEY': SECRET_KEY,
'JWT_GET_USER_SECRET_KEY': None,
'JWT_PUBLIC_KEY': None,
'JWT_PRIVATE_KEY': None,
'JWT_ALGORITHM': 'HS256',
'JWT_VERIFY': True,
'JWT_VERIFY_EXPIRATION': True,
'JWT_LEEWAY': 0,
'JWT_EXPIRATION_DELTA': datetime.timedelta(hours=3),
'JWT_AUDIENCE': None,
'JWT_ISSUER': None,
'JWT_ALLOW_REFRESH': False,
'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7),
'JWT_AUTH_HEADER_PREFIX': 'Bearer',
'JWT_AUTH_COOKIE': None,

}

STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
5 changes: 5 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ psycopg2-binary==2.7.6.1
PyJWT==1.7.1
psycopg2-binary==2.7.6.1
whitenoise==4.1.2
pytest==4.0.1
pytest-cov==2.6.0
pytest-django==3.4.4
djangorestframework-jwt==1.11.0

0 comments on commit c0f1906

Please sign in to comment.