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

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

@ -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(host, port) {
this.host = host;
this.port = port;
GombotClient = function(path) {
var url = URLParse(path);
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);
});
}
};

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

@ -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 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));
//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 + '"';
//// now formulate the authorization header.
//var val =
//'MAC id="' + args.email + '", ' +
//'ts="' + args.date + '", ' +
//'nonce="' + args.nonce + '", ' +
//'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)
// back to the client

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

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

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

@ -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({

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

@ -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 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 = {};
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;
}
};

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

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

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

@ -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"

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

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

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

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