My PHP api requires a user token be submitted with every request from my front-end Backbone app to make sure the user...
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.
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
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.
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
.
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
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);
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!'); });
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.
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 */ )
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With