Skip to content

Commit

Permalink
ft(JWT Authentication): Implement JWT Authentication
Browse files Browse the repository at this point in the history
- Add a property function to enable returning of a jwt when user is created
- Call property function to return a jwt with the user details during sign up and login
- Add an overiding class for authentication of headers and token
- Fix failing tests
- Add tests to test for the feature implemented
- Implement feedback

[finishes #165305753]
  • Loading branch information
Issa Maina authored and Issa Maina committed Apr 30, 2019
1 parent df2b233 commit 5046555
Show file tree
Hide file tree
Showing 9 changed files with 227 additions and 28 deletions.
1 change: 1 addition & 0 deletions .env-example
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ DATABASE_NAME=your database name
DATABASE_USER=your user name
DATABASE_PASSWORD=your user password
DATABASE_HOST=your host
SECRET_KEY=your secret key
69 changes: 62 additions & 7 deletions authors/apps/authentication/backends.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,65 @@
# import jwt
#
# from django.conf import settings
#
# from rest_framework import authentication, exceptions
#
# from .models import User
import jwt
from django.conf import settings
from rest_framework import authentication, exceptions
from .models import User

"""Configure JWT Here"""


class JWTAuthentication(authentication.BaseAuthentication):
"""
This class will handle users' authentication
"""

def authenticate(self, request):
"""
This method will authenticate the authorization headers provided.
It is called regardless of whether the endpoint requires
authentication.
"""
prefix = 'Bearer'
header = authentication.get_authorization_header(request).split()

if not header:
return None

if len(header) < 2:
resp = 'The authorization header provided is invalid!'
raise exceptions.AuthenticationFailed(resp)

if header[0].decode('utf-8') != prefix:
resp = 'Please use a Bearer token!'
raise exceptions.AuthenticationFailed(resp)

token = header[1].decode('utf-8')

return self.authenticate_token(request, token)

def authenticate_token(self, request, token):
"""
This method will authenticate the provided token
"""
try:
payload = jwt.api_jwt.decode(
token, settings.SECRET_KEY, algorithms='HS256')
except jwt.api_jwt.DecodeError:
resp = 'Invalid Token. The token provided cannot be decoded!'
raise exceptions.AuthenticationFailed(resp)
except jwt.api_jwt.ExpiredSignatureError:
resp = 'The token used has expired. Please authenticate again!'
raise exceptions.AuthenticationFailed(resp)

username = payload['user_data']['username']
email = payload['user_data']['email']

try:
user = User.objects.get(email=email, username=username)
except User.DoesNotExist:
resp = 'No user was found from the provided token!'
raise exceptions.AuthenticationFailed(resp)

if not user.is_active:
resp = 'Your account is not active!'
raise exceptions.AuthenticationFailed(resp)

return user, token
26 changes: 23 additions & 3 deletions authors/apps/authentication/models.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import jwt

from datetime import datetime, timedelta

from django.conf import settings
from django.contrib.auth.models import (
AbstractBaseUser, BaseUserManager, PermissionsMixin
)
from django.db import models


class UserManager(BaseUserManager):
"""
Django requires that custom users define their own Manager class. By
Expand Down Expand Up @@ -116,5 +115,26 @@ def get_short_name(self):
the user's real name, we return their username instead.
"""
return self.username

def token(self):
"""
This method allows us to get the jwt token by calling the user.token
method.
"""
return self.generate_jwt_token()


def generate_jwt_token(self):
"""
This method allows the creation of a jwt token. User's username and
email are used in the encoding of the token.
The token is generated during sign up.
"""
user_details = {'email': self.email,
'username': self.username}
token = jwt.encode(
{
'user_data': user_details,
'exp': datetime.now() + timedelta(hours=24)
}, settings.SECRET_KEY, algorithm='HS256'
)
return token.decode('utf-8')
7 changes: 4 additions & 3 deletions authors/apps/authentication/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ class RegistrationSerializer(serializers.ModelSerializer):

# The client should not be able to send a token along with a registration
# request. Making `token` read-only handles that for us.
token = serializers.CharField(max_length=255, read_only=True)

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 +35,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(max_length=255, read_only=True)

def validate(self, data):
# The `validate` method is where we make sure that the current
Expand Down Expand Up @@ -87,7 +88,7 @@ def validate(self, data):
return {
'email': user.email,
'username': user.username,

'token': user.token,
}


Expand Down
41 changes: 37 additions & 4 deletions authors/apps/authentication/tests/basetests.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import json, base64

import jwt
from datetime import datetime, timedelta
from django.conf import settings
import json
from django.contrib.auth import get_user_model

from rest_framework.test import APITestCase, APIClient
from rest_framework.reverse import reverse

Expand All @@ -28,12 +29,35 @@ def setUp(self):
password="@Us3r.com"
)

self.user1 = User.objects.create_user(
username="ian",
email="ian@gmail.com",
password="Maina9176",
)
self.user1 = User.objects.get(username='ian')
self.user1.is_active = False
self.user1.save()

self.super_user = User.objects.create_superuser(
username="admin",
email="admin@authors.com",
password="adm123Pass!!"
)

def generate_jwt_token(self, email, username):
"""
Generates a JSON Web Token to be used in testing
"""
user_details = {'email': email,
'username': username}
token = jwt.encode(
{
'user_data': user_details,
'exp': datetime.now() + timedelta(hours=24)
}, settings.SECRET_KEY, algorithm='HS256'
)
return token.decode('utf-8')

def signup_user(self, username="", email="", password=""):
"""
Method to register a user
Expand Down Expand Up @@ -81,7 +105,7 @@ def login_user(self, email="", password=""):
)

def is_authenticated(self, email="", password=""):
self.client.login(username=email, password=password)
return self.client.login(username=email, password=password)

def update_user(self, username="", email="", password=""):
"""
Expand All @@ -104,3 +128,12 @@ def get_user(self):
A method to retrieve users from the database
"""
return self.client.get(self.get_url)

def authenticate_user(self, email, password):
"""
Authenticates users by adding authorization headers
"""
logdata = self.login_user(email, password)
token = logdata.data['token']
return self.client.credentials(
HTTP_AUTHORIZATION='Bearer ' + token)
80 changes: 80 additions & 0 deletions authors/apps/authentication/tests/test_jwt_authentication.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import os
from authors.apps.authentication.tests.basetests import BaseTest
from rest_framework import status

class TestTokenAuthentication(BaseTest):
"""
This class handles the testing of JWTAuthentication
"""

def test_get_jwt_after_login(self):
self.signup_user('issa', 'issamwangi@gmail.com', 'Maina9176')
response = self.login_user('issamwangi@gmail.com', 'Maina9176')
self.assertIn('token', str(response.data))
self.assertEqual(response.status_code, status.HTTP_200_OK)

def test_get_jwt_after_signup(self):
response = self.signup_user(
'issa', 'issamwangi@gmail.com', 'Maina9176')
self.assertIn('token', str(response.data))
self.assertEqual(response.status_code, status.HTTP_201_CREATED)

def test_invalid_authorization_header(self):
token = self.generate_jwt_token('issamwangi@gmail.com', 'issa')
self.client.credentials(
HTTP_AUTHORIZATION=token)
response = self.client.get(self.get_url)
self.assertEqual(
response.data['detail'],
'The authorization header provided is invalid!')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

def test_invalid_header_prefix(self):
token = self.generate_jwt_token('issamwangi@gmail.com', 'issa')
self.client.credentials(
HTTP_AUTHORIZATION='Invalid ' + token)
response = self.client.get(self.get_url)
self.assertEqual(
response.data['detail'],
'Please use a Bearer token!')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

def test_invalid_token(self):
token = 'sxdcfvgbhjklrty567dfg'
self.client.credentials(
HTTP_AUTHORIZATION='Bearer ' + token)
response = self.client.get(self.get_url)
self.assertEqual(
response.data['detail'],
'Invalid Token. The token provided cannot be decoded!')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

def test_inactive_user(self):
token = self.generate_jwt_token('ian@gmail.com', 'ian')
self.client.credentials(
HTTP_AUTHORIZATION='Bearer ' + token)
response = self.client.get(self.get_url)
self.assertEqual(
response.data['detail'],
'Your account is not active!')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

def test_user_not_found_from_token(self):
token = self.generate_jwt_token('ianmwangi@gmail.com', 'ianmaina')
self.client.credentials(
HTTP_AUTHORIZATION='Bearer ' + token)
response = self.client.get(self.get_url)
self.assertEqual(
response.data['detail'],
'No user was found from the provided token!')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

def test_expired_jwt_token(self):
token = os.getenv('EXPIREDTOKEN')
self.client.credentials(
HTTP_AUTHORIZATION='Bearer ' + token)
response = self.client.get(self.get_url)
self.assertEqual(
response.data['detail'],
'The token used has expired. Please authenticate again!')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
Loading

0 comments on commit 5046555

Please sign in to comment.