Skip to content

Commit

Permalink
feat(JWT): implement token based authentication
Browse files Browse the repository at this point in the history
- ensure a token is returned after successful registration
- ensure a token is return after successful login
- update tests to incorporate the implemented jwt authentication requirement
- Add tests to ensure a valid token is returned on signup and login
[Delivers #165305260]
  • Loading branch information
jkamz committed Apr 28, 2019
1 parent 0604b2a commit 66035cb
Show file tree
Hide file tree
Showing 9 changed files with 161 additions and 53 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ before_install:

install:
- pip install -r requirements.txt
- pip install coveralls

before_script:
- psql -c 'create database ah_the_jedi;' -U postgres
Expand Down
22 changes: 14 additions & 8 deletions authors/apps/authentication/backends.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
# import jwt
#
# from django.conf import settings
#
# from rest_framework import authentication, exceptions
#
# from .models import User
import jwt
from rest_framework_jwt.settings import api_settings

"""Configure JWT Here"""
def handle_token (user):

"""
Generate and return token
"""

jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER

payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)

return token
17 changes: 13 additions & 4 deletions authors/apps/authentication/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from .backends import handle_token

from .renderers import UserJSONRenderer
from .serializers import (
LoginSerializer, RegistrationSerializer, UserSerializer
LoginSerializer, RegistrationSerializer, UserSerializer,
)

from .models import User

class RegistrationAPIView(APIView):
# Allow any user (authenticated or not) to hit this endpoint.
Expand All @@ -25,8 +26,12 @@ def post(self, request):
serializer = self.serializer_class(data=user)
serializer.is_valid(raise_exception=True)
serializer.save()
user_data = User.objects.get(username=user['username'])
token = handle_token(user_data)
res = serializer.data
res['token'] = token

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


class LoginAPIView(APIView):
Expand All @@ -43,8 +48,12 @@ def post(self, request):
# handles everything we need.
serializer = self.serializer_class(data=user)
serializer.is_valid(raise_exception=True)
user_data = User.objects.get(email=user['email'])
token = handle_token(user_data)
res = serializer.data
res['token'] = token

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


class UserRetrieveUpdateAPIView(RetrieveUpdateAPIView):
Expand Down
47 changes: 44 additions & 3 deletions authors/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import os
import environ
import datetime

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)

Expand Down Expand Up @@ -145,9 +146,49 @@
'EXCEPTION_HANDLER': 'authors.apps.core.exceptions.core_exception_handler',
'NON_FIELD_ERRORS_KEY': 'error',

# 'DEFAULT_AUTHENTICATION_CLASSES': (
# 'authors.apps.authentication.backends.JWTAuthentication',
# ),
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
],

'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
),
}

#jwt authentication settings
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(seconds=3600),
'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,
}

STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
48 changes: 35 additions & 13 deletions authors/tests/data/base_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,27 @@ def login_user(self, data=''):

return response

def fetch_current_user(self, headers=""):
def fetch_current_user(self, headers=None):
"""
This method 'fetch_current_user'
fetches details of current user
provided with correct authorization
"""

return self.client.get(
"/api/user",
format="json"
)

def update_user_details(self, data=''):
if headers:
return self.client.get(
"/api/user",
format="json",
HTTP_AUTHORIZATION='Bearer ' + headers
)

else:
return self.client.get(
"/api/user",
format="json"
)


def update_user_details(self, data='', token=None):
"""
This method 'update_user_details'
updates details of current user with
Expand All @@ -68,8 +76,22 @@ def update_user_details(self, data=''):

data = data or self.base_data.update_data

return self.client.put(
"/api/user",
data,
format="json"
)
if token:
return self.client.put(
"/api/user",
data,
format="json",
HTTP_AUTHORIZATION='Bearer ' + token
)
else:
return self.client.put(
"/api/user",
data,
format="json"
)

def login_user_and_get_token(self):
res = self.login_user()
token = res.data['token']

return token
18 changes: 18 additions & 0 deletions authors/tests/data/test_login.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import jwt
from rest_framework import status
from .base_test import BaseTest
from authors.settings import SECRET_KEY


class UserLoginTest(BaseTest):
Expand Down Expand Up @@ -80,3 +82,19 @@ def test_raises_error_if_incorrect_credentials(self):
"email and password was not found" in self.login_user(
).data["errors"]["error"][0]
)


def test_returns_valid_token_on_successful_login(self):
"""
Test if a valid jwt token is returned on successful user login
"""
new_user = self.login_user()

self.assertTrue('token' in new_user.data)

payload = jwt.decode(new_user.data['token'], SECRET_KEY, algorithms=['HS256'])

self.assertEqual(
payload['email'],
self.base_data.user_data["user"]["email"]
)
18 changes: 17 additions & 1 deletion authors/tests/data/test_signup.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import jwt
from authors.apps.authentication.models import User
from rest_framework import status
from .base_test import BaseTest

from authors.settings import SECRET_KEY

class ModelTestCase(BaseTest):
"""
Expand Down Expand Up @@ -107,3 +108,18 @@ def test_raises_error_if_duplicate_data_provided(self):
"username already exists" in
new_user.data["errors"]["username"][0]
)

def test_returns_valid_token_on_successful_registration(self):
"""
Test if a valid jwt token is returned on successful registration
"""
new_user = self.signup_user()

self.assertTrue('token' in new_user.data)

payload = jwt.decode(new_user.data['token'], SECRET_KEY, algorithms=['HS256'])

self.assertEqual(
payload['email'],
self.base_data.user_data["user"]["email"]
)
36 changes: 15 additions & 21 deletions authors/tests/data/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,18 @@ def test_successful_fetch_if_authorization_provided(self):
authenticated user
"""

self.client.login(
username=self.base_data.username,
password=self.base_data.password
)

token = self.login_user_and_get_token()

self.assertEqual(
self.fetch_current_user().status_code,
self.fetch_current_user(token).status_code,
status.HTTP_200_OK
)

self.assertTrue(
self.base_data.user_data["user"]["username"] in
self.fetch_current_user().data["username"] and
self.fetch_current_user(token).data["username"] and
self.base_data.user_data["user"]["email"] in
self.fetch_current_user().data["email"]
self.fetch_current_user(token).data["email"]
)

def test_successful_update_if_authorization_provided(self):
Expand All @@ -45,46 +42,43 @@ def test_successful_update_if_authorization_provided(self):
current user is authenticated
"""

self.client.login(
username=self.base_data.username,
password=self.base_data.password
)
token = self.login_user_and_get_token()

self.assertEqual(
self.update_user_details().status_code,
self.update_user_details(token=token).status_code,
status.HTTP_200_OK
)

self.assertEqual(
self.update_user_details().data["username"],
self.update_user_details(token=token).data["username"],
self.base_data.update_data["user"]["username"]
)

def test_raises_error_if_authorization_missing(self):
"""
Test for Forbidden raised if authorization
Test for unathorized error raised if authorization
not provided
"""

self.assertEqual(
self.fetch_current_user().status_code,
status.HTTP_403_FORBIDDEN
status.HTTP_401_UNAUTHORIZED
)

self.assertTrue(
"Authentication credentials were not provided" in
self.fetch_current_user().data["detail"]
)

def test_forbidden_error_if_authorization_missing(self):
def test_unauthorized_error_if_authorization_missing(self):
"""
Test for Forbidden raised if authorization
Test for unauthorization error raised if authorization
not provided
"""

self.assertEqual(
self.update_user_details().status_code,
status.HTTP_403_FORBIDDEN
status.HTTP_401_UNAUTHORIZED
)

self.assertTrue(
Expand All @@ -105,7 +99,7 @@ def test_raises_error_if_incorrect_authorization_credentials(self):

self.assertEqual(
self.fetch_current_user().status_code,
status.HTTP_403_FORBIDDEN
status.HTTP_401_UNAUTHORIZED
)

def test_raises_forbidden_if_incorrect_authorization_credentials(self):
Expand All @@ -121,5 +115,5 @@ def test_raises_forbidden_if_incorrect_authorization_credentials(self):

self.assertEqual(
self.update_user_details().status_code,
status.HTTP_403_FORBIDDEN
status.HTTP_401_UNAUTHORIZED
)
7 changes: 4 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ astroid==2.2.5
coverage==4.5.3
Django==2.1
django-cors-middleware==1.2.0
django-environ==0.4.5
django-extensions==2.1.6
djangorestframework==3.9.2
djangorestframework-jwt==1.11.0
gunicorn==19.9.0
isort==4.3.17
lazy-object-proxy==1.3.1
mccabe==0.6.1
Expand All @@ -16,7 +19,5 @@ pytz==2019.1
six==1.10.0
sqlparse==0.3.0
typed-ast==1.3.4
wrapt==1.11.1
whitenoise==4.1.2
gunicorn==19.9.0
django-environ==0.4.5
wrapt==1.11.1

0 comments on commit 66035cb

Please sign in to comment.