Skip to content

Commit

Permalink
#162163262 implement jwt authentication feature (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
Kasulejoseph authored and collin5 committed Dec 6, 2018
1 parent 7c2dd54 commit eccdc5c
Show file tree
Hide file tree
Showing 11 changed files with 274 additions and 104 deletions.
4 changes: 0 additions & 4 deletions .vscode/settings.json

This file was deleted.

60 changes: 52 additions & 8 deletions authors/apps/authentication/backends.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,58 @@
# 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:
"""
This class implements a custom authentication by overriding
the .authenticate(self, request) method. The method returns
a two-tuple of (user, token) on successfull authentication
and None otherwise.
"""

def authenticate_header(self, request):
"""
This method returns 'None' if authentication is not attempted
Otherwise it returns the token.
"""
header = authentication.get_authorization_header(request)
token = None
try:
token = header.split()[1].decode('utf-8')
except:
raise exceptions.AuthenticationFailed(
'Token not found in the header')
finally:
return token

def authenticate(self, request):
pass
"""
This method gets the token from the authenticate_header method
and perform permission checks on the token. When the checks
fails, a AuthenticationFailed exception is raised otherwise
a user object and token are returned.
"""
user_token = self.authenticate_header(request)
if not user_token:
return None
try:
payload_id = self.decode_token(user_token)
user = User.objects.get(id=payload_id)
except (User.DoesNotExist):
raise exceptions.AuthenticationFailed('Invalid user credentials')
return (user, user_token)

def decode_token(self, user_token):
try:
payload = jwt.decode(user_token, settings.SECRET_KEY)
return payload['id']
except jwt.InvalidTokenError:
raise exceptions.AuthenticationFailed(
'Invalid token. please login again')
except jwt.ExpiredSignatureError:
raise exceptions.AuthenticationFailed(
'Token expired. Please log in again.')
53 changes: 32 additions & 21 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 All @@ -33,21 +32,21 @@ def create_user(self, username, email, password=None):
return user

def create_superuser(self, username, email, password):
"""
Create and return a `User` with superuser powers.
"""
Create and return a `User` with superuser powers.
Superuser powers means that this use is an admin that can do anything
they want.
"""
if password is None:
raise TypeError('Superusers must have a password.')
Superuser powers means that this use is an admin that can do anything
they want.
"""
if password is None:
raise TypeError('Superusers must have a password.')

user = self.create_user(username, email, password)
user.is_superuser = True
user.is_staff = True
user.save()
user = self.create_user(username, email, password)
user.is_superuser = True
user.is_staff = True
user.save()

return user
return user


class User(AbstractBaseUser, PermissionsMixin):
Expand Down Expand Up @@ -102,12 +101,12 @@ def __str__(self):

@property
def get_full_name(self):
"""
This method is required by Django for things like handling emails.
Typically, this would be the user's first and last name. Since we do
not store the user's real name, we return their username instead.
"""
return self.username
"""
This method is required by Django for things like handling emails.
Typically, this would be the user's first and last name. Since we do
not store the user's real name, we return their username instead.
"""
return self.username

def get_short_name(self):
"""
Expand All @@ -117,4 +116,16 @@ def get_short_name(self):
"""
return self.username


def token(self):
"""
Generate and return a jwt token. The user's unique id and username
are the user details encode within the user token.
"""
expire_date = datetime.now() + timedelta(days=0, hours=24)
token = jwt.encode({
'id': self.pk,
'name': self.get_short_name(),
'exp': expire_date,
}, settings.SECRET_KEY, algorithm="HS256"
)
return token.decode('utf-8')
8 changes: 3 additions & 5 deletions authors/apps/authentication/serializers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
from django.contrib.auth import authenticate

from rest_framework import serializers

from .models import User


Expand Down Expand Up @@ -34,7 +32,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,6 +85,7 @@ def validate(self, data):
return {
'email': user.email,
'username': user.username,
'token': user.token

}

Expand All @@ -106,7 +105,7 @@ class UserSerializer(serializers.ModelSerializer):

class Meta:
model = User
fields = ('email', 'username', 'password')
fields = ('email', 'username', 'password', 'token')

# The `read_only_fields` option is an alternative for explicitly
# specifying the field with `read_only=True` like we did for password
Expand All @@ -116,7 +115,6 @@ class Meta:
# `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
Empty file.
125 changes: 125 additions & 0 deletions authors/apps/authentication/test/test_authentication.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import json
from django.urls import reverse
from rest_framework.views import status
from rest_framework.test import APITestCase, APIClient
from ..models import UserManager, User


class TestUsers(APITestCase):

def setUp(self):
self.client = APIClient()

def generate_user(self, username='', email='', password=''):
user = {
'user': {
'email': email,
'username': username,
'password': password
}
}
return user

def create_user(self, username='', email='', password=''):
user = self.generate_user(username, email, password)
self.client.post('/api/users/', user, format='json')
return user

def test_user_registration(self):
user = self.generate_user(
'athena', 'athena@gmail.com', 'password@user')
response = self.client.post('/api/users/', user, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(
json.loads(response.content),
{"user": {"email": "athena@gmail.com", "username": "athena"}}
)

def test_user_registration_empty_details(self):
user = self.generate_user('', '', '')
response = self.client.post('/api/users/', user, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

def test_user_registration_wrong_email_format(self):
user = self.generate_user('athena', 'athenmail', 'password@user')
response = self.client.post('/api/users/', user, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

def test_user_login(self):
self.create_user('athena', 'athena@gmail.com', 'password@user')
login_details = self.generate_user(
'', 'athena@gmail.com', 'password@user')
response = self.client.post(
'/api/users/login/', login_details, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
json.loads(response.content),
{"user": {
"email": "athena@gmail.com",
"username": "athena",
'token': response.data['token']
}
}
)

def test_unauthorized_access_to_authenticated_endpoint(self):
self.create_user('kasule', 'athena@gmail.com', 'Password@user1')
login_details = self.generate_user(
'', 'athena@gmail.com', 'Password@user1')
response = self.client.post(
'/api/user/', login_details, format='json')
self.assertTrue(response.status_code == 403)
self.assertEqual(
json.loads(response.content),
{"user": {
"detail": "Authentication credentials were not provided."
}
}
)

def test_user_with_valid_token_access_protected_endpoints(self):
self.create_user('soko', 'athena@gmail.com', 'Password@user1')
login_details = self.generate_user(
'', 'athena@gmail.com', 'Password@user1')
response = self.client.post(
'/api/users/login/', login_details, format='json')
token = response.data['token']
self.client.credentials(HTTP_AUTHORIZATION='Bearer ' + token)
res = self.client.get(
'/api/user/', login_details, format='json')
self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertEqual(
json.loads(res.content),
{"user": {
"email": "athena@gmail.com",
"username": "soko",
'token': res.data['token']
}
}
)

def test_invalid_token(self):
self.create_user('josh', 'athena@gmail.com', 'Password@user1')
login_details = self.generate_user(
'', 'athena@gmail.com', 'Password@user1')
self.client.credentials(HTTP_AUTHORIZATION='Bearer ' + '123hjhj12')
res = self.client.get(
'/api/user/', login_details, format='json')
self.assertTrue(res.status_code == 401)
self.assertEqual(
'Invalid token. please login again', res.data['detail'])

def test_login_jwt_with_bad_credentials(self):
self.create_user('kica', 'athena@gmail.com', 'Password@user11')
login_details = self.generate_user(
'', 'kica@gmail.com', 'Password@user11')
response = self.client.post(
'/api/users/login/', login_details, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(
{"errors": {
"error": [
"A user with this email and password was not found."]
}
},
json.loads(response.content))
48 changes: 48 additions & 0 deletions authors/apps/authentication/test/test_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import json
from django.urls import reverse
from rest_framework.views import status
from rest_framework.test import APITestCase, APIClient
from ..models import UserManager, User


class TestUsers(APITestCase):

def setUp(self):
self.client = APIClient()
self.user = User.objects.create_user(
username='', email='', password='')
self.supper = User.objects.create_superuser(
username='henry', email='antena@andela.com', password='longpass')

def test_users_is_instance_of_User(self):
self.assertIsInstance(self.user, User)
self.assertIsInstance(self.supper, User)

def test_raise_type_error_no_username_details(self):
try:
response = User.objects.create_user(
username=None, email='anthena@andela.com'
)
except TypeError as error:
self.assertTrue(str(error), 'Users must have a username.')

def test_user_missing_password(self):
try:
res = User.objects.create_user(
username='soko', email=None
)
except TypeError as error:
self.assertTrue(str(error), 'Users must have an email address.')

def test_create_a_user_model(self):
self.assertTrue(self.supper.username, 'henry')
self.assertNotEqual(self.supper.username, 'kasule')
self.assertTrue(self.supper.email, 'anthena@andela.com')
self.assertTrue(self.user)

def test_get_short_name_and_full_name(self):
self.assertTrue(self.supper.get_full_name, 'henry')
self.assertTrue(self.supper.get_short_name, 'henry')

def test_token_created_successfully(self):
self.assertGreater(len(User.token(self.supper)), 12)
Loading

0 comments on commit eccdc5c

Please sign in to comment.