mirror of
https://github.com/0rangebananaspy/authelia.git
synced 2024-09-14 22:47:21 +07:00
Register TOTP secrets per user
This commit is contained in:
parent
b205ba6a0d
commit
90494407a9
|
@ -32,6 +32,7 @@
|
||||||
"nedb": "^1.8.0",
|
"nedb": "^1.8.0",
|
||||||
"nodemailer": "^2.7.0",
|
"nodemailer": "^2.7.0",
|
||||||
"object-path": "^0.11.3",
|
"object-path": "^0.11.3",
|
||||||
|
"qrcode": "^0.5.0",
|
||||||
"randomstring": "^1.1.5",
|
"randomstring": "^1.1.5",
|
||||||
"speakeasy": "^2.0.0",
|
"speakeasy": "^2.0.0",
|
||||||
"winston": "^2.3.1",
|
"winston": "^2.3.1",
|
||||||
|
|
|
@ -5,6 +5,7 @@ module.exports = {
|
||||||
IdentityError: IdentityError,
|
IdentityError: IdentityError,
|
||||||
AccessDeniedError: AccessDeniedError,
|
AccessDeniedError: AccessDeniedError,
|
||||||
AuthenticationRegulationError: AuthenticationRegulationError,
|
AuthenticationRegulationError: AuthenticationRegulationError,
|
||||||
|
InvalidTOTPError: InvalidTOTPError,
|
||||||
}
|
}
|
||||||
|
|
||||||
function LdapSearchError(message) {
|
function LdapSearchError(message) {
|
||||||
|
@ -36,3 +37,9 @@ function AuthenticationRegulationError(message) {
|
||||||
this.message = (message || "");
|
this.message = (message || "");
|
||||||
}
|
}
|
||||||
AuthenticationRegulationError.prototype = Object.create(Error.prototype);
|
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 reset_password = require('./routes/reset_password');
|
||||||
var verify = require('./routes/verify');
|
var verify = require('./routes/verify');
|
||||||
var u2f_register_handler = require('./routes/u2f_register_handler');
|
var u2f_register_handler = require('./routes/u2f_register_handler');
|
||||||
|
var totp_register = require('./routes/totp_register');
|
||||||
var objectPath = require('object-path');
|
var objectPath = require('object-path');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@ -13,7 +14,8 @@ module.exports = {
|
||||||
first_factor: first_factor,
|
first_factor: first_factor,
|
||||||
second_factor: second_factor,
|
second_factor: second_factor,
|
||||||
reset_password: reset_password,
|
reset_password: reset_password,
|
||||||
u2f_register: u2f_register_handler
|
u2f_register: u2f_register_handler,
|
||||||
|
totp_register: totp_register,
|
||||||
}
|
}
|
||||||
|
|
||||||
function serveLogin(req, res) {
|
function serveLogin(req, res) {
|
||||||
|
|
|
@ -3,31 +3,48 @@ module.exports = totp;
|
||||||
|
|
||||||
var totp = require('../totp');
|
var totp = require('../totp');
|
||||||
var objectPath = require('object-path');
|
var objectPath = require('object-path');
|
||||||
|
var exceptions = require('../../../src/lib/exceptions');
|
||||||
|
|
||||||
var UNAUTHORIZED_MESSAGE = 'Unauthorized access';
|
var UNAUTHORIZED_MESSAGE = 'Unauthorized access';
|
||||||
|
|
||||||
function replyWithUnauthorized(res) {
|
|
||||||
res.status(401);
|
|
||||||
res.send();
|
|
||||||
}
|
|
||||||
|
|
||||||
function totp(req, res) {
|
function totp(req, res) {
|
||||||
if(!objectPath.has(req, 'session.auth_session.second_factor')) {
|
var logger = req.app.get('logger');
|
||||||
replyWithUnauthorized(res);
|
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 token = req.body.token;
|
||||||
|
|
||||||
var totp_engine = req.app.get('totp engine');
|
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() {
|
.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.status(204);
|
||||||
res.send();
|
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) {
|
.catch(function(err) {
|
||||||
console.error(err);
|
logger.error('POST 2ndfactor totp: Internal error %s', err);
|
||||||
replyWithUnauthorized(res);
|
res.status(500);
|
||||||
|
res.send('Internal error');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
92
src/lib/routes/totp_register.js
Normal file
92
src/lib/routes/totp_register.js
Normal file
|
@ -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 + '/login', routes.login);
|
||||||
app.get (base_endpoint + '/logout', routes.logout);
|
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 + '/u2f-register', routes.u2f_register.icheck_interface);
|
||||||
identity_check(app, base_endpoint + '/reset-password', routes.reset_password.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
|
// Reset the password
|
||||||
app.post (base_endpoint + '/new-password', routes.reset_password.post);
|
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
|
// verify authentication
|
||||||
app.get (base_endpoint + '/verify', routes.verify);
|
app.get (base_endpoint + '/verify', routes.verify);
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,8 @@ function UserDataStore(DataStore, options) {
|
||||||
create_collection('identity_check_tokens', options, DataStore);
|
create_collection('identity_check_tokens', options, DataStore);
|
||||||
this._authentication_traces_collection =
|
this._authentication_traces_collection =
|
||||||
create_collection('authentication_traces', options, DataStore);
|
create_collection('authentication_traces', options, DataStore);
|
||||||
|
this._totp_secret_collection =
|
||||||
|
create_collection('totp_secrets', options, DataStore);
|
||||||
}
|
}
|
||||||
|
|
||||||
function create_collection(name, 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);
|
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;
|
width:300px;
|
||||||
height: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 {
|
input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -39,13 +39,26 @@ function onTotpSignButtonClicked() {
|
||||||
var token = $('#totp-token').val();
|
var token = $('#totp-token').val();
|
||||||
validateSecondFactorTotp(token, function(err) {
|
validateSecondFactorTotp(token, function(err) {
|
||||||
if(err) {
|
if(err) {
|
||||||
onSecondFactorTotpFailure();
|
onSecondFactorTotpFailure(err.responseText);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onSecondFactorTotpSuccess();
|
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() {
|
function onU2fSignButtonClicked() {
|
||||||
startU2fAuthentication(function(err) {
|
startU2fAuthentication(function(err) {
|
||||||
if(err) {
|
if(err) {
|
||||||
|
@ -174,8 +187,8 @@ function onSecondFactorTotpSuccess() {
|
||||||
onAuthenticationSuccess();
|
onAuthenticationSuccess();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSecondFactorTotpFailure() {
|
function onSecondFactorTotpFailure(err) {
|
||||||
$.notify('Wrong TOTP token', 'error');
|
$.notify('Error while validating TOTP token. Cause: ' + err, 'error');
|
||||||
}
|
}
|
||||||
|
|
||||||
function onU2fAuthenticationSuccess() {
|
function onU2fAuthenticationSuccess() {
|
||||||
|
@ -216,6 +229,10 @@ function setupTotpSignButton() {
|
||||||
setupEnterKeypressListener('#totp', onTotpSignButtonClicked);
|
setupEnterKeypressListener('#totp', onTotpSignButtonClicked);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setupTotpRegisterButton() {
|
||||||
|
$('#second-factor #totp-register-button').on('click', onTotpRegisterButtonClicked);
|
||||||
|
}
|
||||||
|
|
||||||
function setupU2fSignButton() {
|
function setupU2fSignButton() {
|
||||||
$('#second-factor #u2f-sign-button').on('click', onU2fSignButtonClicked);
|
$('#second-factor #u2f-sign-button').on('click', onU2fSignButtonClicked);
|
||||||
setupEnterKeypressListener('#u2f', onU2fSignButtonClicked);
|
setupEnterKeypressListener('#u2f', onU2fSignButtonClicked);
|
||||||
|
@ -241,6 +258,7 @@ function enterSecondFactor() {
|
||||||
showSecondFactorLayout();
|
showSecondFactorLayout();
|
||||||
cleanupFirstFactorLoginButton();
|
cleanupFirstFactorLoginButton();
|
||||||
setupTotpSignButton();
|
setupTotpSignButton();
|
||||||
|
setupTotpRegisterButton();
|
||||||
setupU2fSignButton();
|
setupU2fSignButton();
|
||||||
setupU2fRegistrationButton();
|
setupU2fRegistrationButton();
|
||||||
}
|
}
|
||||||
|
|
28
src/public_html/js/totp-register.js
Normal file
28
src/public_html/js/totp-register.js
Normal file
|
@ -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>
|
<h2>Time-Based One-Time Password</h2>
|
||||||
<input type="text" name="totp-token" id="totp-token" placeholder="Validation token" />
|
<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-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>
|
||||||
<div id="u2f">
|
<div id="u2f">
|
||||||
<h2>FIDO Universal 2nd Factor</h2>
|
<h2>FIDO Universal 2nd Factor</h2>
|
||||||
|
|
18
src/views/totp-register.ejs
Normal file
18
src/views/totp-register.ejs
Normal file
|
@ -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) {
|
function execute_totp(jar, token) {
|
||||||
return request.postAsync({
|
return request.postAsync({
|
||||||
url: BASE_URL + '/authentication/2ndfactor/totp',
|
url: BASE_URL + '/authentication/2ndfactor/totp',
|
||||||
|
@ -136,6 +167,7 @@ module.exports = function(port) {
|
||||||
first_factor: execute_first_factor,
|
first_factor: execute_first_factor,
|
||||||
failing_first_factor: execute_failing_first_factor,
|
failing_first_factor: execute_failing_first_factor,
|
||||||
totp: execute_totp,
|
totp: execute_totp,
|
||||||
|
register_totp: execute_register_totp,
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,12 @@ var totp = require('../../../src/lib/routes/totp');
|
||||||
var Promise = require('bluebird');
|
var Promise = require('bluebird');
|
||||||
var sinon = require('sinon');
|
var sinon = require('sinon');
|
||||||
var assert = require('assert');
|
var assert = require('assert');
|
||||||
|
var winston = require('winston');
|
||||||
|
|
||||||
describe('test totp route', function() {
|
describe('test totp route', function() {
|
||||||
var req, res;
|
var req, res;
|
||||||
var totp_engine;
|
var totp_engine;
|
||||||
|
var user_data_store;
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
var app_get = sinon.stub();
|
var app_get = sinon.stub();
|
||||||
|
@ -19,6 +21,7 @@ describe('test totp route', function() {
|
||||||
},
|
},
|
||||||
session: {
|
session: {
|
||||||
auth_session: {
|
auth_session: {
|
||||||
|
userid: 'user',
|
||||||
first_factor: false,
|
first_factor: false,
|
||||||
second_factor: false
|
second_factor: false
|
||||||
}
|
}
|
||||||
|
@ -33,46 +36,52 @@ describe('test totp route', function() {
|
||||||
totp_engine = {
|
totp_engine = {
|
||||||
totp: sinon.stub()
|
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('totp engine').returns(totp_engine);
|
||||||
app_get.withArgs('config').returns(config);
|
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() {
|
it('should send status code 204 when totp is valid', function(done) {
|
||||||
return new Promise(function(resolve, reject) {
|
|
||||||
totp_engine.totp.returns('abc');
|
totp_engine.totp.returns('abc');
|
||||||
res.send = sinon.spy(function() {
|
res.send = sinon.spy(function() {
|
||||||
// Second factor passed
|
// Second factor passed
|
||||||
assert.equal(true, req.session.auth_session.second_factor)
|
assert.equal(true, req.session.auth_session.second_factor)
|
||||||
assert.equal(204, res.status.getCall(0).args[0]);
|
assert.equal(204, res.status.getCall(0).args[0]);
|
||||||
resolve();
|
done();
|
||||||
});
|
});
|
||||||
totp(req, res);
|
totp(req, res);
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should send status code 401 when totp is not valid', function() {
|
it('should send status code 401 when totp is not valid', function(done) {
|
||||||
return new Promise(function(resolve, reject) {
|
|
||||||
totp_engine.totp.returns('bad_token');
|
totp_engine.totp.returns('bad_token');
|
||||||
res.send = sinon.spy(function() {
|
res.send = sinon.spy(function() {
|
||||||
assert.equal(false, req.session.auth_session.second_factor)
|
assert.equal(false, req.session.auth_session.second_factor)
|
||||||
assert.equal(401, res.status.getCall(0).args[0]);
|
assert.equal(401, res.status.getCall(0).args[0]);
|
||||||
resolve();
|
done();
|
||||||
});
|
});
|
||||||
totp(req, res);
|
totp(req, res);
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should send status code 401 when session has not been initiated', function() {
|
it('should send status code 401 when session has not been initiated', function(done) {
|
||||||
return new Promise(function(resolve, reject) {
|
|
||||||
totp_engine.totp.returns('abc');
|
totp_engine.totp.returns('abc');
|
||||||
res.send = sinon.spy(function() {
|
res.send = sinon.spy(function() {
|
||||||
assert.equal(401, res.status.getCall(0).args[0]);
|
assert.equal(403, res.status.getCall(0).args[0]);
|
||||||
resolve();
|
done();
|
||||||
});
|
});
|
||||||
req.session = {};
|
req.session = {};
|
||||||
totp(req, res);
|
totp(req, res);
|
||||||
})
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
130
test/unitary/routes/test_totp_register.js
Normal file
130
test/unitary/routes/test_totp_register.js
Normal file
|
@ -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() {
|
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();
|
var j = request.jar();
|
||||||
return requests.login(j)
|
return requests.login(j)
|
||||||
.then(function(res) {
|
.then(function(res) {
|
||||||
|
@ -231,6 +227,14 @@ describe('test the server', function() {
|
||||||
})
|
})
|
||||||
.then(function(res) {
|
.then(function(res) {
|
||||||
assert.equal(res.statusCode, 204, 'first factor failed');
|
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);
|
return requests.totp(j, real_token);
|
||||||
})
|
})
|
||||||
.then(function(res) {
|
.then(function(res) {
|
||||||
|
|
65
test/unitary/user_data_store/test_totp_secret.js
Normal file
65
test/unitary/user_data_store/test_totp_secret.js
Normal file
|
@ -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
Block a user