Last active
January 4, 2016 11:09
-
-
Save adoc/8613025 to your computer and use it in GitHub Desktop.
auth_client.js: REST API Authentication Client using HMAC.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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