зеркало из https://github.com/mozilla/fxa.git
Implement getResetToken and resetPassword of the idp protocol
This commit is contained in:
Родитель
37c7b18660
Коммит
a40f11074f
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Загрузка…
Ссылка в новой задаче