Skip to content

Instantly share code, notes, and snippets.

@adoc
Last active January 4, 2016 11:09
Show Gist options
  • Save adoc/8613025 to your computer and use it in GitHub Desktop.
Save adoc/8613025 to your computer and use it in GitHub Desktop.
auth_client.js: REST API Authentication Client using HMAC.
/*
auth_client.js
REST API Authentication using HMAC.
Author: github.com/adoc
Location: https://gist.github.com/adoc/8613025
Python server counterpart:
[py-rest-auth](https://github.com/adoc/py-rest-auth)
Dependents:
[backbone-rest-auth](https://github.com/adoc/backbone-rest-auth)
Requires:
[underscore](http://underscorejs.org/)
[crypto-js](https://code.google.com/p/crypto-js/)
[hmac](https://gist.github.com/adoc/8611494)
TODO:
Could use docs.
*/
(function(window) {
// http://andrewdupont.net/2009/08/28/deep-extending-objects-in-javascript/
var deepExtend = function(destination, source) {
for (var property in source) {
if (source[property] && source[property].constructor &&
source[property].constructor === Object) {
destination[property] = destination[property] || {};
arguments.callee(destination[property], source[property]);
} else {
destination[property] = source[property];
}
}
return destination;
};
function randomByteArray(n, byteArray) {
byteArray = byteArray || [];
for (i=0; i<n; i++) {
byteArray.push(rng_get_byte());
}
return byteArray;
}
// Just a little data structure for a "Remote".
var Remote = function(id, opts) {
opts = opts || {};
var _Remote = {
id: id,
senderId: 'guest',
senderIp: '',
timeProvider: new TimeProvider(),
_tight: false
};
return deepExtend(_Remote, opts);
}
var Remotes = function(opts) {
opts = opts || {};
var defaults = {
current: null,
remotes: {},
_remotes: {}
};
var _Remotes = {
// update private ._remotes with any new ones added to .remotes.
// Fires often.
_update_remotes: function () {
var that = this;
_.each(this.remotes, function(obj, id) {
var remote = new Remote(id, obj);
var obj = {};
obj[id] = remote;
that._remotes = deepExtend(that._remotes, obj);
});
this.remotes = {};
},
// Picks first available remote.
_delegation: function(not_any) {
this._update_remotes();
var currentRemote;
_.each(this._remotes, function(value, id) {
//console.log(id);
if ((not_any === true && id !== '_any') ||
(not_any !== true)) {
currentRemote = value;
return false;
}
});
if (currentRemote) {
this.current = currentRemote;
}
else {
throw "Remotes: No valid remote found.";
}
return currentRemote;
},
//
_contains: function(id) {
this._update_remotes();
return this._remotes.hasOwnProperty(id);
},
//
_check: function(id) {
if (!this._contains(id)) {
throw "Remotes: id "+id+" is not a valid client.";
}
},
//
set_any: function () {
this._update_remotes();
this.current = this._remotes._any;
},
// Removes all remotes. Including '_any' if arg set to true.
remove_all: function(include_any) {
var that = this;
this._update_remotes();
_.each(this._remotes, function(val, id){
if ((include_any === true && id === '_any') ||
(include_any !== true && id !== '_any')) {
delete that._remotes[id];
}
});
},
// Removes a remote by id. (may not even be needed.)
remove: function(id) {
this._check(id);
delete this._remotes[id];
},
//
get: function() {
return this._delegation.apply(this, arguments);
},
};
return _.extend({}, _Remotes, defaults, opts);
}
var Auth = function(opts) {
opts = opts || {};
var defaults = {
//currentRemote: '_any',
//senderId: 'guest',
//senderIp: '',
remotes: new Remotes(),
//tight: false,
looseExpiry: 600,
tightExpiry: 15,
//timeProvider: new TimeProvider()
};
var _Auth = {
//
send: function (payload) {
payload = payload || {};
var that = this;
// Where get not_any? Something outside has to dictate this!
var remote = this.remotes.current;
//var secret = this.remotes.get(this.currentRemote);
var nonce = new CryptoJS.lib.WordArray.init(
bytesToWords(
randomByteArray(16)));
nonce = nonce.toString(CryptoJS.enc.Base64);
//console.log(remote.timeProvider);
console.log(remote.id, remote.secret);
var hmac = new Hmac({secret: remote.secret,
timeProvider: remote.timeProvider});
payload = JSON.stringify(payload);
if (remote._tight === true) {
console.log('send tight');
var signature = hmac.sign(payload, remote.senderId, nonce,
remote.senderIp);
}
else {
console.log('send loose');
var signature = hmac.sign(payload, remote.senderId, nonce);
}
return [nonce, signature];
},
//
receive: function (sender_id, nonce, challenge, payload) {
var remote = this.remotes.current;
// Try to get secret from remotes store.
if (remote.id !== '_any' && sender_id !== remote.id) {
throw "Auth.receive: Current remote is not the sender.";
}
if (remote._tight === true) {
var expiry = this.tightExpiry;
}
else {
var expiry = this.looseExpiry;
}
var hmac = new Hmac({secret: remote.secret,
expiry: expiry,
timeProvider: remote.timeProvider});
payload = JSON.stringify(payload);
hmac.verify(challenge, payload, sender_id, nonce);
}
}
var Auth = _.extend({}, _Auth, defaults, opts);
Auth.Auth = _Auth;
return Auth;
}
var JsonAuth = function(opts) {
opts = opts || {};
var _JsonAuth = Auth({
send: function(payload) {
var pack = this.Auth.send.call(this, payload);
return {
nonce: pack[0],
signature: pack[1],
sender_id: this.remotes.current.senderId,
payload: payload
};
},
receive: function(package) {
return this.Auth.receive.call(this, package.sender_id,
package.nonce, package.signature, package.payload);
}
});
var JsonAuth = _.extend({}, _JsonAuth, opts);
JsonAuth.JsonAuth = _JsonAuth;
return JsonAuth;
}
var RestAuth = function(opts) {
opts = opts || {};
var defaults = {
authenticated: false
};
var _RestAuth = JsonAuth({
//
send: function (payload) {
var packet = this.JsonAuth.send.call(this, payload);
var headers = {};
headers['X-Restauth-Signature'] = packet.signature;
headers['X-Restauth-Signature-Nonce'] = packet.nonce;
if (this.remotes.current._tight) {
var sender = '*';
} else {
var sender = '';
}
headers['X-Restauth-Sender-Id'] = sender + packet.sender_id;
return headers;
},
//
receive: function (payload, headers) {
var packet = {};
packet.signature = headers('X-Restauth-Signature');
packet.nonce = headers('X-Restauth-Signature-Nonce');
packet.sender_id = headers('X-Restauth-Sender-Id');
packet.payload = payload;
return this.JsonAuth.receive.call(this, packet);
},
//
receive_auth: function (time, addr, remotes) {
if (remotes) {
_.extend(this.remotes.remotes, remotes);
this.remotes.get(true);
this.authenticated = true;
}
this.remotes.current.timeProvider.reset(time);
this.remotes.current.senderIp = addr;
this.remotes.current._tight = Boolean(time && addr);
},
logout: function () {
this.authenticated = false;
this.remotes.remove_all();
this.remotes.set_any();
this.remotes.current.timeProvider.reset();
},
build_cookies: function () {
return {
remotes: this.remotes._remotes
}
}
});
var RestAuth = _.extend({}, _RestAuth, defaults, opts);
RestAuth.RestAuth = _RestAuth;
return RestAuth;
}
// Set global object.
window.Remotes = Remotes;
window.Auth = Auth;
window.JsonAuth = JsonAuth;
window.RestAuth = RestAuth;
// AMD Hook
if (typeof define === "function" && define.amd) {
define( "auth_client", ['underscore', 'hmac'], function () {
return {RestAuth: RestAuth,
JsonAuth: JsonAuth,
Remotes: Remotes,
Auth: Auth};
});
}
}).call(window, this);
function Auth_tests(){
s = new Auth({
senderId: 'server1',
remotes:
new Remotes({
client1: "12345"
})
});
c = new Auth({
senderId: 'client1',
remotes:
new Remotes({
server1: "12345"
})
});
var pack = s.send('client1', 'hi');
var nonce = pack[0];
var sig = pack[1];
c.receive('server1', nonce, sig, 'hi');
}
function HttpAuth_tests() {
s = new JsonAuth({
senderId: 'server1',
remotes:
new Remotes({
client1: "12345"
})
});
c = new JsonAuth({
senderId: 'client1',
remotes:
new Remotes({
server1: "12345"
})
});
var headers = JSON.stringify(s.send('client1', {mine: 'foo'}));
c.receive(headers);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment