Skip to content

Instantly share code, notes, and snippets.

@adoc
Last active January 4, 2016 11:09
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save adoc/8613376 to your computer and use it in GitHub Desktop.
Save adoc/8613376 to your computer and use it in GitHub Desktop.
backbone_auth.js: Provides bi-directional HMAC hooked in to Model and Collection.
/*
backbone_auth.js
Provides bi-directional HMAC hooked in to Model and Collection.
Author: github.com/adoc
Location: https://gist.github.com/adoc/8613376
*/
define(['underscore', 'backbone', 'config', 'events', 'auth_client', 'persist'],
function(_, Backbone, Config, Events, AuthClient, Persist) {
var Store;
//http://stackoverflow.com/a/4994244
var isEmpty = function(obj) {
// null and undefined are "empty"
if (obj == null) return true;
// Assume if it has a length property with a non-zero value
// that that property is correct.
if (obj.length > 0) return false;
if (obj.length === 0) return true;
// Otherwise, does it have any properties of its own?
// Note that this doesn't handle
// toString and toValue enumeration bugs in IE < 9
for (var key in obj) {
if (hasOwnProperty.call(obj, key)) return false;
}
return true;
}
var restAuth;
// Models & Collections
// ====================
// Override 'urlRoot' to return prefix and new `uri`
var EnabledModel = Backbone.Model.extend({
urlRoot: function() { return Config.apiRoot + this.uri; }
});
var EnabledCollection = Backbone.Collection.extend({
url: function() { return Config.apiRoot + this.uri; }
});
var Login = EnabledModel.extend({
uri: '/auth',
validate: function(attrs, options) {
var validation_errors = [];
if (attrs.name.length <= 0) {
validation_errors.push({"field": "name",
"msg": "Must give a value"});
}
if (attrs.name.length > 32) {
validation_errors.push({"field": "name",
"msg": "User name too long. (32 characters)"});
}
if (attrs.pass.length <= 0) {
validation_errors.push({"field": "pass",
"msg": "Must give a value"});
}
if (attrs.pass.length < 3) {
validation_errors.push({"field": "pass",
"msg": "Password too short. (3 characters)"});
}
if (attrs.pass.length > 128) {
validation_errors.push({"field": "pass",
"msg": "Password too long. (128 characters)"});
}
if (validation_errors.length > 0) {
return validation_errors;
}
}
});
// Routes & Views
// ==============
var AuthRequiredRouter = Backbone.Router.extend({
auth_required: function () {
if (!this.not_loggedin) {
throw "AuthRequired based Routers require a `not_loggedin` method.";
}
// Rebind routes.
this.childRoutes = this.routes;
this.routes = {'*path': '_entry'};
this._bindRoutes();
},
_entry: function(path) {
// Hook back in original routes and refresh router.
if (restAuth.authenticated) {
this.routes = this.childRoutes;
delete this.childRoutes;
this._bindRoutes();
this.refresh();
}
else {
this.not_loggedin();
}
},
});
var LoginRouter = function (LoginView) {
return Backbone.Router.extend({
initialize: function () {
this.loginView = new LoginView();
},
routes: {'*path':
function () {
var that = this;
if (restAuth.authenticated) {
this.navhash(); // Go to the hash or root.
}
else {
Events.on('auth.logged_in', function () {
that.refresh();
});
this.loginView.render(); // Render the login view.
}
}
}
});
}
var LogoutRouter = function () {
return Backbone.Router.extend({
routes: {'*path':
function(path) {
Events.trigger('intent.log_out');
Events.trigger('auth.logged_out');
}
}
});
}
// Base Login View. Inherit from this when implementing any login
// view.
var LoginView = Backbone.View.extend({
initialize: function () {
var login = this.login = new Login();
//console.log(login);
Events.on('intent.log_out', function () {
//console.log('attempting auth delete (logout)');
login = new Login();
login.sync('delete', login, {});
this.login = login;
});
},
logout: function() {
Events.trigger('intent.log_out');
Events.trigger('auth.logged_out');
return false;
},
loginEvent: function(ev) {
var that = this;
// Assumes event triggered near a form. (This may be a flaw.)
var form = $(ev.currentTarget).closest('form');
var obj = form.serializeObject();
var userDetails = {name: obj.name, pass: obj.pass};
// var login = new Login();
this.login.on("invalid", this.invalidForm(this, form));
this.login.save(userDetails, {
success: this.loginSuccess,
error: function(xhr, Status) {
if(Status.status==401) {
that.login.trigger('invalid', null , [
{'form': form,
'msg': "User or Password is incorrect."}]);
return false; //??
}
else if (Status.status==403) {
// handle 403.
}
}
});
return false;
},
// Callback to be used upon /auth success. (Where else can this go??)
loginSuccess: function (model, resp, options) {
// remember this is a model, so need to toJSON to
// get the underlying data.
var model = model.toJSON();
delete model.name;
delete model.pass;
var time = model._time;
var addr = model._addr;
delete model._addr;
delete model._time;
restAuth.receive_auth(time, addr, model.remotes);
Store.set('rest_auth', JSON.stringify(restAuth.build_cookies()));
Events.trigger('auth.logged_in');
}
});
var initialize = function(opts) {
opts = opts || {};
Store = new Persist.Store('backbone_auth');
Store.get('rest_auth', function (ok, storageOpts) {
if (ok && storageOpts) {
try {
storageOpts = JSON.parse(storageOpts);
} catch (err) {
storageOpts = {};
Store.set('rest_auth', '{}');
}
//console.log('found options in store.');
//console.log(storageOpts);
var authOpts = _.extend({}, opts.apiDefault || {}, storageOpts);
}
else if (opts.apiDefault) {
console.log('boo. no options in store.');
var authOpts = opts.apiDefault;
}
else {
throw "AuthApi: No cookies set and opts.apiDefault is empty.";
}
// Set up REST Auth.
restAuth = new AuthClient.RestAuth(authOpts);
// Set up our own remotes.
restAuth.remotes = new AuthClient.Remotes(authOpts);
Events.on('intent.log_out', function () {
//Auth.remove_cookies();
Store.set('rest_auth', '{}');
restAuth.logout();
});
try {
// Look for a non '_any' remote first.
restAuth.remotes.get(true);
} catch (err) {
// Upon failure, set to the '_any' remote.
restAuth.remotes.get();
}
});
// Set up ping model.
var Ping = EnabledModel.extend({
uri:'/ping',
});
var ping = new Ping();
// Backbone.sync hook to provide bidirectional HMAC.
var backboneSync = function (method, model, options) {
// Outbound hmac.
if (method === 'delete') {
options.headers = restAuth.send({}); // Delete has no payload.
} else {
var obj = model.toJSON(options);
if (isEmpty(obj)) { // Even [] get's hashed as an empty object.
obj = {};
}
options.headers = restAuth.send(obj);
}
var success = options.success;
// Inbound hmac callback.
options.success = function(model, resp, xhr) {
restAuth.receive(xhr.responseJSON, xhr.getResponseHeader);
if (success) {
success(model, resp, options);
}
}
var error = options.error;
options.error = function(xhr, status, msg) {
if (error) {
error(xhr, status, msg);
}
Events.trigger('auth.sync_error');
}
// Call Backbone native sync function.
Backbone.sync.apply(this, [method, model, options]);
}
// Backbone Monkeypatching
// =======================
Backbone.Model.prototype.sync =
Backbone.Collection.prototype.sync = backboneSync;
// Add "refresh" method to a router.
Backbone.Router.prototype.refresh = function() {
//http://stackoverflow.com/a/8991969
var newFragment = Backbone.history.getFragment($(this).attr('href'));
if (Backbone.history.fragment == newFragment) {
// need to null out Backbone.history.fragement because
// navigate method will ignore when it is the same as newFragment
Backbone.history.fragment = null;
Backbone.history.navigate(newFragment, true);
}
}
Backbone.Router.prototype.navhash = function(fallback) {
var hash = Backbone.history.location.hash;
if (hash) {
window.location = hash.slice(1);
} else {
window.location = fallback || '/';
}
}
Backbone.View.prototype.remove = function() {
this.undelegateEvents();
this.$el.empty();
this.stopListening();
return this;
}
// Main Object
// ===========
var backboneAuth = {
restAuth: restAuth,
tightenAuth: function () {
var that = this;
ping.fetch({
success: function (model) {
var data = model.toJSON();
that.restAuth.receive_auth(data._time, data._addr,
data.remotes);
if (that.restAuth.authenticated) {
Events.trigger('auth.logged_in');
}
else {
Events.trigger('auth.logged_out');
}
Events.trigger('auth.auth_tight');
},
error: function (resp, xhr, msg) {
if (resp.status = 403) {
// We failed on an '_any' remote, removie cookies
// and fail hard.
if (that.restAuth.remotes.current.id === '_any') {
Events.trigger('auth.logged_out');
throw "apiCall: Final auth attempt failed. Nowhere to go from here. :(";
} else {
that.restAuth.logout();
that.tightenAuth(); // Retry
}
}
}
});
},
};
return backboneAuth;
}
return {
initialize: initialize,
LoginView: LoginView,
EnabledModel: EnabledModel,
EnabledCollection: EnabledCollection,
AuthRequiredRouter: AuthRequiredRouter,
LoginRouter: LoginRouter,
LogoutRouter: LogoutRouter
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment