Implement getResetToken and resetPassword of the idp protocol

This commit is contained in:
Zachary Carter 2013-05-24 15:58:08 -07:00
Родитель 37c7b18660
Коммит a40f11074f
4 изменённых файлов: 413 добавлений и 17 удалений

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

@ -19,13 +19,24 @@ var notFound = Hapi.Error.notFound;
* verifier: <password verifier> * verifier: <password verifier>
* kA: <kA key> * kA: <kA key>
* kB: <wrapped kB key> * kB: <wrapped kB key>
* accountTokens: {}
* resetTokens: {}
* signTokens: {}
* } * }
* *
* <sessionId>/session = { * <sessionId>/session = {
* uid: <userId> * uid: <userId>
* } * }
* *
* <accountToken>/account = { * <accountToken>/accountToken = {
* uid: <userId>
* }
*
* <resetToken>/resetToken = {
* uid: <userId>
* }
*
* <signToken>/signer = {
* uid: <userId> * uid: <userId>
* } * }
* *
@ -56,7 +67,10 @@ exports.create = function(data, cb) {
params: data.params, params: data.params,
verifier: data.verifier, verifier: data.verifier,
kA: key, kA: key,
kB: data.kB kB: data.kB,
accountTokens: {},
resetTokens: {},
signTokens: {}
}, cb); }, cb);
} }
], cb); ], cb);
@ -74,7 +88,7 @@ exports.startLogin = function(email, cb) {
// eventually will store SRP state // eventually will store SRP state
// and expiration time // and expiration time
kv.store.set(sid + '/session', { kv.cache.set(sid + '/session', {
uid: uid uid: uid
}, function (err) { }, function (err) {
// return sessionID // return sessionID
@ -92,7 +106,7 @@ exports.finishLogin = function(sessionId, verifier, cb) {
async.waterfall([ async.waterfall([
// get session doc // get session doc
function(cb) { function(cb) {
kv.store.get(sessKey, function(err, session) { kv.cache.get(sessKey, function(err, session) {
if (err) return cb(err); if (err) return cb(err);
if (!session) return cb(notFound('UnknownSession')); if (!session) return cb(notFound('UnknownSession'));
cb(null, session.value); cb(null, session.value);
@ -101,7 +115,7 @@ exports.finishLogin = function(sessionId, verifier, cb) {
// get user info // get user info
function(session, cb) { function(session, cb) {
uid = session.uid; uid = session.uid;
exports.getUser(session.uid, cb); getUser(session.uid, cb);
}, },
// check password // check password
@ -119,11 +133,11 @@ exports.finishLogin = function(sessionId, verifier, cb) {
// create temporary account token doc // create temporary account token doc
function(token, cb) { function(token, cb) {
accountToken = token; accountToken = token;
kv.store.set(token + '/accountToken', { uid: uid }, cb); addAccountToken(uid, token, cb);
}, },
// delete session doc // delete session doc
function(cb) { function(cb) {
kv.store.delete(sessKey, cb); kv.cache.delete(sessKey, cb);
}, },
// return info // return info
function(cb) { function(cb) {
@ -145,7 +159,7 @@ exports.getSignToken = function(accountToken, cb) {
// Check that the accountToken exists // Check that the accountToken exists
// and get the associated user id // and get the associated user id
function(cb) { function(cb) {
kv.store.get(accountKey, function(err, account) { kv.cache.get(accountKey, function(err, account) {
if (err) return cb(err); if (err) return cb(err);
if (!account) return cb(notFound('UknownAccountToken')); if (!account) return cb(notFound('UknownAccountToken'));
cb(null, account.value.uid); cb(null, account.value.uid);
@ -158,14 +172,18 @@ exports.getSignToken = function(accountToken, cb) {
}, },
function(token, cb) { function(token, cb) {
signToken = token; signToken = token;
kv.store.set(token + '/signer', { addSignToken(uid, token, cb);
uid: uid, },
accessTime: Date.now() // delete account token from user's list
function(cb) {
updateUserData(uid, function(userDoc) {
delete userDoc.value.accountTokens[accountToken];
return userDoc;
}, cb); }, cb);
}, },
// delete accountToken // delete accountToken record
function(cb) { function(cb) {
kv.store.delete(accountToken + '/accountToken', cb); kv.cache.delete(accountToken + '/accountToken', cb);
}, },
function(cb) { function(cb) {
cb(null, { signToken: signToken }); cb(null, { signToken: signToken });
@ -173,6 +191,181 @@ exports.getSignToken = function(accountToken, cb) {
], cb); ], cb);
}; };
// Takes an accountToken and creates a new resetToken
exports.getResetToken = function(accountToken, cb) {
var accountKey = accountToken + '/accountToken';
var uid, resetToken;
async.waterfall([
// Check that the accountToken exists
// and get the associated user id
function(cb) {
kv.cache.get(accountKey, function(err, account) {
if (err) return cb(err);
if (!account) return cb(notFound('UknownAccountToken'));
cb(null, account.value.uid);
});
},
// get new resetToken
function(id, cb) {
uid = id;
util.getResetToken(cb);
},
function(token, cb) {
resetToken = token;
addResetToken(uid, token, cb);
},
// delete account token from user's list
function(cb) {
updateUserData(uid, function(userDoc) {
delete userDoc.value.accountTokens[accountToken];
return userDoc;
}, cb);
},
// delete accountToken record
function(cb) {
kv.cache.delete(accountToken + '/accountToken', cb);
},
function(cb) {
cb(null, { resetToken: resetToken });
}
], cb);
};
exports.resetPassword = function(resetToken, data, cb) {
var userId;
async.waterfall([
// Check that the resetToken exists
// and get the associated user id
function(cb) {
kv.cache.get(resetToken + '/resetToken', function(err, doc) {
if (err) return cb(err);
if (!doc) return cb(notFound('UknownResetToken'));
userId = doc.value.uid;
cb(null);
});
},
// delete all accountTokens, signTokens, and resetTokens
function(cb) {
deleteAllTokens(userId, cb);
},
// get new class A key
util.getKA,
// create user account
function(key, cb) {
kv.store.set(userId + '/user', {
params: data.params,
verifier: data.verifier,
kA: key,
kB: data.kB,
accountTokens: {},
resetTokens: {},
signTokens: {}
}, cb);
}
], cb);
};
function deleteAllTokens(userId, cb) {
async.waterfall([
getUser.bind(null, userId),
function(user, cb) {
// map each token into a function that deletes that token's record
var funs = Object.keys(user.accountTokens).map(function(token) {
return function(cb) {
kv.cache.delete(token + '/accountToken', cb);
};
})
.concat(
Object.keys(user.signTokens).map(function(token) {
return function(cb) {
kv.store.delete(token + '/signer', cb);
};
}))
.concat(
Object.keys(user.resetTokens).map(function(token) {
return function(cb) {
kv.cache.delete(token + '/resetToken', cb);
};
}));
// run token deletions in parallel
async.parallel(funs, function(err) { cb(err); });
}
], cb);
}
function addTokenFn(tokenType, fn) {
return function (userId, token, cb) {
async.waterfall([
// First, add the signToken to the user's list
function(cb) {
updateUserData(userId, function(userDoc) {
if (token in userDoc.value[tokenType]) {
userDoc.value[tokenType][token] = true;
}
return userDoc;
}, cb);
},
fn.bind(null, token, userId),
], cb);
};
}
var addSignToken = addTokenFn('signTokens',
function(token, userId, cb) {
kv.store.set(token + '/signer', {
uid: userId,
accessTime: Date.now()
}, cb);
});
var addAccountToken = addTokenFn('accountTokens',
function(token, userId, cb) {
kv.cache.set(token + '/accountToken', {
uid: userId
}, cb);
});
var addResetToken = addTokenFn('resetTokens',
function(token, userId, cb) {
kv.cache.set(token + '/resetToken', {
uid: userId,
expirationTime: Date.now() + 60 * 60 * 24
}, cb);
});
function updateUserData(userId, update, cb) {
retryLoop('cas mismatch', 5, function(cb) {
getUserDoc(userId, function(err, doc) {
try {
doc = update(doc);
} catch (e) {
return cb(e);
}
kv.store.cas(userId + '/user', doc.value, doc.casid, cb);
});
}, cb);
}
// Helper function to retry execution in case of errors.
// 'fn' should be the function to execute, with its arguments bound
// except for the callback.
//
function retryLoop(errType, maxAttempts, fn, cb) {
var numRetries = 0;
var attempt = function() {
fn(function(err) {
if (!err) return cb(null);
if (err !== errType) return cb(err);
if (numRetries > maxAttempts) return cb('too many conflicts');
numRetries++;
process.nextTick(attempt);
});
};
attempt();
}
// This method returns the userId currently associated with an email address. // This method returns the userId currently associated with an email address.
exports.getId = function(email, cb) { exports.getId = function(email, cb) {
kv.store.get(email + '/uid', function(err, result) { kv.store.get(email + '/uid', function(err, result) {
@ -183,10 +376,19 @@ exports.getId = function(email, cb) {
}; };
// get meta data associated with a user // get meta data associated with a user
exports.getUser = function(userId, cb) { var getUserDoc = function(userId, cb) {
kv.store.get(userId + '/user', function(err, doc) { kv.store.get(userId + '/user', function(err, doc) {
if (err) return cb(internalError(err)); if (err) return cb(internalError(err));
if (!doc) return cb(notFound('UnknownUser')); if (!doc) return cb(notFound('UnknownUser'));
doc.id = userId;
cb(null, doc);
});
};
// get meta data associated with a user
var getUser = exports.getUser = function(userId, cb) {
getUserDoc(userId, function(err, doc) {
if (err) return cb(err);
cb(null, doc.value); cb(null, doc.value);
}); });
}; };

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

@ -25,6 +25,12 @@ function getSignToken(cb) {
}); });
} }
function getResetToken(cb) {
return crypto.randomBytes(32, function(err, buf) {
cb(null, buf.toString('hex'));
});
}
function getUserId() { function getUserId() {
return uuid.v4(); return uuid.v4();
} }
@ -39,5 +45,6 @@ module.exports = {
getUserId: getUserId, getUserId: getUserId,
getSessionId: getSessionId, getSessionId: getSessionId,
getAccountToken: getAccountToken, getAccountToken: getAccountToken,
getSignToken: getSignToken getSignToken: getSignToken,
getResetToken: getResetToken
}; };

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

@ -112,7 +112,39 @@ var routes = [
} }
} }
} }
} },
{
method: 'POST',
path: '/resetToken',
config: {
handler: getResetToken,
validate: {
payload: {
accountToken: Hapi.types.String().required()
},
response: {
schema: {
resetToken: Hapi.types.String().required()
}
}
}
}
},
{
method: 'POST',
path: '/resetPassword',
config: {
handler: resetPassword,
validate: {
payload: {
resetToken: Hapi.types.String().required(),
verifier: Hapi.types.String().required(),
params: Hapi.types.Object(),
kB: Hapi.types.String()
}
}
}
},
]; ];
function wellKnown(request) { function wellKnown(request) {
@ -209,6 +241,36 @@ function getSignToken(request) {
); );
} }
function getResetToken(request) {
account.getResetToken(
request.payload.accountToken,
function (err, result) {
if (err) {
request.reply(err);
}
else {
request.reply(result);
}
}
);
}
function resetPassword(request) {
account.resetPassword(
request.payload.resetToken,
request.payload,
function (err) {
if (err) {
request.reply(err);
}
else {
request.reply('ok');
}
}
);
}
module.exports = { module.exports = {
routes: routes routes: routes
}; };

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

@ -10,10 +10,12 @@ var testClient = new helpers.TestClient();
var TEST_EMAIL = 'foo@example.com'; var TEST_EMAIL = 'foo@example.com';
var TEST_PASSWORD = 'foo'; var TEST_PASSWORD = 'foo';
var TEST_PASSWORD_NEW = 'I like pie.';
var TEST_KB = 'secret!'; var TEST_KB = 'secret!';
var TEST_KB_NEW = 'super secret!';
describe('user', function() { describe('user', function() {
var sessionId, accountToken, pubkey, signToken; var sessionId, accountToken, pubkey, signToken, resetToken;
it('should create a new account', function(done) { it('should create a new account', function(done) {
testClient.makeRequest('POST', '/create', { testClient.makeRequest('POST', '/create', {
@ -197,5 +199,128 @@ describe('user', function() {
}); });
}); });
it('should begin a new 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 finish login and get a new accountToken', function(done) {
testClient.makeRequest('POST', '/finishLogin', {
payload: {
sessionId: sessionId,
password: TEST_PASSWORD
}
}, function(res) {
try {
accountToken = res.result.accountToken;
assert.ok(res.result.accountToken);
assert.ok(res.result.kA);
assert.equal(res.result.kB, TEST_KB);
} catch (e) {
return done(e);
}
done();
});
});
it('should get resetToken', function(done) {
testClient.makeRequest('POST', '/resetToken', {
payload: {
accountToken: accountToken
}
}, function(res) {
try {
assert.equal(res.statusCode, 200);
resetToken = res.result.resetToken;
assert.ok(res.result.resetToken);
} catch (e) {
return done(e);
}
done();
});
});
it('should reset the account', function(done) {
testClient.makeRequest('POST', '/resetPassword', {
payload: {
resetToken: resetToken,
verifier: TEST_PASSWORD_NEW,
params: { foo: 'bar2' },
kB: TEST_KB_NEW
}
}, function(res) {
try {
assert.equal(res.statusCode, 200);
assert.equal(res.result, 'ok');
} catch (e) {
return done(e);
}
done();
});
});
it('should begin a login with resetted account', 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 old password', function(done) {
testClient.makeRequest('POST', '/finishLogin', {
payload: {
sessionId: sessionId,
password: TEST_PASSWORD
}
}, function(res) {
try {
assert.equal(res.statusCode, 400);
assert.equal(res.result.message, 'IncorrectPassword');
} catch (e) {
return done(e);
}
done();
});
});
it('should finish login with new password', function(done) {
testClient.makeRequest('POST', '/finishLogin', {
payload: {
sessionId: sessionId,
password: TEST_PASSWORD_NEW
}
}, function(res) {
try {
accountToken = res.result.accountToken;
assert.ok(res.result.accountToken);
assert.ok(res.result.kA);
assert.equal(res.result.kB, TEST_KB_NEW);
} catch (e) {
return done(e);
}
done();
});
});
}); });