зеркало из 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');
|
const kvstore = require('../kvstore');
|
||||||
|
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ const kvstore = require('../kvstore');
|
||||||
// Wrap it so that other datatypes are returned unchanged.
|
// Wrap it so that other datatypes are returned unchanged.
|
||||||
function clone(value) {
|
function clone(value) {
|
||||||
if (typeof value !== 'object') return 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.
|
// 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",
|
"jwcrypto": "0.4.3",
|
||||||
"handlebars": "1.0.10",
|
"handlebars": "1.0.10",
|
||||||
"convict": "0.1.0",
|
"convict": "0.1.0",
|
||||||
"hoek": "0.8.5"
|
"hoek": "0.8.5",
|
||||||
|
"uuid": "1.4.1",
|
||||||
|
"async": "0.2.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"awsbox": "0.4.x",
|
"awsbox": "0.4.x",
|
||||||
|
|
|
@ -9,7 +9,8 @@ const config = require('../lib/config');
|
||||||
const hour = 1000 * 60 * 60;
|
const hour = 1000 * 60 * 60;
|
||||||
|
|
||||||
var cc = new CC({ module: __dirname + '/sign.js' });
|
var cc = new CC({ module: __dirname + '/sign.js' });
|
||||||
var kv = require('../lib/kvstore').connect();
|
|
||||||
|
var account = require('../lib/account');
|
||||||
|
|
||||||
var routes = [
|
var routes = [
|
||||||
{
|
{
|
||||||
|
@ -46,7 +47,7 @@ var routes = [
|
||||||
payload: {
|
payload: {
|
||||||
email: Hapi.types.String().email().required(),
|
email: Hapi.types.String().email().required(),
|
||||||
verifier: Hapi.types.String().required(),
|
verifier: Hapi.types.String().required(),
|
||||||
params: Hapi.types.String(),
|
params: Hapi.types.Object(),
|
||||||
kB: Hapi.types.String()
|
kB: Hapi.types.String()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -69,9 +70,9 @@ var routes = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/beginLogin',
|
path: '/startLogin',
|
||||||
config: {
|
config: {
|
||||||
handler: beginLogin,
|
handler: startLogin,
|
||||||
validate: {
|
validate: {
|
||||||
payload: {
|
payload: {
|
||||||
email: Hapi.types.String().email().required()
|
email: Hapi.types.String().email().required()
|
||||||
|
@ -103,11 +104,11 @@ function wellKnown(request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function create(request) {
|
function create(request) {
|
||||||
kv.get(
|
account.create(
|
||||||
request.payload.email,
|
request.payload,
|
||||||
function (err, record) {
|
function (err, record) {
|
||||||
if (err) {
|
if (err) {
|
||||||
request.reply(Hapi.error.internal('Database errror', err));
|
request.reply(err);
|
||||||
}
|
}
|
||||||
else if (record) {
|
else if (record) {
|
||||||
request.reply('ok');
|
request.reply('ok');
|
||||||
|
@ -135,34 +136,37 @@ function sign(request) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function beginLogin(request) {
|
function startLogin(request) {
|
||||||
kv.get(
|
|
||||||
|
account.startLogin(
|
||||||
request.payload.email,
|
request.payload.email,
|
||||||
function (err, record) {
|
function (err, result) {
|
||||||
if (err) {
|
if (err) {
|
||||||
request.reply(Hapi.error.internal('Unable to get email', err));
|
request.reply(err);
|
||||||
}
|
|
||||||
else if (!record) {
|
|
||||||
request.reply(Hapi.error.notFound('Unknown email'));
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
var token = 'TODO';
|
request.reply(result);
|
||||||
request.reply({ sessionId: token });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function finishLogin(request) {
|
function finishLogin(request) {
|
||||||
// TODO lookup sessionId, verify email/password
|
|
||||||
var accountToken = 'TODO';
|
account.finishLogin(
|
||||||
var kA = 'TODO';
|
request.payload.sessionId,
|
||||||
var kB = 'TODO';
|
request.payload.password,
|
||||||
request.reply({
|
function (err, result) {
|
||||||
accountToken: accountToken,
|
if (err) {
|
||||||
kA: kA,
|
request.reply(err);
|
||||||
kB: kB
|
}
|
||||||
});
|
else {
|
||||||
|
request.reply(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
|
@ -23,11 +23,15 @@ var server = Hapi.createServer(bind.host, bind.port, settings);
|
||||||
server.addRoutes(routes);
|
server.addRoutes(routes);
|
||||||
|
|
||||||
server.ext(
|
server.ext(
|
||||||
'onPreResponse',
|
'onPreResponse',
|
||||||
function (request, next) {
|
function (request, next) {
|
||||||
request.response().header("Strict-Transport-Security", "max-age=10886400");
|
var res = request.response();
|
||||||
next();
|
// error responses don't have `header`
|
||||||
}
|
if (res.header) {
|
||||||
|
res.header("Strict-Transport-Security", "max-age=10886400");
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
//TODO throttle extension
|
//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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
Загрузка…
Ссылка в новой задаче