Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Implement Hawk authorization and status api

  • Loading branch information...
commit 03d0100447126bfd851bffea83221a12a45ee86d 1 parent 47a4c69
Zach Carter zaach authored
30 bin/api
View
@@ -11,22 +11,38 @@ function fatal(msg) {
process.exit(1);
}
-var bindTo = config.process.api;
+function credentialsFunc(id, callback) {
+ db.getAuthKey(id, function (err, key){
+ if (err) return callback(err);
+
+ var credentials = {
+ id: id,
+ key: key,
+ algorithm: 'hmac-sha-256',
+ user: id
+ };
+
+ return callback(null, credentials);
+ });
+}
+
+var apiOptions = config.hapi;
+apiOptions.auth.getCredentialsFunc = credentialsFunc;
+
// Create a server with a host and port
-var server = new Hapi.Server(bindTo.host, bindTo.port, config.hapi);
+var bindTo = config.process.api;
+var server = new Hapi.Server(bindTo.host, bindTo.port, apiOptions);
console.log("api starting up");
-server.on('bound', function(host, port) {
- console.log("running on http://" + host + ":" + port);
-});
-
// now load up api handlers
apiLoader(server, function(err) {
if (err) fatal(err);
db.connect(config.db, function() {
// Start the server
- server.start();
+ server.start(function() {
+ console.log("running on http://" + server.settings.host + ":" + server.settings.port);
+ });
});
});
75 client/client.js
View
@@ -1,12 +1,19 @@
if (typeof GombotCrypto === 'undefined') {
var GombotCrypto = require('./crypto.js');
}
+if (typeof URLParse === 'undefined') {
+ var URLParse = require('./urlparse.js');
+}
+
+(function() {
-;(function() {
+GombotClient = function(path) {
+ var url = URLParse(path);
-GombotClient = function(host, port) {
- this.host = host;
- this.port = port;
+ this.scheme = url.scheme;
+ this.host = url.host;
+ this.port = url.port;
+ this.path = url.path || '';
};
var xhr = typeof jQuery !== 'undefined' ? jQuery.ajax : require('xhrequest');
@@ -21,8 +28,11 @@ function request(args, cb) {
var req = {
url: url,
method: method,
+ type: method,
data: args.data,
- headers: {},
+ //dataType: 'json',
+ //accepts: {json: 'application/json'},
+ headers: args.headers || {},
success: function(data, res, status) {
try {
var body = JSON.parse(data);
@@ -32,16 +42,24 @@ function request(args, cb) {
body.session_context = {};
cb(null, body);
},
+ processData: false,
error: function(data, res, status) {
cb('Error: ' + data + '\nStatus: ' + status);
}
};
if (method == 'PUT' || method == 'POST') {
- req.headers['Content-Type'] = 'application/json';
+ req.contentType = req.headers['Content-Type'] = 'application/json';
}
xhr(url, req);
}
+function mergeArgs(args, def) {
+ args.scheme = def.scheme;
+ args.host = args.host || def.host;
+ args.port = args.port || def.port;
+ return args;
+}
+
GombotClient.prototype = {
// get "session context" from the server
context: function(args, cb) {
@@ -49,28 +67,55 @@ GombotClient.prototype = {
cb = args;
args = {};
}
- args.host = args.host || this.host;
- args.port = args.port || this.port;
+ args = mergeArgs(args, this);
args.method = 'get';
- args.path = '/v1/context';
+ args.path = this.path + '/v1/context';
request(args, cb);
},
account: function(args, cb) {
- args.host = args.host || this.host;
- args.port = args.port || this.port;
- args.method = 'put';
- args.path = '/v1/account';
+ var self = this;
+ args = mergeArgs(args, this);
+ args.method = 'post';
+ args.path = this.path + '/v1/account';
// compute the authKey
- var keys = GombotCrypto.derive({
+ var headers = GombotCrypto.derive({
email: args.email,
- password: args.password
+ password: args.pass
}, function(err, r) {
+ self.authKey = r.authKey;
args.data = JSON.stringify({email: args.email, pass: r.authKey});
// send request with authKey as the password
request(args, cb);
});
+ },
+ status: function(args, cb) {
+ args = mergeArgs(args, this);
+ args.method = 'get';
+ args.path = this.path + '/v1/status';
+
+ var url = args.scheme ? args.scheme : 'http';
+ url += '://' + args.host;
+ if (args.port) url += ':' + args.port;
+ url += args.path;
+
+ // compute the authKey
+ GombotCrypto.sign({
+ email: args.email,
+ key: args.key,
+ url: url,
+ host: args.host,
+ port: args.port,
+ method: args.method,
+ nonce: args.nonce,
+ date: args.date
+ }, function(err, r) {
+ if (err) return cb(err);
+ args.headers = r;
+ // send request with authKey as the password
+ request(args, cb);
+ });
}
};
82 client/crypto.js
View
@@ -7,6 +7,10 @@ if (typeof URLParse === 'undefined') {
var URLParse = require('./urlparse.js');
}
+if (typeof Hawk === 'undefined') {
+ var Hawk = require('hawk');
+}
+
var GombotCrypto = (function() {
// the number of rounds used in PBKDF2 to generate a stretched derived
// key from a user password.
@@ -115,48 +119,56 @@ var GombotCrypto = (function() {
if (typeof cb !== 'function')
throw new Error("missing required callback argument");
- // how about if the key is poorly formated?
- var keyBits = sjcl.codec.base64.toBits(args.key);
-
// normalize method
args.method = args.method.toUpperCase();
- args.url = URLParse(args.url);
- // add a port if default is in use
- if (!args.url.port) {
- args.url.port = (args.url.scheme === 'https' ? '443' : '80');
- }
+ // how about if the key is poorly formated?
+ //var keyBits = sjcl.codec.base64.toBits(args.key);
+
+ var url = URLParse(args.url);
+ //// add a port if default is in use
+ //if (!args.url.port) {
+ //args.url.port = (args.url.scheme === 'https' ? '443' : '80');
+ //}
setTimeout(function() {
// see https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01
// for normalization procedure
- var hmac = new sjcl.misc.hmac(keyBits);
- var body =
- // string representation of seconds since epoch
- args.date.toString() + "\n" +
- // random nonce
- args.nonce + '\n' +
- // normalized method
- args.method + '\n' +
- // path
- args.path + '\n' +
- // hostname
- args.host + '\n' +
- // port
- args.port + '\n' +
- // payload
- args.payload + '\n';
-
- var mac = sjcl.codec.base64.fromBits(hmac.mac(body));
-
- // now formulate the authorization header.
- var val =
- 'MAC id="' + args.email + '", ' +
- 'ts="' + args.date + '", ' +
- 'nonce="' + args.nonce + '", ' +
- 'mac="' + mac + '"';
-
- var headers = { "Authorization": val };
+ //var hmac = new sjcl.misc.hmac(keyBits);
+ //var body =
+ //// string representation of seconds since epoch
+ //args.date.toString() + "\n" +
+ //// random nonce
+ //args.nonce + '\n' +
+ //// normalized method
+ //args.method + '\n' +
+ //// path
+ //args.path + '\n' +
+ //// hostname
+ //args.host + '\n' +
+ //// port
+ //args.port + '\n' +
+ //// payload
+ //args.payload + '\n';
+
+ //var mac = sjcl.codec.base64.fromBits(hmac.mac(body));
+
+ //// now formulate the authorization header.
+ //var val =
+ //'MAC id="' + args.email + '", ' +
+ //'ts="' + args.date + '", ' +
+ //'nonce="' + args.nonce + '", ' +
+ //'mac="' + mac + '"';
+
+ var credentials = {
+ id: args.email,
+ key: args.key,
+ algorithm: 'hmac-sha-256'
+ };
+
+ var headers = {
+ Authorization: Hawk.getAuthorizationHeader(credentials, args.method, args.url, url.host, url.port, args.nonce, args.date)
+ };
// and pass a bag of calculated authorization headers (only one)
// back to the client
5 etc/config.js
View
@@ -21,7 +21,10 @@ var config = module.exports = {
},
hapi: {
name: "Gombot API Server",
- docs: true
+ docs: true,
+ auth: {
+ scheme: 'hawk'
+ }
},
db: {
hosts: [ 'localhost:8091' ],
7 lib/api/v1/account.js
View
@@ -6,10 +6,12 @@ var B = Hapi.Types.Boolean,
S = Hapi.Types.String;
module.exports = {
- method: 'PUT',
+ method: 'POST',
handler: handler,
config: {
- auth: false,
+ auth: {
+ mode: 'none'
+ },
description: 'Stage a new account',
schema: {
email: B(),
@@ -22,7 +24,6 @@ module.exports = {
};
function handler(request) {
- console.log('$$$$$$$$$$');
db.stageAccount(request.payload, function(err) {
if (err) request.reply(Hapi.Error.internal("error staging account"));
request.reply({
4 lib/api/v1/context.js
View
@@ -8,7 +8,9 @@ module.exports = {
method: 'GET',
handler: handler,
config: {
- auth: false,
+ auth: {
+ mode: 'none'
+ },
description: 'Get "context" for subsequent operations',
response: {
server_time: N(),
23 lib/api/v1/status.js
View
@@ -0,0 +1,23 @@
+var Hapi = require('hapi');
+
+var B = Hapi.Types.Boolean;
+
+module.exports = {
+ method: 'GET',
+ handler: handler,
+ auth: {
+ mode: 'hawk'
+ },
+ config: {
+ description: 'Check authorization status',
+ response: {
+ success: B()
+ }
+ }
+};
+
+function handler(request) {
+ request.reply({
+ success: true
+ });
+}
10 lib/db/json.js
View
@@ -7,17 +7,25 @@ module.exports = {
db = {};
cb(null);
}, 0);
+ return this;
},
stageAccount: function(data, cb) {
var account = {
pass: data.pass,
email: data.email,
- staged: true
+ staged: false
+ //staged: true
};
setTimeout(function() {
db[data.email] = account;
cb(null);
}, 0);
return this;
+ },
+ getAuthKey: function(email, cb) {
+ setTimeout(function() {
+ cb(null, db[email].pass);
+ }, 0);
+ return this;
}
};
3  lib/views.js
View
@@ -1,5 +1,6 @@
const fs = require('fs');
const path = require('path');
+const db = require('./db');
// generated from public key during packaging
var appid = 'gbmmgmjoeplelogofbnjpmkmpodpfaif';
@@ -19,7 +20,7 @@ function setup(app) {
});
app.post('/join_alpha', function(req, res) {
- console.log('got new alpha user email:', req.param('email'));
+ console.log('got new alpha user email:', req.params.email);
res.redirect('/download');
});
5 package.json
View
@@ -4,13 +4,14 @@
"version": "0.0.1",
"private": true,
"dependencies": {
- "hapi": "git://github.com/lloyd/hapi#7e6cf0",
+ "hapi": "0.9.2",
"walkdir": "0.0.5",
"express": "3.0.2",
"nunjucks": "0.1.5",
"irc": "0.3.3",
"awsbox": "0.3.3",
- "temp": "0.4.0"
+ "temp": "0.4.0",
+ "hawk": "0.0.6"
},
"optionalDependencies": {
"couchbase": "0.0.4"
4 test/api.v1.account.js
View
@@ -12,7 +12,7 @@ describe('the servers', function() {
should.not.exist(err);
should.exist(r);
servers = r;
- client = new Client(servers.host, servers.port);
+ client = new Client('http://' + servers.host + ':' + servers.port);
done();
});
});
@@ -22,7 +22,7 @@ describe('/api/v1/account', function() {
it('staging should return success', function(done) {
client.account({
email: 'foo',
- password: 'bar'
+ pass: 'bar'
}, function(err, r) {
should.not.exist(err);
should.exist(r);
2  test/api.v1.context.js
View
@@ -12,7 +12,7 @@ describe('the servers', function() {
should.not.exist(err);
should.exist(r);
servers = r;
- client = new Client(servers.host, servers.port);
+ client = new Client('http://' + servers.host + ':' + servers.port);
done();
});
});
62 test/api.v1.status.js
View
@@ -0,0 +1,62 @@
+const
+should = require('should'),
+runner = require('./lib/runner.js'),
+Client = require('../client/client.js');
+
+var servers;
+var client;
+
+var test_user = 'foo';
+var test_pass = 'bar';
+
+describe('the servers', function() {
+ it('should start up', function(done) {
+ runner(function(err, r) {
+ should.not.exist(err);
+ should.exist(r);
+ servers = r;
+ client = new Client('http://' + servers.host + ':' + servers.port);
+ done();
+ });
+ });
+});
+
+function createAccount(email, pass, cb) {
+ client.account({
+ email: email,
+ pass: pass
+ }, function(err, r) {
+ if (err) cb(err);
+ cb(null, client.key);
+ });
+}
+
+describe("/api/v1/status", function() {
+ it ('should pass authorization', function(done) {
+ createAccount(test_user, test_pass, function() {
+ try {
+ client.status({
+ email: test_user,
+ key: client.authKey,
+ nonce: 'oh hai',
+ date: new Date()
+ }, function(err, r) {
+ should.not.exist(err);
+ should.exist(r);
+ done();
+ });
+ } catch (e) {
+ done(e);
+ }
+ });
+ });
+});
+
+describe('the servers', function() {
+ it('should stop', function(done) {
+ servers.stop(function(err) {
+ should.not.exist(err);
+ done();
+ });
+ });
+});
3  test/crypto.sign.js
View
@@ -77,7 +77,8 @@ describe('GumbotCrypto.sign', function() {
should.not.exist(err);
should.exist(rez);
(rez.Authorization).should.be.a('string');
- (rez.Authorization).should.equal('MAC id="bar", ts="1352177818", nonce="one time only please", mac="Zt21WXS7nkwIUdocxbzMBsXKv+0NREsxQ7aBHA9MS4w="');
+ //(rez.Authorization).should.equal('MAC id="bar", ts="1352177818", nonce="one time only please", mac="Zt21WXS7nkwIUdocxbzMBsXKv+0NREsxQ7aBHA9MS4w="');
+ (rez.Authorization).should.equal('Hawk id="bar", ts="1352177818", ext="one time only please", mac="2JSJGewL+/9eoCKgf51mEbhI4cZuEVqNEeZkC3SfXp4="');
done();
});
});
Please sign in to comment.
Something went wrong with that request. Please try again.