Implement Hawk authorization and status api

This commit is contained in:
Zachary Carter 2012-12-06 09:34:58 -08:00
Родитель 47a4c690c7
Коммит 03d0100447
14 изменённых файлов: 242 добавлений и 67 удалений

30
bin/api
Просмотреть файл

@ -11,22 +11,38 @@ function fatal(msg) {
process.exit(1); 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 // 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"); console.log("api starting up");
server.on('bound', function(host, port) {
console.log("running on http://" + host + ":" + port);
});
// now load up api handlers // now load up api handlers
apiLoader(server, function(err) { apiLoader(server, function(err) {
if (err) fatal(err); if (err) fatal(err);
db.connect(config.db, function() { db.connect(config.db, function() {
// Start the server // Start the server
server.start(); server.start(function() {
console.log("running on http://" + server.settings.host + ":" + server.settings.port);
});
}); });
}); });

Просмотреть файл

@ -1,12 +1,19 @@
if (typeof GombotCrypto === 'undefined') { if (typeof GombotCrypto === 'undefined') {
var GombotCrypto = require('./crypto.js'); var GombotCrypto = require('./crypto.js');
} }
if (typeof URLParse === 'undefined') {
var URLParse = require('./urlparse.js');
}
;(function() { (function() {
GombotClient = function(host, port) { GombotClient = function(path) {
this.host = host; var url = URLParse(path);
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'); var xhr = typeof jQuery !== 'undefined' ? jQuery.ajax : require('xhrequest');
@ -21,8 +28,11 @@ function request(args, cb) {
var req = { var req = {
url: url, url: url,
method: method, method: method,
type: method,
data: args.data, data: args.data,
headers: {}, //dataType: 'json',
//accepts: {json: 'application/json'},
headers: args.headers || {},
success: function(data, res, status) { success: function(data, res, status) {
try { try {
var body = JSON.parse(data); var body = JSON.parse(data);
@ -32,16 +42,24 @@ function request(args, cb) {
body.session_context = {}; body.session_context = {};
cb(null, body); cb(null, body);
}, },
processData: false,
error: function(data, res, status) { error: function(data, res, status) {
cb('Error: ' + data + '\nStatus: ' + status); cb('Error: ' + data + '\nStatus: ' + status);
} }
}; };
if (method == 'PUT' || method == 'POST') { if (method == 'PUT' || method == 'POST') {
req.headers['Content-Type'] = 'application/json'; req.contentType = req.headers['Content-Type'] = 'application/json';
} }
xhr(url, req); 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 = { GombotClient.prototype = {
// get "session context" from the server // get "session context" from the server
context: function(args, cb) { context: function(args, cb) {
@ -49,28 +67,55 @@ GombotClient.prototype = {
cb = args; cb = args;
args = {}; args = {};
} }
args.host = args.host || this.host; args = mergeArgs(args, this);
args.port = args.port || this.port;
args.method = 'get'; args.method = 'get';
args.path = '/v1/context'; args.path = this.path + '/v1/context';
request(args, cb); request(args, cb);
}, },
account: function(args, cb) { account: function(args, cb) {
args.host = args.host || this.host; var self = this;
args.port = args.port || this.port; args = mergeArgs(args, this);
args.method = 'put'; args.method = 'post';
args.path = '/v1/account'; args.path = this.path + '/v1/account';
// compute the authKey // compute the authKey
var keys = GombotCrypto.derive({ var headers = GombotCrypto.derive({
email: args.email, email: args.email,
password: args.password password: args.pass
}, function(err, r) { }, function(err, r) {
self.authKey = r.authKey;
args.data = JSON.stringify({email: args.email, pass: r.authKey}); args.data = JSON.stringify({email: args.email, pass: r.authKey});
// send request with authKey as the password // send request with authKey as the password
request(args, cb); 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);
});
} }
}; };

Просмотреть файл

@ -7,6 +7,10 @@ if (typeof URLParse === 'undefined') {
var URLParse = require('./urlparse.js'); var URLParse = require('./urlparse.js');
} }
if (typeof Hawk === 'undefined') {
var Hawk = require('hawk');
}
var GombotCrypto = (function() { var GombotCrypto = (function() {
// the number of rounds used in PBKDF2 to generate a stretched derived // the number of rounds used in PBKDF2 to generate a stretched derived
// key from a user password. // key from a user password.
@ -115,48 +119,56 @@ var GombotCrypto = (function() {
if (typeof cb !== 'function') if (typeof cb !== 'function')
throw new Error("missing required callback argument"); 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 // normalize method
args.method = args.method.toUpperCase(); args.method = args.method.toUpperCase();
args.url = URLParse(args.url); // how about if the key is poorly formated?
// add a port if default is in use //var keyBits = sjcl.codec.base64.toBits(args.key);
if (!args.url.port) {
args.url.port = (args.url.scheme === 'https' ? '443' : '80'); 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() { setTimeout(function() {
// see https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01 // see https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01
// for normalization procedure // for normalization procedure
var hmac = new sjcl.misc.hmac(keyBits); //var hmac = new sjcl.misc.hmac(keyBits);
var body = //var body =
// string representation of seconds since epoch //// string representation of seconds since epoch
args.date.toString() + "\n" + //args.date.toString() + "\n" +
// random nonce //// random nonce
args.nonce + '\n' + //args.nonce + '\n' +
// normalized method //// normalized method
args.method + '\n' + //args.method + '\n' +
// path //// path
args.path + '\n' + //args.path + '\n' +
// hostname //// hostname
args.host + '\n' + //args.host + '\n' +
// port //// port
args.port + '\n' + //args.port + '\n' +
// payload //// payload
args.payload + '\n'; //args.payload + '\n';
var mac = sjcl.codec.base64.fromBits(hmac.mac(body)); //var mac = sjcl.codec.base64.fromBits(hmac.mac(body));
// now formulate the authorization header. //// now formulate the authorization header.
var val = //var val =
'MAC id="' + args.email + '", ' + //'MAC id="' + args.email + '", ' +
'ts="' + args.date + '", ' + //'ts="' + args.date + '", ' +
'nonce="' + args.nonce + '", ' + //'nonce="' + args.nonce + '", ' +
'mac="' + mac + '"'; //'mac="' + mac + '"';
var headers = { "Authorization": val }; 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) // and pass a bag of calculated authorization headers (only one)
// back to the client // back to the client

Просмотреть файл

@ -21,7 +21,10 @@ var config = module.exports = {
}, },
hapi: { hapi: {
name: "Gombot API Server", name: "Gombot API Server",
docs: true docs: true,
auth: {
scheme: 'hawk'
}
}, },
db: { db: {
hosts: [ 'localhost:8091' ], hosts: [ 'localhost:8091' ],

Просмотреть файл

@ -6,10 +6,12 @@ var B = Hapi.Types.Boolean,
S = Hapi.Types.String; S = Hapi.Types.String;
module.exports = { module.exports = {
method: 'PUT', method: 'POST',
handler: handler, handler: handler,
config: { config: {
auth: false, auth: {
mode: 'none'
},
description: 'Stage a new account', description: 'Stage a new account',
schema: { schema: {
email: B(), email: B(),
@ -22,7 +24,6 @@ module.exports = {
}; };
function handler(request) { function handler(request) {
console.log('$$$$$$$$$$');
db.stageAccount(request.payload, function(err) { db.stageAccount(request.payload, function(err) {
if (err) request.reply(Hapi.Error.internal("error staging account")); if (err) request.reply(Hapi.Error.internal("error staging account"));
request.reply({ request.reply({

Просмотреть файл

@ -8,7 +8,9 @@ module.exports = {
method: 'GET', method: 'GET',
handler: handler, handler: handler,
config: { config: {
auth: false, auth: {
mode: 'none'
},
description: 'Get "context" for subsequent operations', description: 'Get "context" for subsequent operations',
response: { response: {
server_time: N(), server_time: N(),

23
lib/api/v1/status.js Normal file
Просмотреть файл

@ -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
});
}

Просмотреть файл

@ -7,17 +7,25 @@ module.exports = {
db = {}; db = {};
cb(null); cb(null);
}, 0); }, 0);
return this;
}, },
stageAccount: function(data, cb) { stageAccount: function(data, cb) {
var account = { var account = {
pass: data.pass, pass: data.pass,
email: data.email, email: data.email,
staged: true staged: false
//staged: true
}; };
setTimeout(function() { setTimeout(function() {
db[data.email] = account; db[data.email] = account;
cb(null); cb(null);
}, 0); }, 0);
return this; return this;
},
getAuthKey: function(email, cb) {
setTimeout(function() {
cb(null, db[email].pass);
}, 0);
return this;
} }
}; };

Просмотреть файл

@ -1,5 +1,6 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const db = require('./db');
// generated from public key during packaging // generated from public key during packaging
var appid = 'gbmmgmjoeplelogofbnjpmkmpodpfaif'; var appid = 'gbmmgmjoeplelogofbnjpmkmpodpfaif';
@ -19,7 +20,7 @@ function setup(app) {
}); });
app.post('/join_alpha', function(req, res) { 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'); res.redirect('/download');
}); });

Просмотреть файл

@ -4,13 +4,14 @@
"version": "0.0.1", "version": "0.0.1",
"private": true, "private": true,
"dependencies": { "dependencies": {
"hapi": "git://github.com/lloyd/hapi#7e6cf0", "hapi": "0.9.2",
"walkdir": "0.0.5", "walkdir": "0.0.5",
"express": "3.0.2", "express": "3.0.2",
"nunjucks": "0.1.5", "nunjucks": "0.1.5",
"irc": "0.3.3", "irc": "0.3.3",
"awsbox": "0.3.3", "awsbox": "0.3.3",
"temp": "0.4.0" "temp": "0.4.0",
"hawk": "0.0.6"
}, },
"optionalDependencies": { "optionalDependencies": {
"couchbase": "0.0.4" "couchbase": "0.0.4"

Просмотреть файл

@ -12,7 +12,7 @@ describe('the servers', function() {
should.not.exist(err); should.not.exist(err);
should.exist(r); should.exist(r);
servers = r; servers = r;
client = new Client(servers.host, servers.port); client = new Client('http://' + servers.host + ':' + servers.port);
done(); done();
}); });
}); });
@ -22,7 +22,7 @@ describe('/api/v1/account', function() {
it('staging should return success', function(done) { it('staging should return success', function(done) {
client.account({ client.account({
email: 'foo', email: 'foo',
password: 'bar' pass: 'bar'
}, function(err, r) { }, function(err, r) {
should.not.exist(err); should.not.exist(err);
should.exist(r); should.exist(r);

Просмотреть файл

@ -12,7 +12,7 @@ describe('the servers', function() {
should.not.exist(err); should.not.exist(err);
should.exist(r); should.exist(r);
servers = r; servers = r;
client = new Client(servers.host, servers.port); client = new Client('http://' + servers.host + ':' + servers.port);
done(); done();
}); });
}); });

62
test/api.v1.status.js Normal file
Просмотреть файл

@ -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();
});
});
});

Просмотреть файл

@ -77,7 +77,8 @@ describe('GumbotCrypto.sign', function() {
should.not.exist(err); should.not.exist(err);
should.exist(rez); should.exist(rez);
(rez.Authorization).should.be.a('string'); (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(); done();
}); });
}); });