Implement create, startLogin, and finishLogin sans SRP
This commit is contained in:
Danny Coates 2013-05-15 17:34:53 -07:00
Родитель 612861fcea 7ccdf1b9cf
Коммит ee14abe54f
7 изменённых файлов: 348 добавлений и 33 удалений

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

@ -0,0 +1,153 @@
const uuid = require('uuid');
const async = require('async');
const Hapi = require('hapi');
const kvstore = require('./kvstore');
const config = require('./config');
const util = require('./util');
var internalError = Hapi.Error.internal;
var badRequest = Hapi.Error.badRequest;
var notFound = Hapi.Error.notFound;
var kv = kvstore.connect(config.get('kvstore'));
/* user account model
*
* user should have account id
*
* <email>/userid = <userId>
*
* <userId>/meta = {
* params: <user params>
* passwordVerifier: <password verifier>
* kA: <kA key>
* kBWrapped: <wrapped kB key>
* }
*
* */
exports.create = function(data, cb) {
// generate user id
var userId = util.getUserId();
var metaKey = userId + '/user';
var kA;
async.waterfall([
// ensure that an account doesn't already exist for the email
function(cb) {
kv.get(data.email + '/uid', function (err, doc) {
if (doc) return cb(badRequest('AccountExistsForEmail'));
cb(null);
});
},
// link email to userid
function(cb) {
kv.set(data.email + '/uid', userId, cb);
},
// get new class A key
util.getKA,
// create user account
function(key, cb) {
kA = key;
kv.set(metaKey, {
params: data.params,
verifier: data.verifier,
kA: key,
kB: data.kB
}, cb);
}
], cb);
};
// Initialize the SRP process
exports.startLogin = function(email, cb) {
async.waterfall([
// get uid
exports.getId.bind(null, email),
// create a temporary session document
function (uid, cb) {
// get sessionID
var sid = util.getSessionId();
// eventually will store SRP state
// and expiration time
kv.set(sid + '/session', {
uid: uid
}, function (err) {
// return sessionID
cb(err, { sessionId: sid });
});
}
], cb);
};
// Finish the SRP process and return account info
exports.finishLogin = function(sessionId, verifier, cb) {
var sessKey = sessionId + '/session';
var uid, user, accountToken;
async.waterfall([
// get session doc
function(cb) {
kv.get(sessKey, function(err, session) {
if (err) return cb(err);
if (!session) return cb(notFound('UnknownSession'));
cb(null, session.value);
});
},
// get user info
function(session, cb) {
uid = session.uid;
exports.getUser(session.uid, cb);
},
// check password
function(result, cb) {
user = result;
if (user.verifier !== verifier) {
return cb(badRequest('IncorrectPassword'));
}
cb(null);
},
// create accountToken
util.getAccountToken,
// create temporary account token doc
function(token, cb) {
accountToken = token;
kv.set(token + '/accountToken', { uid: uid }, cb);
},
// delete session doc
function(cb) {
kv.delete(sessKey, cb);
},
// return info
function(cb) {
cb({
accountToken: accountToken,
kA: user.kA,
kB: user.kB,
});
}
], cb);
};
// This method returns the userId currently associated with an email address.
exports.getId = function(email, cb) {
kv.get(email + '/uid', function(err, result) {
if (err) return cb(internalError(err));
if (!result) return cb(notFound('UnknownUser'));
cb(null, result.value);
});
};
// get meta data associated with a user
exports.getUser = function(userId, cb) {
kv.get(userId + '/user', function(err, doc) {
if (err) return cb(internalError(err));
if (!doc) return cb(notFound('UnknownUser'));
cb(null, doc.value);
});
};

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

@ -7,7 +7,7 @@
*
*/
const Hapi = require('hapi');
const hoek = require('hoek');
const kvstore = require('../kvstore');
@ -15,7 +15,7 @@ const kvstore = require('../kvstore');
// Wrap it so that other datatypes are returned unchanged.
function clone(value) {
if (typeof value !== 'object') return value;
return Hapi.Utils.clone(value);
return hoek.clone(value);
}
// This is the in-memory store for the data, shared across all connections.

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

@ -0,0 +1,36 @@
var crypto = require('crypto');
var uuid = require('uuid');
function getKA(cb) {
return crypto.randomBytes(32, function(err, buf) {
cb(null, buf.toString('base64'));
});
}
function getDeviceId(cb) {
return crypto.randomBytes(32, function(err, buf) {
cb(null, buf.toString('hex'));
});
}
function getAccountToken(cb) {
return crypto.randomBytes(32, function(err, buf) {
cb(null, buf.toString('hex'));
});
}
function getUserId() {
return uuid.v4();
}
function getSessionId() {
return uuid.v4();
}
module.exports = {
getKA: getKA,
getDeviceId: getDeviceId,
getUserId: getUserId,
getSessionId: getSessionId,
getAccountToken: getAccountToken
};

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

@ -22,7 +22,9 @@
"jwcrypto": "0.4.3",
"handlebars": "1.0.10",
"convict": "0.1.0",
"hoek": "0.8.5"
"hoek": "0.8.5",
"uuid": "1.4.1",
"async": "0.2.8"
},
"devDependencies": {
"awsbox": "0.4.x",

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

@ -9,7 +9,8 @@ const config = require('../lib/config');
const hour = 1000 * 60 * 60;
var cc = new CC({ module: __dirname + '/sign.js' });
var kv = require('../lib/kvstore').connect();
var account = require('../lib/account');
var routes = [
{
@ -46,7 +47,7 @@ var routes = [
payload: {
email: Hapi.types.String().email().required(),
verifier: Hapi.types.String().required(),
params: Hapi.types.String(),
params: Hapi.types.Object(),
kB: Hapi.types.String()
}
}
@ -69,9 +70,9 @@ var routes = [
},
{
method: 'POST',
path: '/beginLogin',
path: '/startLogin',
config: {
handler: beginLogin,
handler: startLogin,
validate: {
payload: {
email: Hapi.types.String().email().required()
@ -103,11 +104,11 @@ function wellKnown(request) {
}
function create(request) {
kv.get(
request.payload.email,
account.create(
request.payload,
function (err, record) {
if (err) {
request.reply(Hapi.error.internal('Database errror', err));
request.reply(err);
}
else if (record) {
request.reply('ok');
@ -135,34 +136,37 @@ function sign(request) {
);
}
function beginLogin(request) {
kv.get(
function startLogin(request) {
account.startLogin(
request.payload.email,
function (err, record) {
function (err, result) {
if (err) {
request.reply(Hapi.error.internal('Unable to get email', err));
}
else if (!record) {
request.reply(Hapi.error.notFound('Unknown email'));
request.reply(err);
}
else {
var token = 'TODO';
request.reply({ sessionId: token });
request.reply(result);
}
}
);
}
function finishLogin(request) {
// TODO lookup sessionId, verify email/password
var accountToken = 'TODO';
var kA = 'TODO';
var kB = 'TODO';
request.reply({
accountToken: accountToken,
kA: kA,
kB: kB
});
account.finishLogin(
request.payload.sessionId,
request.payload.password,
function (err, result) {
if (err) {
request.reply(err);
}
else {
request.reply(result);
}
}
);
}
module.exports = {

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

@ -23,11 +23,15 @@ var server = Hapi.createServer(bind.host, bind.port, settings);
server.addRoutes(routes);
server.ext(
'onPreResponse',
function (request, next) {
request.response().header("Strict-Transport-Security", "max-age=10886400");
next();
}
'onPreResponse',
function (request, next) {
var res = request.response();
// error responses don't have `header`
if (res.header) {
res.header("Strict-Transport-Security", "max-age=10886400");
}
next();
}
);
//TODO throttle extension

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

@ -0,0 +1,116 @@
var assert = require('assert');
//var config = require('../../lib/config');
var helpers = require('../helpers');
var testClient = new helpers.TestClient();
var TEST_EMAIL = 'foo@example.com';
var TEST_PASSWORD = 'foo';
var TEST_KB = 'secret!';
describe('user', function() {
var sessionId;
it('should create a new account', function(done) {
testClient.makeRequest('POST', '/create', {
payload: {
email: TEST_EMAIL,
verifier: TEST_PASSWORD,
params: { foo: 'bar' },
kB: TEST_KB
}
}, function(res) {
try {
assert.equal(res.statusCode, 200);
assert.equal(res.result, 'ok');
} catch (e) {
return done(e);
}
done();
});
});
it('should fail to login with an unknown email', function(done) {
testClient.makeRequest('POST', '/startLogin', {
payload: { email: 'bad@emai.l' }
}, function(res) {
try {
assert.equal(res.statusCode, 404);
assert.equal(res.result.message, 'UnknownUser');
} catch (e) {
return done(e);
}
done();
});
});
it('should begin login', function(done) {
testClient.makeRequest('POST', '/startLogin', {
payload: { email: TEST_EMAIL }
}, function(res) {
sessionId = res.result.sessionId;
try {
assert.ok(res.result.sessionId);
} catch (e) {
return done(e);
}
done();
});
});
it('should fail to login with a bad password', function(done) {
testClient.makeRequest('POST', '/finishLogin', {
payload: {
sessionId: sessionId,
password: 'bad pass'
}
}, function(res) {
try {
assert.equal(res.statusCode, 400);
assert.equal(res.result.message, 'IncorrectPassword');
} catch (e) {
return done(e);
}
done();
});
});
it('should fail to login with an unknown sessionId', function(done) {
testClient.makeRequest('POST', '/finishLogin', {
payload: {
sessionId: 'bad sessionid',
password: TEST_PASSWORD
}
}, function(res) {
try {
assert.equal(res.statusCode, 404);
assert.equal(res.result.message, 'UnknownSession');
} catch (e) {
return done(e);
}
done();
});
});
it('should finish login', function(done) {
testClient.makeRequest('POST', '/finishLogin', {
payload: {
sessionId: sessionId,
password: TEST_PASSWORD
}
}, function(res) {
try {
assert.ok(res.result.accountToken);
assert.ok(res.result.kA);
assert.equal(res.result.kB, TEST_KB);
} catch (e) {
return done(e);
}
done();
});
});
});