Chapter 14: Authorization
Our social messaging app is basically done at this point but there’s a big, big issue we need to fix. Right now, anyone can visit any page. That means if a random user without an account knows the URL for creating a new message they can add messages! Try this for yourself. Click on the “logout” link in the dropdown list on the nav bar under your username.
Once logged out, go to http://127.0.0.1:8000/posts/new/, add text for a message, and click “Save.” You’ll see an error.
This is because our model expects an
author field which is linked to the current logged-in user. Since there’s no author, the submission fails.
Even if we fixed this particular error, the fact that a non-loggedin user can view the entire site poses additional security challenges. What if a malicious person wrote a program to submit a million different message posts and comments? If you require a user account, you can simply block this person. But with no user account required, there are limited ways to prevent such an attack.
This general problem is known as authorization. It’s common to set different rules around who is authorized to view areas of your site. Note that this is different than authentication which is the process of registering and logging-in users. Authorization restricts access; authentication enables a user signup and login flow.
As a mature web framework, Django has built-in functionality for authorization that we can quickly use. Throughout the rest of this chapter we’ll limit access to various pages only to logged-in users. We’ll also add our tests and deploy the site to Heroku.
A mixin is a special kind of multiple inheritance that Django uses to both avoid duplicate code and allow for customization. For example, the built-in generic ListView needs a way to return a template. But so does DetailView and in fact almost every other view. Rather than repeat the same code in each big generic view, Django breaks out this functionality into a “mixin” known as TemplateResponseMixin. Both
DetailView use this mixin to render the proper template.
If you read the Django source code, which is freely available on Github, you’ll see mixins used all over the place.
To restrict view access to only logged in users, Django has a LoginRequired mixin that we can use. It’s powerful and extremely concise.
Within the existing
posts/views.py file import it at the top and then add
LoginRequiredMixin to our
PostCreateView. Make sure that the mixin is to the left of
ListView so it will be read first. We want the
ListView to already know we intend to restrict access.
And that’s it! We’re done.
# posts/views.py from django.contrib.auth.mixins import LoginRequiredMixin # new from django.views.generic import ListView, DetailView from django.views.generic.edit import CreateView, UpdateView, DeleteView from django.urls import reverse_lazy from . import models class PostCreateView(LoginRequiredMixin, CreateView): model = models.Post template_name = 'post_new.html' fields = ['message'] def form_valid(self, form): form.instance.author = self.request.user return super().form_valid(form) ...
Now return to the URL for creating new messages at http://127.0.0.1:8000/posts/new/ and you’ll see the following error:
What’s happening? Django has automatically redirected us to the default location for the
login page which is at
/accounts/login however if you recall, in our project-level URLs we are using
users/ as our route. That’s why our login page is at
users/login. So how do we tell our
PostCreateView about this?
If you look at the documation for LoginRequired mixin it tells us the answer. We can add a
login_url to override the default parameter. We’re using the named URL of our login route here
# posts/views.py class PostCreateView(LoginRequiredMixin, CreateView): model = models.Post template_name = 'post_new.html' fields = ['message'] login_url = 'login' # new def form_valid(self, form): form.instance.author = self.request.user return super().form_valid(form)
Try the link for creating new messages again: http://127.0.0.1:8000/posts/new/.
It now redirects users to the login page. Just as we desired!
Now we see that restricting view access is just a matter of adding
LoginRequiredMixin at the beginning of all existing views and specifying the correct
login_url. Let’s update the rest of our
posts views since we don’t want a user to be able to create, read, update, or delete a message if they aren’t logged in.
views.py file should now look like this:
# posts/views.py from django.contrib.auth.mixins import LoginRequiredMixin from django.views.generic import ListView, DetailView from django.views.generic.edit import CreateView, UpdateView, DeleteView from django.urls import reverse_lazy from . import models class PostListView(LoginRequiredMixin, ListView): # new model = models.Post template_name = 'post_list.html' login_url = 'login' # new class PostCreateView(LoginRequiredMixin, CreateView): # new model = models.Post template_name = 'post_new.html' fields = ['message'] login_url = 'login' # new def form_valid(self, form): form.instance.author = self.request.user return super().form_valid(form) class PostDetailView(LoginRequiredMixin, DetailView): # new model = models.Post template_name = 'post_detail.html' login_url = 'login' # new class PostUpdateView(LoginRequiredMixin, UpdateView): # new model = models.Post fields = ['message'] template_name = 'post_edit.html' login_url = 'login' # new class PostDeleteView(LoginRequiredMixin, DeleteView): # new model = models.Post template_name = 'post_delete.html' success_url = reverse_lazy('posts') login_url = 'login' # new
Go ahead and play around with the site to confirm that the login redirects now work as expected. If you need help recalling what the proper URLs are, log in first and write down the URLs for each of the CRUD functionality.
Tests & Deployment
Author’s note: I’m still working on the text for the tests and deployment instructions for this app. Otherwise it’s done though and you can proceed to the conclusion.