Register TOTP secrets per user
parent
b205ba6a0d
commit
90494407a9
|
@ -32,6 +32,7 @@
|
|||
"nedb": "^1.8.0",
|
||||
"nodemailer": "^2.7.0",
|
||||
"object-path": "^0.11.3",
|
||||
"qrcode": "^0.5.0",
|
||||
"randomstring": "^1.1.5",
|
||||
"speakeasy": "^2.0.0",
|
||||
"winston": "^2.3.1",
|
||||
|
|
|
@ -5,6 +5,7 @@ module.exports = {
|
|||
IdentityError: IdentityError,
|
||||
AccessDeniedError: AccessDeniedError,
|
||||
AuthenticationRegulationError: AuthenticationRegulationError,
|
||||
InvalidTOTPError: InvalidTOTPError,
|
||||
}
|
||||
|
||||
function LdapSearchError(message) {
|
||||
|
@ -36,3 +37,9 @@ function AuthenticationRegulationError(message) {
|
|||
this.message = (message || "");
|
||||
}
|
||||
AuthenticationRegulationError.prototype = Object.create(Error.prototype);
|
||||
|
||||
function InvalidTOTPError(message) {
|
||||
this.name = "InvalidTOTPError";
|
||||
this.message = (message || "");
|
||||
}
|
||||
InvalidTOTPError.prototype = Object.create(Error.prototype);
|
||||
|
|
|
@ -4,6 +4,7 @@ var second_factor = require('./routes/second_factor');
|
|||
var reset_password = require('./routes/reset_password');
|
||||
var verify = require('./routes/verify');
|
||||
var u2f_register_handler = require('./routes/u2f_register_handler');
|
||||
var totp_register = require('./routes/totp_register');
|
||||
var objectPath = require('object-path');
|
||||
|
||||
module.exports = {
|
||||
|
@ -13,7 +14,8 @@ module.exports = {
|
|||
first_factor: first_factor,
|
||||
second_factor: second_factor,
|
||||
reset_password: reset_password,
|
||||
u2f_register: u2f_register_handler
|
||||
u2f_register: u2f_register_handler,
|
||||
totp_register: totp_register,
|
||||
}
|
||||
|
||||
function serveLogin(req, res) {
|
||||
|
|
|
@ -3,31 +3,48 @@ module.exports = totp;
|
|||
|
||||
var totp = require('../totp');
|
||||
var objectPath = require('object-path');
|
||||
var exceptions = require('../../../src/lib/exceptions');
|
||||
|
||||
var UNAUTHORIZED_MESSAGE = 'Unauthorized access';
|
||||
|
||||
function replyWithUnauthorized(res) {
|
||||
res.status(401);
|
||||
res.send();
|
||||
}
|
||||
|
||||
function totp(req, res) {
|
||||
if(!objectPath.has(req, 'session.auth_session.second_factor')) {
|
||||
replyWithUnauthorized(res);
|
||||
var logger = req.app.get('logger');
|
||||
var userid = objectPath.get(req, 'session.auth_session.userid');
|
||||
logger.info('POST 2ndfactor totp: Initiate TOTP validation for user %s', userid);
|
||||
|
||||
if(!userid) {
|
||||
logger.error('POST 2ndfactor totp: No user id in the session');
|
||||
res.status(403);
|
||||
res.send();
|
||||
return;
|
||||
}
|
||||
|
||||
var token = req.body.token;
|
||||
|
||||
var totp_engine = req.app.get('totp engine');
|
||||
var config = req.app.get('config');
|
||||
var data_store = req.app.get('user data store');
|
||||
|
||||
totp.validate(totp_engine, token, config.totp_secret)
|
||||
logger.debug('POST 2ndfactor totp: Fetching secret for user %s', userid);
|
||||
data_store.get_totp_secret(userid)
|
||||
.then(function(doc) {
|
||||
logger.debug('POST 2ndfactor totp: TOTP secret is %s', JSON.stringify(doc));
|
||||
return totp.validate(totp_engine, token, doc.secret.base32)
|
||||
})
|
||||
.then(function() {
|
||||
req.session.auth_session.second_factor = true;
|
||||
logger.debug('POST 2ndfactor totp: TOTP validation succeeded');
|
||||
objectPath.set(req, 'session.auth_session.second_factor', true);
|
||||
res.status(204);
|
||||
res.send();
|
||||
}, function(err) {
|
||||
throw new exceptions.InvalidTOTPError();
|
||||
})
|
||||
.catch(exceptions.InvalidTOTPError, function(err) {
|
||||
logger.error('POST 2ndfactor totp: Invalid TOTP token %s', err);
|
||||
res.status(401);
|
||||
res.send('Invalid TOTP token');
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.error(err);
|
||||
replyWithUnauthorized(res);
|
||||
logger.error('POST 2ndfactor totp: Internal error %s', err);
|
||||
res.status(500);
|
||||
res.send('Internal error');
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
var objectPath = require('object-path');
|
||||
var Promise = require('bluebird');
|
||||
var QRCode = require('qrcode');
|
||||
|
||||
var CHALLENGE = 'totp-register';
|
||||
|
||||
var icheck_interface = {
|
||||
challenge: CHALLENGE,
|
||||
render_template: 'totp-register',
|
||||
pre_check_callback: pre_check,
|
||||
email_subject: 'Register your TOTP secret key',
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
icheck_interface: icheck_interface,
|
||||
post: post,
|
||||
}
|
||||
|
||||
|
||||
function pre_check(req) {
|
||||
var first_factor_passed = objectPath.get(req, 'session.auth_session.first_factor');
|
||||
if(!first_factor_passed) {
|
||||
return Promise.reject('Authentication required before registering TOTP secret key');
|
||||
}
|
||||
|
||||
var userid = objectPath.get(req, 'session.auth_session.userid');
|
||||
var email = objectPath.get(req, 'session.auth_session.email');
|
||||
|
||||
if(!(userid && email)) {
|
||||
return Promise.reject('User ID or email is missing');
|
||||
}
|
||||
|
||||
var identity = {};
|
||||
identity.email = email;
|
||||
identity.userid = userid;
|
||||
return Promise.resolve(identity);
|
||||
}
|
||||
|
||||
|
||||
function secretToDataURLAsync(secret) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
QRCode.toDataURL(secret.otpauth_url, function(err, url_data) {
|
||||
if(err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(url_data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Generate a secret and send it to the user
|
||||
function post(req, res) {
|
||||
var logger = req.app.get('logger');
|
||||
var userid = objectPath.get(req, 'session.auth_session.identity_check.userid');
|
||||
var challenge = objectPath.get(req, 'session.auth_session.identity_check.challenge');
|
||||
|
||||
if(challenge != CHALLENGE || !userid) {
|
||||
res.status(403);
|
||||
res.send();
|
||||
return;
|
||||
}
|
||||
|
||||
var user_data_store = req.app.get('user data store');
|
||||
var totp = req.app.get('totp engine');
|
||||
var secret = totp.generateSecret();
|
||||
var qrcode_data;
|
||||
|
||||
secretToDataURLAsync(secret)
|
||||
.then(function(data) {
|
||||
qrcode_data = data;
|
||||
logger.debug('POST new-totp-secret: save the TOTP secret in DB');
|
||||
return user_data_store.set_totp_secret(userid, secret);
|
||||
})
|
||||
.then(function() {
|
||||
var doc = {};
|
||||
doc.qrcode = qrcode_data;
|
||||
doc.base32 = secret.base32;
|
||||
doc.ascii = secret.ascii;
|
||||
|
||||
objectPath.set(req, 'session', undefined);
|
||||
|
||||
res.status(200);
|
||||
res.json(doc);
|
||||
})
|
||||
.catch(function(err) {
|
||||
logger.error('POST new-totp-secret: Internal error %s', err);
|
||||
res.status(500);
|
||||
res.send();
|
||||
});
|
||||
}
|
||||
|
|
@ -69,6 +69,7 @@ function run(config, ldap_client, deps, fn) {
|
|||
app.get (base_endpoint + '/login', routes.login);
|
||||
app.get (base_endpoint + '/logout', routes.logout);
|
||||
|
||||
identity_check(app, base_endpoint + '/totp-register', routes.totp_register.icheck_interface);
|
||||
identity_check(app, base_endpoint + '/u2f-register', routes.u2f_register.icheck_interface);
|
||||
identity_check(app, base_endpoint + '/reset-password', routes.reset_password.icheck_interface);
|
||||
|
||||
|
@ -77,6 +78,9 @@ function run(config, ldap_client, deps, fn) {
|
|||
// Reset the password
|
||||
app.post (base_endpoint + '/new-password', routes.reset_password.post);
|
||||
|
||||
// Generate a new TOTP secret
|
||||
app.post (base_endpoint + '/new-totp-secret', routes.totp_register.post);
|
||||
|
||||
// verify authentication
|
||||
app.get (base_endpoint + '/verify', routes.verify);
|
||||
|
||||
|
|
|
@ -10,6 +10,8 @@ function UserDataStore(DataStore, options) {
|
|||
create_collection('identity_check_tokens', options, DataStore);
|
||||
this._authentication_traces_collection =
|
||||
create_collection('authentication_traces', options, DataStore);
|
||||
this._totp_secret_collection =
|
||||
create_collection('totp_secrets', options, DataStore);
|
||||
}
|
||||
|
||||
function create_collection(name, options, DataStore) {
|
||||
|
@ -104,3 +106,19 @@ UserDataStore.prototype.consume_identity_check_token = function(token) {
|
|||
return Promise.resolve(doc_content);
|
||||
})
|
||||
}
|
||||
|
||||
UserDataStore.prototype.set_totp_secret = function(userid, secret) {
|
||||
var doc = {}
|
||||
doc.userid = userid;
|
||||
doc.secret = secret;
|
||||
|
||||
var query = {};
|
||||
query.userid = userid;
|
||||
return this._totp_secret_collection.updateAsync(query, doc, { upsert: true });
|
||||
}
|
||||
|
||||
UserDataStore.prototype.get_totp_secret = function(userid) {
|
||||
var query = {};
|
||||
query.userid = userid;
|
||||
return this._totp_secret_collection.findOneAsync(query);
|
||||
}
|
||||
|
|
|
@ -39,11 +39,27 @@ body {
|
|||
width:300px;
|
||||
height:300px;
|
||||
}
|
||||
.login h1 { color: #fff; text-shadow: 0 0 10px rgba(0,0,0,0.3); letter-spacing:1px; text-align:center; }
|
||||
|
||||
.login h2 { color: #fff; text-shadow: 0 0 10px rgba(0,0,0,0.3); letter-spacing:1px; text-align:center; font-size: 1em; }
|
||||
.totp {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin: -150px 0 0 -150px;
|
||||
width:400px;
|
||||
height:300px;
|
||||
}
|
||||
|
||||
.login p { color: #fff; text-shadow: 0 0 10px rgba(0,0,0,0.3); letter-spacing:1px; text-align:center; }
|
||||
h1 { color: #fff; text-shadow: 0 0 10px rgba(0,0,0,0.3); letter-spacing:1px; text-align:center; }
|
||||
|
||||
h2 { color: #fff; text-shadow: 0 0 10px rgba(0,0,0,0.3); letter-spacing:1px; text-align:center; font-size: 1em; }
|
||||
|
||||
p { color: #fff; text-shadow: 0 0 10px rgba(0,0,0,0.3); letter-spacing:1px; text-align:center; }
|
||||
|
||||
a { color: #fff; text-align: center; }
|
||||
|
||||
#qrcode { text-align: center; }
|
||||
|
||||
#secret { font-size: 0.7em; }
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
|
|
|
@ -39,13 +39,26 @@ function onTotpSignButtonClicked() {
|
|||
var token = $('#totp-token').val();
|
||||
validateSecondFactorTotp(token, function(err) {
|
||||
if(err) {
|
||||
onSecondFactorTotpFailure();
|
||||
onSecondFactorTotpFailure(err.responseText);
|
||||
return;
|
||||
}
|
||||
onSecondFactorTotpSuccess();
|
||||
});
|
||||
}
|
||||
|
||||
function onTotpRegisterButtonClicked() {
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/authentication/totp-register'
|
||||
})
|
||||
.done(function(data) {
|
||||
$.notify('An email has been sent to your email address', 'info');
|
||||
})
|
||||
.fail(function(xhr, status) {
|
||||
$.notify('Unable to send you an email', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
function onU2fSignButtonClicked() {
|
||||
startU2fAuthentication(function(err) {
|
||||
if(err) {
|
||||
|
@ -174,8 +187,8 @@ function onSecondFactorTotpSuccess() {
|
|||
onAuthenticationSuccess();
|
||||
}
|
||||
|
||||
function onSecondFactorTotpFailure() {
|
||||
$.notify('Wrong TOTP token', 'error');
|
||||
function onSecondFactorTotpFailure(err) {
|
||||
$.notify('Error while validating TOTP token. Cause: ' + err, 'error');
|
||||
}
|
||||
|
||||
function onU2fAuthenticationSuccess() {
|
||||
|
@ -216,6 +229,10 @@ function setupTotpSignButton() {
|
|||
setupEnterKeypressListener('#totp', onTotpSignButtonClicked);
|
||||
}
|
||||
|
||||
function setupTotpRegisterButton() {
|
||||
$('#second-factor #totp-register-button').on('click', onTotpRegisterButtonClicked);
|
||||
}
|
||||
|
||||
function setupU2fSignButton() {
|
||||
$('#second-factor #u2f-sign-button').on('click', onU2fSignButtonClicked);
|
||||
setupEnterKeypressListener('#u2f', onU2fSignButtonClicked);
|
||||
|
@ -241,6 +258,7 @@ function enterSecondFactor() {
|
|||
showSecondFactorLayout();
|
||||
cleanupFirstFactorLoginButton();
|
||||
setupTotpSignButton();
|
||||
setupTotpRegisterButton();
|
||||
setupU2fSignButton();
|
||||
setupU2fRegistrationButton();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
(function() {
|
||||
|
||||
function generateSecret(fn) {
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/authentication/new-totp-secret',
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
})
|
||||
.done(function(data) {
|
||||
fn(undefined, data);
|
||||
})
|
||||
.fail(function(xhr, status) {
|
||||
$.notify('Error when generating TOTP secret');
|
||||
});
|
||||
}
|
||||
|
||||
function onSecretGenerated(err, secret) {
|
||||
// console.log('secret generated successfully', secret);
|
||||
var img = $('<img src="' + secret.qrcode + '" alt="secret-qrcode"/>');
|
||||
$('#qrcode').append(img);
|
||||
$("#secret").text(secret.base32);
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
generateSecret(onSecretGenerated);
|
||||
});
|
||||
})();
|
|
@ -20,6 +20,7 @@
|
|||
<h2>Time-Based One-Time Password</h2>
|
||||
<input type="text" name="totp-token" id="totp-token" placeholder="Validation token" />
|
||||
<button type="button" id="totp-sign-button" class="btn btn-primary btn-block btn-large">Sign</button>
|
||||
<button type="button" id="totp-register-button" class="btn btn-primary btn-block btn-large">Register</button>
|
||||
</div>
|
||||
<div id="u2f">
|
||||
<h2>FIDO Universal 2nd Factor</h2>
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
<!DOCTYPE html>
|
||||
<head>
|
||||
<title>TOTP Registration</title>
|
||||
<% include head %>
|
||||
</head>
|
||||
<body>
|
||||
<div class="totp">
|
||||
<h1>TOTP Secret</h1>
|
||||
<p>Insert your secret in Google Authenticator</p>
|
||||
<p id="secret"></p>
|
||||
<div id="qrcode"></div>
|
||||
<p><a href="/authentication/login">Login</a></p>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<% include scripts %>
|
||||
<script src="js/totp-register.js"></script>
|
||||
</html>
|
|
@ -36,6 +36,37 @@ module.exports = function(port) {
|
|||
});
|
||||
}
|
||||
|
||||
function execute_register_totp(jar, transporter) {
|
||||
return request.postAsync({
|
||||
url: BASE_URL + '/authentication/totp-register',
|
||||
jar: jar
|
||||
})
|
||||
.then(function(res) {
|
||||
assert.equal(res.statusCode, 204);
|
||||
var html_content = transporter.sendMail.getCall(0).args[0].html;
|
||||
var regexp = /identity_token=([a-zA-Z0-9]+)/;
|
||||
var token = regexp.exec(html_content)[1];
|
||||
// console.log(html_content, token);
|
||||
return request.getAsync({
|
||||
url: BASE_URL + '/authentication/totp-register?identity_token=' + token,
|
||||
jar: jar
|
||||
})
|
||||
})
|
||||
.then(function(res) {
|
||||
assert.equal(res.statusCode, 200);
|
||||
return request.postAsync({
|
||||
url : BASE_URL + '/authentication/new-totp-secret',
|
||||
jar: jar,
|
||||
})
|
||||
})
|
||||
.then(function(res) {
|
||||
console.log(res.statusCode);
|
||||
console.log(res.body);
|
||||
assert.equal(res.statusCode, 200);
|
||||
return Promise.resolve(res.body);
|
||||
});
|
||||
}
|
||||
|
||||
function execute_totp(jar, token) {
|
||||
return request.postAsync({
|
||||
url: BASE_URL + '/authentication/2ndfactor/totp',
|
||||
|
@ -136,6 +167,7 @@ module.exports = function(port) {
|
|||
first_factor: execute_first_factor,
|
||||
failing_first_factor: execute_failing_first_factor,
|
||||
totp: execute_totp,
|
||||
register_totp: execute_register_totp,
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -3,10 +3,12 @@ var totp = require('../../../src/lib/routes/totp');
|
|||
var Promise = require('bluebird');
|
||||
var sinon = require('sinon');
|
||||
var assert = require('assert');
|
||||
var winston = require('winston');
|
||||
|
||||
describe('test totp route', function() {
|
||||
var req, res;
|
||||
var totp_engine;
|
||||
var user_data_store;
|
||||
|
||||
beforeEach(function() {
|
||||
var app_get = sinon.stub();
|
||||
|
@ -19,6 +21,7 @@ describe('test totp route', function() {
|
|||
},
|
||||
session: {
|
||||
auth_session: {
|
||||
userid: 'user',
|
||||
first_factor: false,
|
||||
second_factor: false
|
||||
}
|
||||
|
@ -33,46 +36,52 @@ describe('test totp route', function() {
|
|||
totp_engine = {
|
||||
totp: sinon.stub()
|
||||
}
|
||||
|
||||
user_data_store = {};
|
||||
user_data_store.get_totp_secret = sinon.stub();
|
||||
|
||||
var doc = {};
|
||||
doc.userid = 'user';
|
||||
doc.secret = {};
|
||||
doc.secret.base32 = 'ABCDEF';
|
||||
user_data_store.get_totp_secret.returns(Promise.resolve(doc));
|
||||
|
||||
app_get.withArgs('logger').returns(winston);
|
||||
app_get.withArgs('totp engine').returns(totp_engine);
|
||||
app_get.withArgs('config').returns(config);
|
||||
app_get.withArgs('user data store').returns(user_data_store);
|
||||
});
|
||||
|
||||
|
||||
it('should send status code 204 when totp is valid', function() {
|
||||
return new Promise(function(resolve, reject) {
|
||||
it('should send status code 204 when totp is valid', function(done) {
|
||||
totp_engine.totp.returns('abc');
|
||||
res.send = sinon.spy(function() {
|
||||
// Second factor passed
|
||||
assert.equal(true, req.session.auth_session.second_factor)
|
||||
assert.equal(204, res.status.getCall(0).args[0]);
|
||||
resolve();
|
||||
done();
|
||||
});
|
||||
totp(req, res);
|
||||
})
|
||||
});
|
||||
|
||||
it('should send status code 401 when totp is not valid', function() {
|
||||
return new Promise(function(resolve, reject) {
|
||||
it('should send status code 401 when totp is not valid', function(done) {
|
||||
totp_engine.totp.returns('bad_token');
|
||||
res.send = sinon.spy(function() {
|
||||
assert.equal(false, req.session.auth_session.second_factor)
|
||||
assert.equal(401, res.status.getCall(0).args[0]);
|
||||
resolve();
|
||||
done();
|
||||
});
|
||||
totp(req, res);
|
||||
})
|
||||
});
|
||||
|
||||
it('should send status code 401 when session has not been initiated', function() {
|
||||
return new Promise(function(resolve, reject) {
|
||||
it('should send status code 401 when session has not been initiated', function(done) {
|
||||
totp_engine.totp.returns('abc');
|
||||
res.send = sinon.spy(function() {
|
||||
assert.equal(401, res.status.getCall(0).args[0]);
|
||||
resolve();
|
||||
assert.equal(403, res.status.getCall(0).args[0]);
|
||||
done();
|
||||
});
|
||||
req.session = {};
|
||||
totp(req, res);
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
var sinon = require('sinon');
|
||||
var winston = require('winston');
|
||||
var totp_register = require('../../../src/lib/routes/totp_register');
|
||||
var assert = require('assert');
|
||||
var Promise = require('bluebird');
|
||||
|
||||
describe('test totp register', function() {
|
||||
var req, res;
|
||||
var user_data_store;
|
||||
|
||||
beforeEach(function() {
|
||||
req = {}
|
||||
req.app = {};
|
||||
req.app.get = sinon.stub();
|
||||
req.app.get.withArgs('logger').returns(winston);
|
||||
req.session = {};
|
||||
req.session.auth_session = {};
|
||||
req.session.auth_session.userid = 'user';
|
||||
req.session.auth_session.email = 'user@example.com';
|
||||
req.session.auth_session.first_factor = true;
|
||||
req.session.auth_session.second_factor = false;
|
||||
req.headers = {};
|
||||
req.headers.host = 'localhost';
|
||||
|
||||
var options = {};
|
||||
options.inMemoryOnly = true;
|
||||
|
||||
user_data_store = {};
|
||||
user_data_store.set_u2f_meta = sinon.stub().returns(Promise.resolve({}));
|
||||
user_data_store.get_u2f_meta = sinon.stub().returns(Promise.resolve({}));
|
||||
user_data_store.issue_identity_check_token = sinon.stub().returns(Promise.resolve({}));
|
||||
user_data_store.consume_identity_check_token = sinon.stub().returns(Promise.resolve({}));
|
||||
user_data_store.set_totp_secret = sinon.stub().returns(Promise.resolve({}));
|
||||
req.app.get.withArgs('user data store').returns(user_data_store);
|
||||
|
||||
res = {};
|
||||
res.send = sinon.spy();
|
||||
res.json = sinon.spy();
|
||||
res.status = sinon.spy();
|
||||
});
|
||||
|
||||
describe('test totp registration check', test_registration_check);
|
||||
describe('test totp post secret', test_post_secret);
|
||||
|
||||
function test_registration_check() {
|
||||
it('should fail if first_factor has not been passed', function(done) {
|
||||
req.session.auth_session.first_factor = false;
|
||||
totp_register.icheck_interface.pre_check_callback(req)
|
||||
.catch(function(err) {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail if userid is missing', function(done) {
|
||||
req.session.auth_session.first_factor = false;
|
||||
req.session.auth_session.userid = undefined;
|
||||
|
||||
totp_register.icheck_interface.pre_check_callback(req)
|
||||
.catch(function(err) {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail if email is missing', function(done) {
|
||||
req.session.auth_session.first_factor = false;
|
||||
req.session.auth_session.email = undefined;
|
||||
|
||||
totp_register.icheck_interface.pre_check_callback(req)
|
||||
.catch(function(err) {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should succeed if first factor passed, userid and email are provided', function(done) {
|
||||
totp_register.icheck_interface.pre_check_callback(req)
|
||||
.then(function(err) {
|
||||
done();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function test_post_secret() {
|
||||
it('should send the secret in json format', function(done) {
|
||||
req.app.get.withArgs('totp engine').returns(require('speakeasy'));
|
||||
req.session.auth_session.identity_check = {};
|
||||
req.session.auth_session.identity_check.userid = 'user';
|
||||
req.session.auth_session.identity_check.challenge = 'totp-register';
|
||||
res.json = sinon.spy(function() {
|
||||
done();
|
||||
});
|
||||
totp_register.post(req, res);
|
||||
});
|
||||
|
||||
it('should clear the session for reauthentication', function(done) {
|
||||
req.app.get.withArgs('totp engine').returns(require('speakeasy'));
|
||||
req.session.auth_session.identity_check = {};
|
||||
req.session.auth_session.identity_check.userid = 'user';
|
||||
req.session.auth_session.identity_check.challenge = 'totp-register';
|
||||
res.json = sinon.spy(function() {
|
||||
assert.equal(req.session, undefined);
|
||||
done();
|
||||
});
|
||||
totp_register.post(req, res);
|
||||
});
|
||||
|
||||
it('should return 403 if the identity check challenge is not set', function(done) {
|
||||
req.session.auth_session.identity_check = {};
|
||||
req.session.auth_session.identity_check.challenge = undefined;
|
||||
res.send = sinon.spy(function() {
|
||||
assert.equal(res.status.getCall(0).args[0], 403);
|
||||
done();
|
||||
});
|
||||
totp_register.post(req, res);
|
||||
});
|
||||
|
||||
it('should return 500 if db throws', function(done) {
|
||||
req.app.get.withArgs('totp engine').returns(require('speakeasy'));
|
||||
req.session.auth_session.identity_check = {};
|
||||
req.session.auth_session.identity_check.userid = 'user';
|
||||
req.session.auth_session.identity_check.challenge = 'totp-register';
|
||||
user_data_store.set_totp_secret.throws('internal error');
|
||||
|
||||
res.send = sinon.spy(function() {
|
||||
assert.equal(res.status.getCall(0).args[0], 500);
|
||||
done();
|
||||
});
|
||||
totp_register.post(req, res);
|
||||
});
|
||||
}
|
||||
});
|
|
@ -219,10 +219,6 @@ describe('test the server', function() {
|
|||
});
|
||||
|
||||
it('should return status code 204 when user is authenticated using totp', function() {
|
||||
var real_token = speakeasy.totp({
|
||||
secret: 'totp_secret',
|
||||
encoding: 'base32'
|
||||
});
|
||||
var j = request.jar();
|
||||
return requests.login(j)
|
||||
.then(function(res) {
|
||||
|
@ -231,6 +227,14 @@ describe('test the server', function() {
|
|||
})
|
||||
.then(function(res) {
|
||||
assert.equal(res.statusCode, 204, 'first factor failed');
|
||||
return requests.register_totp(j, transporter);
|
||||
})
|
||||
.then(function(secret) {
|
||||
var sec = JSON.parse(secret);
|
||||
var real_token = speakeasy.totp({
|
||||
secret: sec.base32,
|
||||
encoding: 'base32'
|
||||
});
|
||||
return requests.totp(j, real_token);
|
||||
})
|
||||
.then(function(res) {
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
|
||||
var assert = require('assert');
|
||||
var Promise = require('bluebird');
|
||||
var sinon = require('sinon');
|
||||
var MockDate = require('mockdate');
|
||||
var UserDataStore = require('../../../src/lib/user_data_store');
|
||||
var DataStore = require('nedb');
|
||||
|
||||
describe('test user data store', function() {
|
||||
describe('test totp secrets store', test_totp_secrets);
|
||||
});
|
||||
|
||||
function test_totp_secrets() {
|
||||
it('should save and reload a totp secret', function() {
|
||||
var options = {};
|
||||
options.inMemoryOnly = true;
|
||||
|
||||
var data_store = new UserDataStore(DataStore, options);
|
||||
var userid = 'user';
|
||||
var secret = {};
|
||||
secret.ascii = 'abc';
|
||||
secret.base32 = 'ABCDKZLEFZGREJK';
|
||||
|
||||
return data_store.set_totp_secret(userid, secret)
|
||||
.then(function() {
|
||||
return data_store.get_totp_secret(userid);
|
||||
})
|
||||
.then(function(doc) {
|
||||
assert('_id' in doc);
|
||||
assert.equal(doc.userid, 'user');
|
||||
assert.equal(doc.secret.ascii, 'abc');
|
||||
assert.equal(doc.secret.base32, 'ABCDKZLEFZGREJK');
|
||||
return Promise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
it('should only remember last secret', function() {
|
||||
var options = {};
|
||||
options.inMemoryOnly = true;
|
||||
|
||||
var data_store = new UserDataStore(DataStore, options);
|
||||
var userid = 'user';
|
||||
var secret1 = {};
|
||||
secret1.ascii = 'abc';
|
||||
secret1.base32 = 'ABCDKZLEFZGREJK';
|
||||
var secret2 = {};
|
||||
secret2.ascii = 'def';
|
||||
secret2.base32 = 'XYZABC';
|
||||
|
||||
return data_store.set_totp_secret(userid, secret1)
|
||||
.then(function() {
|
||||
return data_store.set_totp_secret(userid, secret2)
|
||||
})
|
||||
.then(function() {
|
||||
return data_store.get_totp_secret(userid);
|
||||
})
|
||||
.then(function(doc) {
|
||||
assert('_id' in doc);
|
||||
assert.equal(doc.userid, 'user');
|
||||
assert.equal(doc.secret.ascii, 'def');
|
||||
assert.equal(doc.secret.base32, 'XYZABC');
|
||||
return Promise.resolve();
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue