Setting up JWT Authentication

Allow users to register, sign in and sign out with Django REST Framework and JWTs.

Weyou!

Help spread the word about this tutorial!

Django comes with a session-based authentication system that works out of the box. It includes all of the models, views, and templates you need to let users log in and create a new account. Here's the rub though: Django's authentication only works with the traditional HTML request-response cycle.

What do we mean by "the traditional HTML request-response cycle"? Historically, when a user wanted to perform some action (such as creating a new account), the user would fill out a form in their web browser. When they clicked the "Submit" button, the browser would make a request — which included the data the user had typed into the registration form — to the server, the server would process that request, and it would respond with HTML or redirect the browser to a new page. This is what we mean when we talk about doing a "full page refresh."

Why is knowing that Django's built-in authentication only works with the traditional HTML request-response cycle important? Because the client we're building this API for does not adhere to this cycle. Instead, the client expects the server to return JSON instead of HTML. By returning JSON, we can let the client decide what it should do next instead of letting the server decide. With a JSON request-response cycle, the server receives data, processes it, and returns a response (just like in the HTML request-response cycle), but the response does not control the browser's behavior. It just tells us the result of the request.

Luckily, the team behind Django realized that the trend of web development was moving in this direction. They also knew that some projects might not want to use the built-in models, views, and templates. They may choose to use custom versions instead. To make sure all of the efforts that went into building Django's built-in authentication system wasn't wasted, they decided to make it possible to use the most important parts while maintaining the ability to customize the end result.

We will dive into this later in the chapter. For now, here's what you want to know:

We will be creating our own User model to replace Django's.

We will have to write our views to support returning JSON instead of HTML.

Because we won't be using HTML, we have no need for Django's built-in login and register templates.

If you're wondering what's left for us to use, that's a fair question. This goes back to what we talked about earlier about Django making it possible to use the core parts of authentication without using the default authentication system.

Session-based authentication

By default, Django uses sessions for authentication. Before going further, we should talk about what this means, why it's important, what token-based authentication and JSON Web Tokens (JWTs for short) are, and which one we'll be using in this course.

In Django, sessions are stored as cookies. These sessions, along with some built-in middleware and request objects, ensure that there is a user available on every request. The user can be accessed as request.user. When the user is logged in, request.user is an instance of the User class. When they're logged out, request.user is an instance of the AnonymousUser class. Whether the user is authenticated or not, request.user will always exist.

What's the difference? Put simply, anytime you want to know if the current user is authentication, you can use request.user.is_authenticated() which will return True if the user is authenticated and False if they aren't. If request.user is an instance of AnonymousUser, request.user.is_authenticated() will always return False. This allows the developer (you!) to turn if request.user is not None and request.user.is_authenticated(): into if request.user.is_authenticated():. Less typing is a good thing in this case!

In our case, the client and the server will be running at different locations. The server will be running at http://localhost:3000/ and the client will be at http://localhost:5000/. The browser considers these two locations to be on different domains, similar to running the server on http://www.server.com and running the client on http://www.client.com. We will not be allowing external domains access to our cookies, so we have to find another alternative solution to using sessions.

If you're wondering why we won't be allowing access to our cookies, you should check out the articles on Cross-Origin Resource Sharing (CORS), and Cross-Site Request Forgery (CSRF) linked below. If you just want to start coding, check the boxes and move on.

Token-based authentication

The most common alternative to session-based authentication is token-based authentication, and we will be using a specific form of token-based authentication to secure our application.

With token-based auth, the server provides the client with a token upon a successful login request. This token is unique to the user logging in and is stored in the database along with the user's ID. The client is expected to send the token with future requests so the server can identify the user. The server does this by searching the database table containing all of the tokens that have been created. If a matching token is found, the server goes on to verify that the token is still valid. If no matching token is found, then we say the user is not authenticated.

Because tokens are stored in the database and not in cookies, token-based authentication will suit our needs.

Verifying tokens

We always have the option of storing more than just the user's ID with their token. We can also store things such as a date on which the token will expire. In this example we would need to make sure that this expiration has not passed. If it has, then the token is not valid. So we delete it from the database and ask the user to log in again.

JSON Web Tokens

JSON Web Token (JWT for short) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between two parties. You can think of JWTs as authentication tokens on steroids.

Remember when I said we'll be using a specific form of token-based authentication? JWTs are what I was referring to.

Why are JSON Web Tokens better than regular tokens?

There are a few benefits we get when going with JWTs over regular tokens:

JWT is an open standard. That means that all implementations of JWT should be fairly similar, which is a benefit when working with different languages and technologies. Regular tokens are more free-form, allowing the developer to decide how best to implement the tokens.

JWTs can contain all of the information about the user, which is convenient for the client.

Libraries handle the heavy lifting here. Rolling out your own authentication is dangerous, so we leave the important stuff to battle-tested libraries that we can trust.

Creating the User model

How about we get started?

The file conduit/apps/authentication/models.py stores the models we will use for authentication. If you cloned the repository earlier in the course, you'd notice that the directory conduit/apps/authentication/ already exists. However, the file models.py does not. You'll want to create this file yourself.

Our explanations of the code throughout this course is intentionally short. Between the comments in the code and the resources we link to, we're confident that you can find the information you need.

Create conduit/apps/authentication/models.py.

We will need the following imports to create the User and UserManager classes, so go ahead and add the following to the top of the file:

When customizing authentication in Django, one requirement is that you specify a custom Manager class with two methods: create_user and create_superuser. To learn about custom authentication in Django, read Substituting a custom User model.

Let's start by creating the UserManager class.

Type the code for the UserManager class into conduit/apps/authentication/models.py and take note of the comments:

classUserManager(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`.
All we have to do is override the `create_user` function which we will use
to create `User` objects.
"""defcreate_user(self,username,email,password=None):"""Create and return a `User` with an email, username and password."""ifusernameisNone:raiseTypeError('Users must have a username.')ifemailisNone:raiseTypeError('Users must have an email address.')user=self.model(username=username,email=self.normalize_email(email))user.set_password(password)user.save()returnuserdefcreate_superuser(self,username,email,password):"""
Create and return a `User` with superuser (admin) permissions.
"""ifpasswordisNone:raiseTypeError('Superusers must have a password.')user=self.create_user(username,email,password)user.is_superuser=Trueuser.is_staff=Trueuser.save()returnuser

Now that we have the manager class, we can create the User model.

Add the User model to the bottom of conduit/apps/authentication/models.py.

classUser(AbstractBaseUser,PermissionsMixin):# Each `User` needs a human-readable unique identifier that we can use to# represent the `User` in the UI. We want to index this column in the# database to improve lookup performance.username=models.CharField(db_index=True,max_length=255,unique=True)# We also need a way to contact the user and a way for the user to identify# themselves when logging in. Since we need an email address for contacting# the user anyways, we will also use the email for logging in because it is# the most common form of login credential at the time of writing.email=models.EmailField(db_index=True,unique=True)# When a user no longer wishes to use our platform, they may try to delete# their account. That's a problem for us because the data we collect is# valuable to us and we don't want to delete it. We# will simply offer users a way to deactivate their account instead of# letting them delete it. That way they won't show up on the site anymore,# but we can still analyze the data.is_active=models.BooleanField(default=True)# The `is_staff` flag is expected by Django to determine who can and cannot# log into the Django admin site. For most users this flag will always be# false.is_staff=models.BooleanField(default=False)# A timestamp representing when this object was created.created_at=models.DateTimeField(auto_now_add=True)# A timestamp reprensenting when this object was last updated.updated_at=models.DateTimeField(auto_now=True)# More fields required by Django when specifying a custom user model.# The `USERNAME_FIELD` property tells us which field we will use to log in.# In this case we want it to be the email field.USERNAME_FIELD='email'REQUIRED_FIELDS=['username']# Tells Django that the UserManager class defined above should manage# objects of this type.objects=UserManager()def__str__(self):"""
Returns a string representation of this `User`.
This string is used when a `User` is printed in the console.
"""returnself.email@propertydeftoken(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".
"""returnself._generate_jwt_token()defget_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.
"""returnself.usernamedefget_short_name(self):"""
This method is required by Django for things like handling emails.
Typically, this would be the user's first name. Since we do not store
the user's real name, we return their username instead.
"""returnself.usernamedef_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')returntoken.decode('utf-8')

Glance over the above snippet for a few minutes and then do the following:

If you want to know more about custom authentication in Django, the docs are a great place to learn. The following links are optional, but we recommend you check them out if interested:

Model field reference lists the different field types supported by Django and the options that each field accepts (things like db_index and unique).

Specifying the AUTH_USER_MODEL setting

By default, Django assumes that the user model is django.contrib.auth.models.User. We want to use our own custom User though. Since we've created the User class, the next thing we need to do is to tell Django to use our User model instead of using its own.

If you've already migrated your database before specifying the custom User model, you may need to delete your database and re-run your migrations.

Tell Django about our User model by specifying the AUTH_USER_MODEL setting in conduit/settings.py.

To set the AUTH_USER_MODEL setting, type the following at the bottom of conduit/settings.py:

# Tell Django about the custom `User` model we created. The string# `authentication.User` tells Django we are referring to the `User` model in# the `authentication` module. This module is registered above in a setting# called `INSTALLED_APPS`.AUTH_USER_MODEL='authentication.User'

Creating and running migrations

As we add new models and change existing models, we will need to update the database to reflect these changes. Migrations are what Django uses to tell the database that something has changed, and ours will tell the database that we need to add a new table for our custom User model.

**Note: **If you have run python manage.py makemigrations or python manage.py migrate, you'll need to delete your database before continuing. If you're using SQLite as your database, then all you need to do is delete the file called db.sqlite3 in the root directory of your project.

If you're using PostgreSQL, then follow the instructions below:

If you have run python manage.py makemigrations or python manage.py migrate already, please delete the db.sqlite3 file from the root directory of your project. Django gets unhappy if you change AUTH_USER_MODEL after creating the database and it's best to just drop the database and start anew.

Now you're ready to create and apply migrations. After that, we can create our first user.

To create migrations, you'll need to run the following command in your console to create migrations:

python manage.py makemigrations

This creates the default migrations for our new Django project. However, it will not create migrations for new apps inside of our project. The first time we want to create migrations for a new app, we have to be more explicit about it.

To create a set of migrations for the authentication app, run the following:

python manage.py makemigrations authentication

This will create the initial migration for the authentication app. In the future, whenever you want to generate migrations for the authentication app, you only need to run python manage.py makemigrations.

Make the default migrations by running python manage.py makemigrations.

Unlike the makemigrations command, you never need to specify the app to be migrated when running the migrate command.

Apply the newly create migrations by running make migrate.

Our first user

We've created our User model, and our database is up and running. The next thing to do is to create our first User object. We'll make this user a superuser since we'll be using it to test our site.

Create your first user by running the following command:

python manage.py createsuperuser

Django will ask you a few questions such as your email, username, and password. Once answered, your new user will be created. Congratulations!

Create your first user by running python manage.py createsuperuser.

To test that the user has been created, let's start by opening a Django shell from the command line:

python manage.py shell_plus

If you've used Django before, you may be familiar with the shell command, but not with shell_plus. shell_plus is provided by a library called django-extensions, which has been included in the boilerplate project you cloned before starting this course. It's useful because it automatically imports the models for each app in the INSTALLED_APPS setting. It can also be set up to import other utilities automatically.

Once the shell is open, run the following:

NOTE: Only type what appears on lines preceded by >>>. Lines that are not preceded by >>> are output from the previous command.

Registering new users

At the moment a user can't do anything interesting. Our next task is to create an endpoint for registering new users.

RegistrationSerializer

Create conduit/apps/authentication/serializers.py.

Start by creating conduit/apps/authentication/serializers.py and typing the following code:

fromrest_frameworkimportserializersfrom.modelsimportUserclassRegistrationSerializer(serializers.ModelSerializer):"""Serializers registration requests and creates a new user."""# Ensure passwords are at least 8 characters long, no longer than 128# characters, and can not be read by the client.password=serializers.CharField(max_length=128,min_length=8,write_only=True)# 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)classMeta: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','token']defcreate(self,validated_data):# Use the `create_user` method we wrote earlier to create a new user.returnUser.objects.create_user(**validated_data)

Read through the code, paying special attention to the comments, and then move on when you're done.

In the code above, we created a class RegistrationSerializer that inherits from serializers.ModelSerializer. serializers.ModelSerializer is just an abstraction on top of serializers.Serializer, which you probably remember from the Django REST Framework (DRF) tutorial. ModelSerializer handles a few things relevant to serializing Django models for us, so we don't have to.

Another thing to point out is that it allows you to specify two methods: create and update. In the above example we wrote our own create method using User.objects.create_user, but we did not specify the update method. In this case, DRF will use it's own default update method to update a user.

RegistrationAPIView

We can now serialize requests and responses for registering a user. Next, we want to create a view to use as an endpoint, so the client will have a URL to hit to create a new user.

Create conduit/apps/authentication/views.py and type the following:

fromrest_frameworkimportstatusfromrest_framework.permissionsimportAllowAnyfromrest_framework.responseimportResponsefromrest_framework.viewsimportAPIViewfrom.serializersimportRegistrationSerializerclassRegistrationAPIView(APIView):# Allow any user (authenticated or not) to hit this endpoint.permission_classes=(AllowAny,)serializer_class=RegistrationSerializerdefpost(self,request):user=request.data.get('user',{})# The create serializer, validate serializer, save serializer pattern# below is common and you will see it a lot throughout this course and# your own work later on. Get familiar with it.serializer=self.serializer_class(data=user)serializer.is_valid(raise_exception=True)serializer.save()returnResponse(serializer.data,status=status.HTTP_201_CREATED)

Let's talk about a couple of new things in this snippet:

The permission_classes property is how we decide who can use this endpoint. We can restrict this to authenticated users or admin users. We can also allow users who are authenticated or not based on whether this endpoint they're hitting is "safe" — that means the endpoint is a GET, HEAD, or OPTIONS request. For this course, you only need to know about GET requests. We will talk more about permissions_classes later on.

The create serializer, validate serializer, save serializer pattern you see inside the post method is very common when using DRF. You'll want to familiarize yourself with this pattern as you will be using it a lot.

Now we need to get the routing for our project set up. There were some changes introduced from Django 1.x => 2.x switching from url to path. There are quite a few questions on adapting the old regular expression url to the new way of doing things in a path. Considerate Code created a URL to Path Cheatsheet and write-up on the change. Some unique circumstances might necessitate the need for using re_path and using regular expressions.

Create conduit/authentication/urls.py and place the following into the file to handle our routes for this application:

If you're coming from another framework like Rails, it is common to put all of your routes in a single file. While you can do this in Django, it is considered best practice to create modular app specific paths. This forces you to think about your app design and keeping them self contained and reusable. That's what we've done here. We've also specified app_name = 'authentication' so that we can use an include and keep our application modular. Now let's include the above file in our global URLs file.

Create conduit/apps/authentication/urls.py.

Open conduit/urls.py and you'll see the following line near the top of the file:

fromdjango.urlsimportpath

The first thing we want to do is import a method called include from django.urls:

-from django.urls import path
+from django.urls import include, path

The include method let's include another urls.py file without having to do a bunch of work like importing and then re-registering the routes in this file.

Include the URLs from the authentication app in the main urls.py file.

Registering a user with Postman

Now that we've created the User model and added an endpoint for registering new users, we're going to run a quick sanity check to make sure we're on track. To do this, we're going to use a tool called Postman with a pre-made collection of endpoints.

Open Postman and use the "Register" request inside the "Auth" folder to create a new user.

Make your first Postman request.

Awesome! We're making some real progress now!

There is one thing we need to fix though. Notice how the response from the "Register" request has all of the user's information at the root level. Our client expects this information to be namespaced under "user." To do that, we'll need to create a custom DRF renderer.

Rendering User objects

Create a file called conduit/apps/authentication/renderers.py and type the following content:

importjsonfromrest_framework.renderersimportJSONRendererclassUserJSONRenderer(JSONRenderer):charset='utf-8'defrender(self,data,media_type=None,renderer_context=None):# If we receive a `token` key as part of the response, it will be a# byte object. Byte objects don't serialize well, so we need to# decode it before rendering the User object.token=data.get('token',None)iftokenisnotNoneandisinstance(token,bytes):# Also as mentioned above, 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.returnjson.dumps({'user':data})

Create conduit/apps/authentication/renderers.py.

There's nothing new or interesting happening here, so just read through the comments in the snippet and then we can move on.

Now open conduit/apps/authentication/views.py and import UserJSONRenderer by adding the following line to the top of your file:

from.renderersimportUserJSONRenderer

You'll also need to set the renderer_classes property of the RegistrationAPIView class like so:

Update RegistrationAPIView to use the UserJSONRenderer renderer class.

With UserJSONRenderer in place, go ahead and use the "Register" Postman request to create a new user. Notice how this time the response is inside the "user" namespace.

Register a new user with Postman.

Logging users in

Since users can now register for Conduit, we need to build a way for them to log into their account. In this lesson, we will add the serializer and view needed for users to log in. We will also start looking at how our API should handle errors.

LoginSerializer

Open conduit/apps/authentication/serializers.py and add the following import to the top of the file:

fromdjango.contrib.authimportauthenticate

After that, create the following serializer in the same file:

classLoginSerializer(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)defvalidate(self,data):# The `validate` method is where we make sure that the current# instance of `LoginSerializer` has "valid". In the case of logging a# user in, this means validating that they've provided an email# and password and that this combination matches one of the users in# our database.email=data.get('email',None)password=data.get('password',None)# Raise an exception if an# email is not provided.ifemailisNone:raiseserializers.ValidationError('An email address is required to log in.')# Raise an exception if a# password is not provided.ifpasswordisNone:raiseserializers.ValidationError('A password is required to log in.')# The `authenticate` method is provided by Django and handles checking# for a user that matches this email/password combination. Notice how# we pass `email` as the `username` value since in our User# model we set `USERNAME_FIELD` as `email`.user=authenticate(username=email,password=password)# If no user was found matching this email/password combination then# `authenticate` will return `None`. Raise an exception in this case.ifuserisNone:raiseserializers.ValidationError('A user with this email and password was not found.')# Django provides a flag on our `User` model called `is_active`. The# purpose of this flag is to tell us whether the user has been banned# or deactivated. This will almost never be the case, but# it is worth checking. Raise an exception in this case.ifnotuser.is_active:raiseserializers.ValidationError('This user has been deactivated.')# 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.return{'email':user.email,'username':user.username,'token':user.token}

Add the new LoginSerializer class to conduit/apps/authentication/serializers.py.

With the serializer in place, let's move on to creating the view.

LoginAPIView

Open conduit/apps/authentication/views.py and update the following import:

classLoginAPIView(APIView):permission_classes=(AllowAny,)renderer_classes=(UserJSONRenderer,)serializer_class=LoginSerializerdefpost(self,request):user=request.data.get('user',{})# Notice here that we do not call `serializer.save()` like we did for# the registration endpoint. This is because we don't have# anything to save. Instead, the `validate` method on our serializer# handles everything we need.serializer=self.serializer_class(data=user)serializer.is_valid(raise_exception=True)returnResponse(serializer.data,status=status.HTTP_200_OK)

Add the new LoginAPIView class to conduit/apps/authentication/views.py.

Open conduit/apps/authentication/urls.py and update the following import:

Add a URL pattern for LoginAPIView to conduit/apps/authentication/urls.py.

Logging a user in with Postman

At this point, a user should be able to log in by hitting the new login endpoint. Let's test this out. Open Postman and use the "Login" request to log in with one of the users you created previously. If the login attempt was successful, the response will include a token that can be used in the future when making requests that require the user be authenticated.

Log in a user using Postman.

There is something else we need to handle here. Try using the "Login" request to log in with an invalid email/password combination. Notice the error response. There are two problems with this.

First of all, non_field_errors sounds strange. Usually, this key corresponds to whatever field caused the serializer to fail validation. Since we overrode the validate method, instead of a field-specific method such as validate_email, the Django REST Framework didn't know what field to attribute the error. The default is non_field_errors, and since our client will be using this key to display errors, we're going to change this to say error.

Secondly, the client expects any errors to be namespaced under the errors key in a JSON response, similar to how we namespaced the login and register requests under the user key. We will accomplish this by overriding Django REST Framework's (DRF) default error handling.

Overriding EXCEPTION_HANDLER and NON_FIELD_ERRORS_KEY

One of DRF's settings is called EXCEPTION_HANDLER that returns a dictionary of errors. We want our errors namespaced under the errors key, so we're going to have to override EXCEPTION_HANDLER. We will also override NON_FIELD_ERRORS_KEY as mentioned earlier.

Let's start by creating conduit/apps/core/exceptions.py and adding the following snippet:

fromrest_framework.viewsimportexception_handlerdefcore_exception_handler(exc,context):# If an exception is thrown that we don't explicitly handle here, we want# to delegate to the default exception handler offered by DRF. If we do# handle this exception type, we will still want access to the response# generated by DRF, so we get that response up front.response=exception_handler(exc,context)handlers={'ValidationError':_handle_generic_error}# This is how we identify the type of the current exception. We will use# this in a moment to see whether we should handle this exception or let# Django REST Framework do its thing.exception_class=exc.__class__.__name__ifexception_classinhandlers:# If this exception is one that we can handle, handle it. Otherwise,# return the response generated earlier by the default exception # handler.returnhandlers[exception_class](exc,context,response)returnresponsedef_handle_generic_error(exc,context,response):# This is the most straightforward exception handler we can create.# We take the response generated by DRF and wrap it in the `errors` key.response.data={'errors':response.data}returnresponse

Create conduit/apps/core/exceptions.py with the above code.

With that taken care of, open up conduit/settings.py and add a new setting to the bottom of the file called REST_FRAMEWORK, like so:

Add the REST_FRAMEWORK dict to conduit/settings.py with the EXCEPTION_HANDLER and NON_FIELD_ERRORS_KEY keys as above.

This is how we override settings in DRF. We will add one more settings in a bit when we start writing views that require the user to be authenticated.

Let's try sending another login request using Postman. Be sure to use an email/password combination that is invalid.

Use Postman to log in with an invalid username and password. Make sure the response is formatted as we discussed earlier.

Updating UserJSONRenderer

Uh oh! Still not quite what we want. We've got the errors key now, but everything is namespaced under the user key. That's not good.

Let's update UserJSONRenderer to check for the errors key and do things a bit differently. Open up conduit/apps/authentication/renderers.py and make these changes:

class UserJSONRenderer(JSONRenderer):
charset = 'utf-8'
def render(self, data, media_type=None, renderer_context=None):
+ # If the view throws an error (such as the user can't be authenticated
+ # or something similar), `data` will contain an `errors` key. We want
+ # the default JSONRenderer to handle rendering errors, so we need to
+ # check for this case.
+ errors = data.get('errors', None)
# If we receive a `token` key in the response, it will be a
# byte object. Byte objects don't serializer well, so we need to
# decode it before rendering the User object.
token = data.get('token', None)
+ if errors is not None:
+ # As mentioned above, 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({
'user': data
})

Update UserJSONRenderer in conduit/apps/authentication/renderers.py to handle the case where data contains an errors key.

Now send the login request with Postman one more time and all should be well.

Use Postman to log in with an invalid username and password. Make sure the response has a users key instead of an errors key.

Retrieving and updating users

Users can register new accounts and log into those accounts. Now users will need a way to retrieve and update their information. Let's implement this before we move on to creating user profiles.

UserSerializer

We're going to create one more serializer for the profile. We've got serializers for login and register requests, but we need to be able to serializer user objects too.

Open conduit/apps/authentication/serializers.py and add the following:

classUserSerializer(serializers.ModelSerializer):"""Handles serialization and deserialization of User objects."""# 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 lets just stick with the defaults.password=serializers.CharField(max_length=128,min_length=8,write_only=True)classMeta:model=Userfields=('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# above. The reason we want to use `read_only_fields` here is that# we don't need to specify anything else about the field. The# password field needed the `min_length` and # `max_length` properties, but that isn't the case for the token# field.read_only_fields=('token',)defupdate(self,instance,validated_data):"""Performs an update on a User."""# Passwords should not be handled with `setattr`, unlike other fields.# Django provides a function that handles hashing and# salting passwords. That means# we need to remove the password field from the# `validated_data` dictionary before iterating over it.password=validated_data.pop('password',None)for(key,value)invalidated_data.items():# For the keys remaining in `validated_data`, we will set them on# the current `User` instance one at a time.setattr(instance,key,value)ifpasswordisnotNone:# `.set_password()` handles all# of the security stuff that we shouldn't be concerned with.instance.set_password(password)# After everything has been updated we must explicitly save# the model. It's worth pointing out that `.set_password()` does not# save the model.instance.save()returninstance

Add the new UserSerializer class to conduit/apps/authentication/serializers.py.

It's worth pointing out that we do not explicitly define the create method in the serializer since DRF provides a default create method for all instances of serializers.ModelSerializer. It's possible to create a user with this serializer, but we want the creation of a User to be handled by RegistrationSerializer.

UserRetrieveUpdateAPIView

Open up conduit/apps/authentication/views.py and update the imports like so:

Below the imports, create a new view called UserRetrieveUpdateAPIView:

classUserRetrieveUpdateAPIView(RetrieveUpdateAPIView):permission_classes=(IsAuthenticated,)renderer_classes=(UserJSONRenderer,)serializer_class=UserSerializerdefretrieve(self,request,*args,**kwargs):# There is nothing to validate or save here. Instead, we just want the# serializer to handle turning our `User` object into something that# can be JSONified and sent to the client.serializer=self.serializer_class(request.user)returnResponse(serializer.data,status=status.HTTP_200_OK)defupdate(self,request,*args,**kwargs):serializer_data=request.data.get('user',{})# Here is that serialize, validate, save pattern we talked about# before.serializer=self.serializer_class(request.user,data=serializer_data,partial=True)serializer.is_valid(raise_exception=True)serializer.save()returnResponse(serializer.data,status=status.HTTP_200_OK)

Add the new UserRetrieveUpdateAPIView class to conduit/apps/authentication/views.py.

Now go over to conduit/apps/authentication/urls.py and update the imports to include UserRetrieveUpdateAPIView:

Add a URL pattern for UserRetrieveUpdateAPIView to conduit/apps/authentication/urls.py.

Open Postman again and send the "Current User" request. You should get an error with a response that looks like this:

{"user":{"detail":"Authentication credentials were not provided."}}

Use Postman to request data on the current user. You should get an error saying Authentication credenetials were not provided.

Authenticating Users

Django has this idea of authentication backends. Without going into too much detail, a backend is essentially a plan for deciding whether a user is authenticated. We'll need to create a custom backend to support JWT since this isn't supported by Django nor Django REST Framework(DRF) by default.

Create and open conduit/apps/authentication/backends.py and add the following code:

importjwtfromdjango.confimportsettingsfromrest_frameworkimportauthentication,exceptionsfrom.modelsimportUserclassJWTAuthentication(authentication.BaseAuthentication):authentication_header_prefix='Token'defauthenticate(self,request):"""
The `authenticate` method is called on every request regardless of
whether the endpoint requires authentication.
`authenticate` has two possible return values:
1) `None` - We return `None` if we do not wish to authenticate. Usually
this means we know authentication will fail. An example of
this is when the request does not include a token in the
headers.
2) `(user, token)` - We return a user/token combination when
authentication is successful.
If neither case is met, that means there's an error
and we do not return anything.
We simple raise the `AuthenticationFailed`
exception and let Django REST Framework
handle the rest.
"""request.user=None# `auth_header` should be an array with two elements: 1) the name of# the authentication header (in this case, "Token") and 2) the JWT # that we should authenticate against.auth_header=authentication.get_authorization_header(request).split()auth_header_prefix=self.authentication_header_prefix.lower()ifnotauth_header:returnNoneiflen(auth_header)==1:# Invalid token header. No credentials provided. Do not attempt to# authenticate.returnNoneeliflen(auth_header)>2:# Invalid token header. The Token string should not contain spaces. Do# not attempt to authenticate.returnNone# 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')ifprefix.lower()!=auth_header_prefix:# The auth header prefix is not what we expected. Do not attempt to# authenticate.returnNone# By now, we are sure there is a *chance* that authentication will# succeed. We delegate the actual credentials authentication to the# method below.returnself._authenticate_credentials(request,token)def_authenticate_credentials(self,request,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.'raiseexceptions.AuthenticationFailed(msg)try:user=User.objects.get(pk=payload['id'])exceptUser.DoesNotExist:msg='No user matching this token was found.'raiseexceptions.AuthenticationFailed(msg)ifnotuser.is_active:msg='This user has been deactivated.'raiseexceptions.AuthenticationFailed(msg)return(user,token)

Create conduit/apps/authentication/backends.py

There is a lot of logic and exceptions being thrown in this file, but the code is pretty straight forward. All we've done is list conditions where the user would not be authenticated and throw an exception if any of those conditions are true.

There isn't any extra reading to do here, but feel free to check out the docs for the PyJWT library if you're interested.

Telling DRF about our authentication backend

We must explicitly tell Django REST Framework which authentication backend we want to use, similar to how we told Django to use our custom User model.

Open up conduit/settings.py and update the REST_FRAMEWORK dict with a new key:

Retrieving and updating users with Postman

Now that our new authentication backend is in place, the authentication error we saw a while ago should be gone. Test this by opening Postman and sending another "Current User" request. The request should be successful, and you should see the information about your user in the response.

Use Postman to request the current user. This time the request should be successful.

Remember that we created an update endpoint at the same time we created the retrieve endpoint. Let's test this one too. Send the request labeled "Update User" in the "Auth" folder in Postman. If you used the default body, then the email of your user should have changed. Feel free to play around with these requests to make changes to your user.

Use Postman to update the current user. Play around with this request to make changes to your user.

On to better things

That's all there is for this chapter. We've created a user model and serialized users in three different ways. There are four shiny new endpoints that let users register, login, and retrieve and update their information. We're off to a strong start here!

Next up we're going to create profiles for our users. You may have noticed that the User model is pretty bare-bones. We only included things essential for authentication. Other information such as a biography and avatar URL will go in the Profile model that we'll build out in the next chapter.