зеркало из https://github.com/mozilla/fxa.git
Merge pull request mozilla/fxa-auth-server#4 from mozilla/accounts
Implement create, startLogin, and finishLogin sans SRP
This commit is contained in:
Коммит
ee14abe54f
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
Загрузка…
Ссылка в новой задаче