A RESTful password locker with Django and backbone.js part 4

28 May

So far we’ve got a single-user password storing application. That’s not very secure or useful 🙂 So now we’re going to support multiple users and lock down the app so users must be authenticated. We’ll also lock down the REST API.

There’s now sufficient code that I’ll point you in the direction of certain files in the repository for some of the more standard Django code for things such as user registration.

Supporting multiple users

I created a registration form in `apps/users/forms.py` and updated `urls.py` so the django registration app would use it. The registration form asks for users first and last names, email address, a user name and their password (twice). This is pretty standard stuff.

Now users can register, we need to update the Password model in `apps/passwords/models.py` so passwords are associated with the user that created them. Add a new foreign key to django.contrib.auth.models.User:

from django.db import models
from django.contrib.auth.models import User

class Password(models.Model):
    """
    Represents a username and password together with several other fields
    """
    created_by = models.ForeignKey(User, related_name='+', editable=False)
    title = models.CharField(max_length=200)
    username = models.CharField(max_length=200,
        blank=True)
    password = models.CharField(max_length=200)
    url = models.URLField(max_length=500,
        blank=True,
        verbose_name='Site URL')
    notes = models.CharField(
        max_length=500,
        blank=True)
    created_at = models.DateTimeField(auto_now_add=True, editable=False)
    updated_at = models.DateTimeField(auto_now=True, editable=False)

    def __unicode__(self):
        return self.title

We’ll use South to create a migration with `./manage.py schemamigration passwords –auto`. When it asks what to do about defaults for the created_by column, just make it set the column to ‘1’ (or any number you like). Since we’re developing, and we don’t have legacy data to support, we can don’t need to maintain the integrity of the database at this point.

Apply the migration with `./manage.py migrate passwords`.

Locking down the REST API

We need to do two things with the API:

  1. Require users to be authenticated to access it,
  2. Restrict data users can work with to only that which they have created.

Because Django REST API uses generic class-based views, it’s very simple to add these constraints. Simply create `apps/api/views.py` and add the following:

from djangorestframework.mixins import ModelMixin
from djangorestframework.permissions import IsAuthenticated
from djangorestframework.views import ListOrCreateModelView, InstanceModelView

from apps.passwords.resources import PasswordResource

class RestrictToUserMixin(ModelMixin):
    """
    Mixin that restricts users to working with their own data
    """
    def get_queryset(self):
        """
        Only return objects created by the currently authenticated user.
        """
        return self.resource.model.objects.filter(created_by=self.user)

    def get_instance_data(self, model, content, **kwargs):
        """
        Set the created_by field to the currently authenticated user.
        """
        content['created_by'] = self.user
        return super(RestrictToUserMixin, self).get_instance_data(model, content, **kwargs)

class PasswordListView(RestrictToUserMixin, ListOrCreateModelView):
    """
    List view for Password objects.
    """
    resource = PasswordResource
    permissions = (IsAuthenticated, )

class PasswordInstanceView(RestrictToUserMixin, InstanceModelView):
    """
    View for individual Password instances
    """
    resource = PasswordResource
    permissions = (IsAuthenticated, )

We’ve created a mixin to add the same logic to both classes. The mixin filters the queryset used by the API methods so that the `created_by` field is the currently authenticated user. This prevents users from accessing data belonging to other users. It also sets the `created_by` field to the currently authenticated user for CREATE (and UPDATE) operations. It’s very simple and very elegant.

The `permissions` tuple instructs Django REST API to require that users are authenticated in order to access those resources.

And now update `apps/api/urls.py` to use these views instead of the others we had configured:

from django.conf.urls.defaults import patterns, url

from views import PasswordListView, PasswordInstanceView

urlpatterns = patterns('',
    url(r'^passwords/$', PasswordListView.as_view(), name='passwords_api_root'),
    url(r'^passwords/(?P<id>[0-9]+)$', PasswordInstanceView.as_view(), name='passwords_api_instance'),
)

If you check out the API browser at http://localhost:8000/api/1.0/passwords/ you should notice you need to be logged in to access anything. Once you register and log in, you can use the API, but you can only view data associated with the account you’ve logged in as. However, if you try to use the front-end application, it will load the list of passwords, but CREATE, UPDATE and DELETE API access will be refused. You can see this if you have firebug open, or if you reload the page. This is because Django is now enforcing CSRF tokens.

Before we fix this, lets go off on a little tangent. Unless you had firebug open, you may not have spotted that the API was refusing your access – the app appeared to work correctly. So let’s add error handling to the backbone.js application so it’ll be easier for us to know when we’ve fixed this issue.

To be alerted when deletions fail, update `staticfiles/js/passwords.js` as follows:

    var Password = Backbone.Model.extend({
        ...
        remove: function(options) {
            mergedOptions = {wait: true};
            $.extend(mergedOptions, options);
            this.destroy(mergedOptions);
        },
    ...
    });

    var PasswordView = Backbone.View.extend({
        ...
        remove: function(event) {
            event.stopImmediatePropagation();
            event.preventDefault();
            if (confirm("Are you sure you want to delete this entry?"))
            {
                this.model.remove({error: function(model, response) {
                        if (response.status == 403) {
                            alert("You don't have permission to delete that data");
                        }
                        else {
                            alert("Unable to delete that data");
                        }
                    }
                });
            }
        },
        ...
    });

    var PasswordListView = Backbone.View.extend({
        ...
        addNew: function(password, options) {
            mergedOptions = {wait: true};
            $.extend(mergedOptions, options);
            this.passwords.create(password, mergedOptions);
            return this;
        },

        updatePassword: function(passwordData, options) {
            options = options || {};
            var password = this.passwords.get(passwordData.id);
            if (_.isObject(password))
            {
                // iterate through all the data in passwordData, setting it
                // to the password model
                for (var key in passwordData)
                {
                    // ignore the ID attribute
                    if (key != 'id')
                    {
                        password.set(key, passwordData[key]);
                    }
                }

                // persist the change
                password.save({}, options);
                this.passwords.sort();
            }
        },
        ...
    });

    var AppView = Backbone.View.extend({
        ...
        displayError: function(model, response) {
            if (response.status == 403) {
                alert("You don't have permission to edit that data");
            }
            else {
                alert("Unable to create or edit that data. Please make sure you entered valid data.");
            }
        },

        handleModal: function(event) {
            event.preventDefault();
            event.stopImmediatePropagation();
            var form = $('#passwordForm');

            var passwordData = {
                title: $(form).find('#id_title').val(),
                username: $(form).find('#id_username').val(),
                password: $(form).find('#id_password').val(),
                url: $(form).find('#id_url').val(),
                notes: $(form).find('#id_notes').val()
            };

            if ($('#passwordModal').data('passwordId'))
            {
                passwordData.id = $('#passwordModal').data('passwordId');
                this.passwordList.updatePassword(passwordData, { error: this.displayError });
            }
            else
            {
                // add or update the password
                this.passwordList.addNew(passwordData, { error: this.displayError });
            }

            // hide the modal
            $('#passwordModal').modal('hide');

            return this;
        },
        ...
    });

We’re passing an error handler through the code to the CRUD methods. The handler checks the status code and displays an appropriate message. Now when we try to use the javascript app, we at least have some feedback that things are failing.

We’ve also made backbone.js wait until it receives a response from the server before firing a ‘change’ event and updating the UI (with the `{wait: true}` option), so the UI will only update on success.

Finally, to fix this CSRF issue, we need to add the `{% csrf_token %}` template tag to `templates/passwords/password_list.html`. Just add it after the `{{ form }}` tag. This adds the actual token to the template.

Then we need to tweak `staticfiles/js/passwords.js` so jQuery will send the token as a header with each AJAX request.

Add the following at the end of the javascript code, just inside the final closing braces of the `$(function() {})` function:

// Setup $.ajax to always send an X-CSRFToken header:
    var csrfToken = $('input[name=csrfmiddlewaretoken]').val();
    $(document).ajaxSend(function(e, xhr, settings) {
        xhr.setRequestHeader('X-CSRFToken', csrfToken);
    });

Now, try creating several different users, create, edit and delete some data, and try to access data owned by other users. You should find that the API correctly restricts your access to data, and that the front-end javascript app works correctly too.

Masking passwords

For added security, we’ll mask the passwords in the UI, and only reveal them when the user moves their mouse over them. This stops people being able to see your complete list of credentials if they can see your screen.

First, we’ll update the backbone.js app in `staticfiles/js/passwords.js` to set a new property on the model called ‘maskedPassword’ which will just be a string of asterisks. We’ll also add some events for displaying and hiding the clear passwords:

    var Password = Backbone.Model.extend({
        initialize: function() {
            this.hidePassword();
        },

        // display the password
        showPassword: function() {
            this.set({"maskedPassword": this.get('password')});
        },

        // hide the password
        hidePassword: function() {
            this.set({"maskedPassword": '********'});
        },
        ...
    });

    var PasswordView = Backbone.View.extend({
        ...
        events: {
            "mouseover .password": "showPassword",
            "mouseout .password": "hidePassword",
            "click a.edit" : "editPassword",
            "click a.destroy" : "remove"
        },

        showPassword: function(event) {
            event.stopImmediatePropagation();
            this.model.showPassword();
        },

        hidePassword: function(event) {
            event.stopImmediatePropagation();
            this.model.hidePassword();
        },
        ...
    });

Now update the ICanHaz template in `templates/passwords/password_list.html` to populate the password field using `maskedPassword` instead of `password`:

    <script id="passwordRowTpl" type="text/html">
        <td>
            <a href="{{ url }}" target="_blank">
                {{ title }}
            </a>
        </td>
        <td>{{ username }}</td>
        <td class="password">{{ maskedPassword }}</td>
        <td>{{ notes }}</td>
        <td>
            <a href="#" class="edit" title="Edit this entry"><i class="icon-pencil"></i></a>
            <a href="#" class="destroy" title="Delete this entry"><i class="icon-remove"></i></a>
        </td>
    </script>

Reload the front-end app and it should load the list correctly with the passwords masked. It should also display the clear password when you hover the mouse over the asterisks. However, CRUD operations will fail because backbone.js will submit the extra `maskedPassword` field to the API, and Django REST framework will complain.

We’ve already got a way of ignoring certain fields submitted to the API, so just add `maskedPassword` to the `ignore_fields` tuple in `apps/passwords/resources.py`:

class PasswordResource(ModelResource):
    ...
    ignore_fields = ('created_at', 'updated_at', 'id', 'maskedPassword')
    ...

Summary

We’re done for this iteration. The app now supports multiple users, displays error messages to users and masks passwords.

In the penultimate part of this series, we’ll add the ability to share passwords between users.

Advertisements

3 Responses to “A RESTful password locker with Django and backbone.js part 4”

  1. clasense4 27 November, 2012 at 07:54 #

    Reblogged this on clasense4 blog and commented:
    Django + rest framework + backbone.js = awesome..

  2. Andrew 7 December, 2012 at 22:31 #

    Once a user has logged in, do you have to do anything special in Backbone.js to pass their session information with each API request? I would have expected that you would need to add a session token/id to each request, much as the CSRF token is added to each request, but this doesn’t seem to be the case.

    Is that information automatically included in requests? Or in other words, how does Backbone.js prove to the Django views that the user is logged in when hitting API endpoints?

    • 10kblogger 19 December, 2012 at 11:51 #

      I think it just gets handled using cookies as usual. Backbone makes requests, adds the user’s session ID as a cookie (nothing special there) and django authenticates them accordingly. I don’t think anything special was needed (but I wrote this a while ago so might be wrong).

      I think the only custom request work was to forward the CSRF token as you mentioned.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: