зеркало из https://github.com/mozilla/gombot.git
Implement Hawk authorization and status api
This commit is contained in:
Родитель
47a4c690c7
Коммит
03d0100447
30
bin/api
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(),
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Загрузка…
Ссылка в новой задаче