зеркало из https://github.com/mozilla/fxa.git
Merge pull request mozilla/fxa-auth-server#15 from mozilla/reset_protocol
Implement getResetToken and resetPassword of the idp protocol
This commit is contained in:
Коммит
d44e0a149e
|
@ -19,13 +19,24 @@ var notFound = Hapi.Error.notFound;
|
|||
* verifier: <password verifier>
|
||||
* kA: <kA key>
|
||||
* kB: <wrapped kB key>
|
||||
* accountTokens: {}
|
||||
* resetTokens: {}
|
||||
* signTokens: {}
|
||||
* }
|
||||
*
|
||||
* <sessionId>/session = {
|
||||
* uid: <userId>
|
||||
* }
|
||||
*
|
||||
* <accountToken>/account = {
|
||||
* <accountToken>/accountToken = {
|
||||
* uid: <userId>
|
||||
* }
|
||||
*
|
||||
* <resetToken>/resetToken = {
|
||||
* uid: <userId>
|
||||
* }
|
||||
*
|
||||
* <signToken>/signer = {
|
||||
* uid: <userId>
|
||||
* }
|
||||
*
|
||||
|
@ -56,7 +67,10 @@ exports.create = function(data, cb) {
|
|||
params: data.params,
|
||||
verifier: data.verifier,
|
||||
kA: key,
|
||||
kB: data.kB
|
||||
kB: data.kB,
|
||||
accountTokens: {},
|
||||
resetTokens: {},
|
||||
signTokens: {}
|
||||
}, cb);
|
||||
}
|
||||
], cb);
|
||||
|
@ -74,7 +88,7 @@ exports.startLogin = function(email, cb) {
|
|||
|
||||
// eventually will store SRP state
|
||||
// and expiration time
|
||||
kv.store.set(sid + '/session', {
|
||||
kv.cache.set(sid + '/session', {
|
||||
uid: uid
|
||||
}, function (err) {
|
||||
// return sessionID
|
||||
|
@ -92,7 +106,7 @@ exports.finishLogin = function(sessionId, verifier, cb) {
|
|||
async.waterfall([
|
||||
// get session doc
|
||||
function(cb) {
|
||||
kv.store.get(sessKey, function(err, session) {
|
||||
kv.cache.get(sessKey, function(err, session) {
|
||||
if (err) return cb(err);
|
||||
if (!session) return cb(notFound('UnknownSession'));
|
||||
cb(null, session.value);
|
||||
|
@ -101,7 +115,7 @@ exports.finishLogin = function(sessionId, verifier, cb) {
|
|||
// get user info
|
||||
function(session, cb) {
|
||||
uid = session.uid;
|
||||
exports.getUser(session.uid, cb);
|
||||
getUser(session.uid, cb);
|
||||
},
|
||||
|
||||
// check password
|
||||
|
@ -119,11 +133,11 @@ exports.finishLogin = function(sessionId, verifier, cb) {
|
|||
// create temporary account token doc
|
||||
function(token, cb) {
|
||||
accountToken = token;
|
||||
kv.store.set(token + '/accountToken', { uid: uid }, cb);
|
||||
addAccountToken(uid, token, cb);
|
||||
},
|
||||
// delete session doc
|
||||
function(cb) {
|
||||
kv.store.delete(sessKey, cb);
|
||||
kv.cache.delete(sessKey, cb);
|
||||
},
|
||||
// return info
|
||||
function(cb) {
|
||||
|
@ -145,7 +159,7 @@ exports.getSignToken = function(accountToken, cb) {
|
|||
// Check that the accountToken exists
|
||||
// and get the associated user id
|
||||
function(cb) {
|
||||
kv.store.get(accountKey, function(err, account) {
|
||||
kv.cache.get(accountKey, function(err, account) {
|
||||
if (err) return cb(err);
|
||||
if (!account) return cb(notFound('UknownAccountToken'));
|
||||
cb(null, account.value.uid);
|
||||
|
@ -158,14 +172,18 @@ exports.getSignToken = function(accountToken, cb) {
|
|||
},
|
||||
function(token, cb) {
|
||||
signToken = token;
|
||||
kv.store.set(token + '/signer', {
|
||||
uid: uid,
|
||||
accessTime: Date.now()
|
||||
addSignToken(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
|
||||
// delete accountToken record
|
||||
function(cb) {
|
||||
kv.store.delete(accountToken + '/accountToken', cb);
|
||||
kv.cache.delete(accountToken + '/accountToken', cb);
|
||||
},
|
||||
function(cb) {
|
||||
cb(null, { signToken: signToken });
|
||||
|
@ -173,6 +191,181 @@ exports.getSignToken = function(accountToken, 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.
|
||||
exports.getId = function(email, cb) {
|
||||
kv.store.get(email + '/uid', function(err, result) {
|
||||
|
@ -183,10 +376,19 @@ exports.getId = function(email, cb) {
|
|||
};
|
||||
|
||||
// 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) {
|
||||
if (err) return cb(internalError(err));
|
||||
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);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -25,6 +25,12 @@ function getSignToken(cb) {
|
|||
});
|
||||
}
|
||||
|
||||
function getResetToken(cb) {
|
||||
return crypto.randomBytes(32, function(err, buf) {
|
||||
cb(null, buf.toString('hex'));
|
||||
});
|
||||
}
|
||||
|
||||
function getUserId() {
|
||||
return uuid.v4();
|
||||
}
|
||||
|
@ -39,5 +45,6 @@ module.exports = {
|
|||
getUserId: getUserId,
|
||||
getSessionId: getSessionId,
|
||||
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) {
|
||||
|
@ -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 = {
|
||||
routes: routes
|
||||
};
|
||||
|
|
|
@ -10,10 +10,12 @@ var testClient = new helpers.TestClient();
|
|||
|
||||
var TEST_EMAIL = 'foo@example.com';
|
||||
var TEST_PASSWORD = 'foo';
|
||||
var TEST_PASSWORD_NEW = 'I like pie.';
|
||||
var TEST_KB = 'secret!';
|
||||
var TEST_KB_NEW = 'super secret!';
|
||||
|
||||
describe('user', function() {
|
||||
var sessionId, accountToken, pubkey, signToken;
|
||||
var sessionId, accountToken, pubkey, signToken, resetToken;
|
||||
|
||||
it('should create a new account', function(done) {
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче