Skip to content

Commit

Permalink
feature(add jwt authentication): return jwt token when user logs in
Browse files Browse the repository at this point in the history
-update user serializer
-update user model
-update user renderer
-configure JWT on backends.py
[finishes #164047057]
  • Loading branch information
Swaleh Matongwa authored and Swaleh Matongwa committed Mar 4, 2019
1 parent fbcfd60 commit b0e42fa
Show file tree
Hide file tree
Showing 10 changed files with 329 additions and 68 deletions.
78 changes: 71 additions & 7 deletions authors/apps/authentication/backends.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,74 @@
# import jwt
#
# from django.conf import settings
#
# from rest_framework import authentication, exceptions
#
# from .models import User
import jwt
import datetime
from django.conf import settings

from rest_framework import authentication, exceptions

from .models import User

"""Configure JWT Here"""


class JWTAuthentication(authentication.BaseAuthentication):
authentication_header_prefix = 'Token'

def authenticate(self, request):
"""
The `authenticate` method is called on every request regardless of
whether the endpoint requires authentication.
"""
request.user = None

auth_header = authentication.get_authorization_header(request).split()
auth_header_prefix = self.authentication_header_prefix.lower()

if not auth_header:
return None

if len(auth_header) == 1:
# Invalid token header. No credentials provided. Do not attempt to
# authenticate.
return None

elif len(auth_header) > 2:
# Invalid token header. The Token string should not contain spaces. Do
# not attempt to authenticate.
return None

# The JWT library we're using can't handle the `byte` type, which is
# commonly used by standard libraries in Python 3. To get around this,
# we simply have to decode `prefix` and `token`. This does not make for
# clean code, but it is a good decision because we would get an error
# if we didn't decode these values.
prefix = auth_header[0].decode('utf-8')
token = auth_header[1].decode('utf-8')

if prefix.lower() != auth_header_prefix:
# The auth header prefix is not what we expected. Do not attempt to
# authenticate.
return None

return self._authenticate_credentials(token)

def _authenticate_credentials(self, token):
"""
Try to authenticate the given credentials. If authentication is
successful, return the user and token. If not, throw an error.
"""
try:
payload = jwt.decode(token, settings.SECRET_KEY)
except:
msg = 'Invalid authentication. Could not decode token.'
raise exceptions.AuthenticationFailed(msg)

try:
user = User.objects.get(pk=payload['id'])
except User.DoesNotExist:
msg = 'No user matching this token was found.'
raise exceptions.AuthenticationFailed(msg)

if not user.is_active:
msg = 'This user has been deactivated.'
raise exceptions.AuthenticationFailed(msg)

return (user, token)
61 changes: 41 additions & 20 deletions authors/apps/authentication/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
)
from django.db import models


class UserManager(BaseUserManager):
"""
Django requires that custom users define their own Manager class. By
inheriting from `BaseUserManager`, we get a lot of the same code used by
Django to create a `User` for free.
All we have to do is override the `create_user` function which we will use
to create `User` objects.
"""
Expand All @@ -33,21 +33,20 @@ 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.
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.')
"""
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.')

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 @@ -101,13 +100,23 @@ def __str__(self):
return self.email

@property
def token(self):
"""
Allows us to get a user's token by calling `user.token` instead of
`user.generate_jwt_token().
The `@property` decorator above makes this possible. `token` is called
a "dynamic property".
"""
return self._generate_jwt_token()

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 +126,16 @@ def get_short_name(self):
"""
return self.username

def _generate_jwt_token(self):
"""
Generates a JSON Web Token that stores this user's ID and has an expiry
date set to 60 days into the future.
"""
dt = datetime.now() + timedelta(days=60)

token = jwt.encode({
'id': self.pk,
'exp': int(dt.strftime('%s'))
}, settings.SECRET_KEY, algorithm='HS256')

return token.decode('utf-8')
5 changes: 5 additions & 0 deletions authors/apps/authentication/renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,17 @@ def render(self, data, media_type=None, renderer_context=None):
# the default JSONRenderer to handle rendering errors, so we need to
# check for this case.
errors = data.get('errors', None)
token = data.get('token', None)

if errors is not None:
# As mentioned about, we will let the default JSONRenderer handle
# rendering errors.
return super(UserJSONRenderer, self).render(data)

if token is not None and isinstance(token, bytes):
# We will decode `token` if it is of type
# bytes.
data['token'] = token.decode('utf-8')

# Finally, we can render our data under the "user" namespace.
return json.dumps({
Expand Down
9 changes: 4 additions & 5 deletions authors/apps/authentication/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,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,14 +87,14 @@ def validate(self, data):
return {
'email': user.email,
'username': user.username,

'token': user.token
}


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 +112,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
28 changes: 17 additions & 11 deletions authors/apps/authentication/tests/test_api.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"""Test API endpoints"""
import json
from rest_framework.test import APITestCase, APIClient
from rest_framework.views import status
Expand Down Expand Up @@ -44,7 +45,6 @@ def test_register_user_existing_email(self):
self.assertIn('user with this email already exists.', str(result))
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)


def test_register_user_empty_string(self):
"""
test register
Expand Down Expand Up @@ -78,20 +78,23 @@ def test_login_user(self):
self.user['user']['email'] = "james@gmail.com"
self.user['user']['password'] = "4567890123"
# hit the API endpoint
response = self.client.post('/api/users/login/', self.user, format='json')
response = self.client.post(
'/api/users/login/', self.user, format='json')
result = json.loads(response.content)

self.assertIn('A user with this email and password was not found.', str(result))
self.assertIn(
'A user with this email and password was not found.', str(result))
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

def test_login_unregistered_user(self):
"""
test login
"""
#create user
# create user
self.client.post('/api/users/', self.user, format='json')
# hit the API endpoint
response = self.client.post('/api/users/login/', self.user, format='json')
response = self.client.post(
'/api/users/login/', self.user, format='json')
result = json.loads(response.content)

self.assertIn('caro@yahoo.com', str(result))
Expand All @@ -101,10 +104,11 @@ def test_login_user_with_norequired(self):
"""
test login user without required fields
"""
#create user
# create user
self.client.post('/api/users/', format='json')
# hit the API endpoint
response = self.client.post('/api/users/login/', self.user, format='json')
response = self.client.post(
'/api/users/login/', self.user, format='json')
result = json.loads(response.content)

self.assertIn('errors', str(result))
Expand All @@ -114,25 +118,27 @@ def test_retrieve_user_unauthenticated(self):
"""
test retrieve user
"""
#create user
# create user
self.client.post('/api/users/', self.user, format='json')
# hit the API endpoint
response = self.client.get('/api/user/')
result = json.loads(response.content)

self.assertIn('Authentication credentials were not provided.', str(result))
self.assertIn(
'Authentication credentials were not provided.', str(result))
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

def test_update_user_unauthenticated(self):
"""
test update user
"""
self.user['user']['password'] = "4567890123"
#create user
# create user
self.client.post('/api/users/', self.user, format='json')
# hit the API endpoint
response = self.client.put('/api/user/', self.user, format='json')
result = json.loads(response.content)

self.assertIn('Authentication credentials were not provided.', str(result))
self.assertIn(
'Authentication credentials were not provided.', str(result))
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
Loading

0 comments on commit b0e42fa

Please sign in to comment.