Backbone.js apps with Authentication Tutorial
At my current company I am working on my first large-scale production backbone.js app and I couldn’t be happier. After using backbone.js for a few months I have caught the vision and I am becoming more and more proficient. But every once and a while I still run into problems I would consider basic, but I can’t seem to find much help on the interwebs. Authentication with backbone.js apps was one of those problems. So I am posting the solution I came up with in hopes it will benefit someone else, and hopefully will garner some feedback or potentially better ways to solve authentication with Backbone.js.
Starting Code Base
To start this tutorial, I will be using an already created backbone.js application called Backbone Directory, created by Christophe Coenraets who has some great tutorials and information about backbone on his blog. He has some mobile versions of the app in the code base as well, but we will be working in the “web” directory.
Project Overview
Backbone Directory uses the Slim PHP framework on the server to communicate with backbone, but the principles we will be going over are language agnostic. In addition, Slim is based on the Sinatra (Ruby) methodology which in turn translates to Express.js framework for Node.js (JavaScript), and Tornado (Python).
Setting Up (Very) Basic Server Side Authentication
To get this started, we need to setup the server side login functions, and also a way to protect API requests so no data goes to anyone that isn’t authenticated. First, let’s add a login function to the api/index.php in the web directory:
// file: api/index.php session_start(); // Add this to the top of the file /** * Quick and dirty login function with hard coded credentials (admin/admin) * This is just an example. Do not use this in a production environment */ function login() { if(!empty($_POST['email']) && !empty($_POST['password'])) { // normally you would load credentials from a database. // This is just an example and is certainly not secure if($_POST['email'] == 'admin' && $_POST['password'] == 'admin') { $user = array("email"=>"admin", "firstName"=>"Clint", "lastName"=>"Berry", "role"=>"user"); $_SESSION['user'] = $user; echo json_encode($user); } else { echo '{"error":{"text":"You shall not pass..."}}'; } } else { echo '{"error":{"text":"Username and Password are required."}}'; } }
This is a very basic login function that is obviously not secure, but will do the job for us, since our focus is really on the backbone side of things. The key thing to note here, is that since we are using backbone, even the login function works as a JSON api request. We don’t generate any HTML, we simply send back JSON data with a user identity, or an error if something went wrong. Now we need to associate this function with a route in Slim, so add the following code under the other defined routes in index.php:
// file: api/index.php // I add the login route as a post, since we will be posting the login form info $app->post('/login', 'login');
Now we also need to make sure no data gets sent to anyone that isn’t authorized. So now we define an authorize function to check that a user has the right permissions to get the data:
// File: api/index.php /** * Authorise function, used as Slim Route Middlewear */ function authorize($role = "user") { return function () use ( $role ) { // Get the Slim framework object $app = Slim::getInstance(); // First, check to see if the user is logged in at all if(!empty($_SESSION['user'])) { // Next, validate the role to make sure they can access the route // We will assume admin role can access everything if($_SESSION['user']['role'] == $role || $_SESSION['user']['role'] == 'admin') { //User is logged in and has the correct permissions... Nice! return true; } else { // If a user is logged in, but doesn't have permissions, return 403 $app->halt(403, 'You shall not pass!'); } } else { // If a user is not logged in at all, return a 401 $app->halt(401, 'You shall not pass!'); } }; }
The authorize function uses some PHP closure Kung Fu, but the key is to return HTTP error codes to backbone. In our case we are going to return a 401 error (unauthorized) if a user is trying to access something they need to be logged in for, and a 403 (forbidden) if the user is logged in but doesn’t have enough privs to get the data he wants.
The last thing we need to do in our server-side code is add the middleware to the routes we want to protect:
// File: api/index.php $app->get('/employees', authorize('user'), 'getEmployees'); $app->get('/employees/:id', authorize('user'),'getEmployee'); $app->get('/employees/:id/reports', authorize('admin'),'getReports'); $app->get('/employees/search/:query', authorize('user'),'getEmployeesByName'); $app->get('/employees/modifiedsince/:timestamp', authorize('user'), 'findByModifiedDate');
Setting Up Backbone Views
Now let’s get to the good stuff: Setting up our backbone views. For authentication we will of course need a login view:
// File: web/js/views/login.js window.LoginView = Backbone.View.extend({ initialize:function () { console.log('Initializing Login View'); }, events: { "click #loginButton": "login" }, render:function () { $(this.el).html(this.template()); return this; }, login:function (event) { event.preventDefault(); // Don't let this button submit the form $('.alert-error').hide(); // Hide any errors on a new submit var url = '../api/login'; console.log('Loggin in... '); var formValues = { email: $('#inputEmail').val(), password: $('#inputPassword').val() }; $.ajax({ url:url, type:'POST', dataType:"json", data: formValues, success:function (data) { console.log(["Login request details: ", data]); if(data.error) { // If there is an error, show the error messages $('.alert-error').text(data.error.text).show(); } else { // If not, send them back to the home page window.location.replace('#'); } } }); } });
This view is pretty straight forward. It renders the login template, and put a click event handler on the login button. The event handler fires the login function when the button is clicked and sends an ajax request to our php login function. If an error comes back, we put it in the error div and show that div.
Here is the login template code:
<!-- File: web/tpl/Login.html --> <h1>Login</h1> <div class="alert alert-error" style="display:none;"> </div> <form class="form-horizontal"> <div class="control-group"> <label class="control-label" for="inputEmail">Email</label> <div class="controls"> <input type="text" id="inputEmail" placeholder="Email"> </div> </div> <div class="control-group"> <label class="control-label" for="inputPassword">Password</label> <div class="controls"> <input type="password" id="inputPassword" placeholder="Password"> </div> </div> <div class="control-group"> <div class="controls"> <button type="submit" class="btn" id="loginButton">Sign in</button> </div> </div> </form>
Telling Backbone How to Handle 401 � 403 Errors (ajaxSetup)
Now here comes the kicker. We need backbone/jquery to catch any requests that return a 401 or 403 error and handle those requests appropriately. The method I have used to do this is to call the jquery function ajaxSetup which allows us to watch for certain status codes and to handle them appropriately.
// File: web/js/main.js // Tell jQuery to watch for any 401 or 403 errors and handle them appropriately $.ajaxSetup({ statusCode: { 401: function(){ // Redirec the to the login page. window.location.replace('/#login'); }, 403: function() { // 403 -- Access denied window.location.replace('/#denied'); } } });
Now all 401s and 403s will be redirected to appropriate place. (I haven’t implemented the “denied” view yet, but you get the idea)
Lastly we update the backbone routing to include the login url and login view:
// File: web/js/main.js window.Router = Backbone.Router.extend({ routes: { "": "home", "contact": "contact", "employees/:id": "employeeDetails", "login" : "login" }, // ... login: function() { $('#content').html(new LoginView().render().el); } }
The Final Word (and source code)
That is it! You should now have a password protected REST API for BackboneJS. I have posted the project to github (here), so feel free to check out the code and see it in action. Currently, you will need PHP/Apache with MySQL setup and the database imported. I am working on a Vagrant file for the project so you will be able to see it in action without setting up your own server.
As always, let me know if you have any questions or suggestions.
Source Code: https://github.com/clintberry/backbone-directory-auth