Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Send token with every backbone sync request

My PHP api requires a user token be submitted with every request from my front-end Backbone app to make sure the user...

  1. Is active
  2. Has permissions to access the resource

What is the easiest way to set this up in Backbone? I am guessing the only way is to overwrite Backbone.sync, but what would the code look like? CoffeeScript preferred.

EDIT

Two more things
1. I would like to redirect the user to /login if I get a 403: Access Forbidden Error
2. I pull the user model which includes the token from localStorage when the app is bootstrapped
3. I have a baseModel and baseCollection which all models / collections come from

like image 829
imrane Avatar asked Jan 07 '13 20:01

imrane


3 Answers

Authentication is a responsibility of the app.

For a Backbone app, the auth logic should be within the Backbone code and changing the global jQuery's ajax behavior should be avoided at all cost.

Cons of ajaxSetup or ajaxSend

From the jQuery doc on ajaxSetup:

Note: The settings specified here will affect all calls to $.ajax or Ajax-based derivatives such as $.get(). This can cause undesirable behavior since other callers (for example, plugins) may be expecting the normal default settings. For that reason we strongly recommend against using this API. Instead, set the options explicitly in the call or define a simple plugin to do so.

ajaxSend has the same problem as mentioned above. The only advantage it has over ajaxSetup is calling a function each time, giving you more flexibility than the object based options passed to ajaxSetup.

The safest way, the AuthModel and AuthCollection

Put the authentication logic into a base model and collection. This is the most scoped solution.

Here, you could use your already existing BaseModel, but I'd still favor separating the BaseModel from the AuthModel as you may want to create a custom model which uses your base model but also uses a different external API for example.

Since the new sync function for the model and the collection are similar but both may have a different parent implementation, I made a simple function generator.

/**
 * Generates a new sync function which adds the token to the request header 
 * and handles a redirect on error.
 * @param  {Function} syncFn the parent `sync` function to call.
 * @return {Function}  a new version of sync which implements the auth logic.
 */
var authSyncFunction = function(syncFn) {
    return function(method, model, options) {
        options = options || {};

        var beforeSend = options.beforeSend,
            error = options.error;

        _.extend(options, {
            // Add auth headers
            beforeSend: function(xhr) {
                xhr.setRequestHeader('Authorization', "Bearer " + yourTokenHere);
                if (beforeSend) return beforeSend.apply(this, arguments);
            },

            // handle unauthorized error (401)
            error: function(xhr, textStatus, errorThrown) {
                if (error) error.call(options.context, xhr, textStatus, errorThrown);
                if (xhr.status === 401) {
                    Backbone.history.navigate('login');
                }
            }
        });

        return syncFn.call(this, method, model, options);
    };
};

Use the generator on both a model and a collection.

var AuthModel = BaseModel.extend({
    sync: authSyncFunction(BaseModel.prototype.sync)
});

var AuthCollection = BaseCollection.extend({
    sync: authSyncFunction(BaseCollection.prototype.sync)
});

Then you're ready to use these on models and collection you're sure will need authentication. Since you were already using a base model and collection, it would be just a matter of changing the BaseModel.extend to AuthModel.extend.

While I know you asked for a redirect on a 403 Forbidden response, I think it should be on a 401 Unauthorized. See 403 Forbidden vs 401 Unauthorized HTTP responses

Overriding Backbone's sync

If you don't feel like changing all models and collections at this point, but still want to follow good practices and avoid changing the global ajax setup, overriding the Backbone.sync function is an easy alternative.

Using our previously defined sync generator:

Backbone.sync = authSyncFunction(Backbone.sync);

Managing the local storage and the authentication

To manage the data in the local storage, check Backbone-session.

It's a nice implementation of a Backbone model which syncs with the local storage instead of a REST API. It also provides a nice interface to manage the authentication.

// Extend from Session to implement your API's behaviour
var Account = Session.extend({
  signIn: function () {},
  signOut: function () {},
  getAuthStatus: function () {}
});

// Using the custom Account implementation
var session = new Account();
session.fetch()
  .then(session.getAuthStatus)
  .then(function () {
    console.log('Logged in as %s', session.get('name'));
  })
  .fail(function () {
    console.log('Not yet logged in!');
  });
like image 196
Emile Bergeron Avatar answered Oct 09 '22 03:10

Emile Bergeron


Backbone uses jQuery's $.ajax, so you can use $.ajaxSetup to "set default values for future Ajax requests":

$.ajaxSetup({
   headers: {
     "accept": "application/json",
     "token": YOUR_TOKEN
   }
});

Update: an improvement to this idea (thanks to @Glen) is to use $.ajaxSend to check for the existence of a token each time before setting it in the headers of the request:

$(document).ajaxSend(function(event, request) {
   var token = App.getAuthToken();
   if (token) {
      request.setRequestHeader("token", token);
   }
});

Where App.getAuthToken() is a function in your Backbone app.

like image 19
jackocnr Avatar answered Nov 10 '22 00:11

jackocnr


You could do this:

var _sync = Backbone.sync;
Backbone.sync = function(method, model, options) {

    if( model && (method === 'create' || method === 'update' || method === 'patch') ) {
        options.contentType = 'application/json';
        options.data = JSON.stringify(options.attrs || model.toJSON());
    }

    _.extend( options.data, {
        "access_token": "some-token"
    });

    return _sync.call( this, method, model, options );
}

And just listen for the fail event of fetch/save method to redirect a user to /login

model.fetch().fail( /* redirect */ )
like image 11
Simon Boudrias Avatar answered Nov 10 '22 00:11

Simon Boudrias