Better naming, start on temp tokens table, etc.
This commit is contained in:
parent
29e0c8f790
commit
4158b07ad0
|
@ -79,7 +79,7 @@ exports.getModule = class AbracadabraModule extends MenuModule {
|
||||||
|
|
||||||
/*
|
/*
|
||||||
:TODO:
|
:TODO:
|
||||||
* disconnecting wile door is open leaves dosemu
|
* disconnecting while door is open leaves dosemu
|
||||||
* http://bbslink.net/sysop.php support
|
* http://bbslink.net/sysop.php support
|
||||||
* Font support ala all other menus... or does this just work?
|
* Font support ala all other menus... or does this just work?
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -203,6 +203,22 @@ const DB_INIT_TABLE = {
|
||||||
);`
|
);`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Table for temporary tokens, generally used for e.g. 'outside'
|
||||||
|
// access such as email links.
|
||||||
|
// Examples: PW reset, enabling of 2FA/OTP, etc.
|
||||||
|
//
|
||||||
|
dbs.user.run(
|
||||||
|
`CREATE TABLE IF NOT EXISTS user_temporary_token (
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
token VARCHAR NOT NULL,
|
||||||
|
timestamp DATETIME NOT NULL,
|
||||||
|
purpose VARCHAR NOT NULL,
|
||||||
|
UNIQUE(user_id, token),
|
||||||
|
FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE
|
||||||
|
);`
|
||||||
|
);
|
||||||
|
|
||||||
return cb(null);
|
return cb(null);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -9,11 +9,16 @@ const {
|
||||||
otpFromType,
|
otpFromType,
|
||||||
createQRCode,
|
createQRCode,
|
||||||
} = require('./user_2fa_otp.js');
|
} = require('./user_2fa_otp.js');
|
||||||
|
const { Errors } = require('./enig_error.js');
|
||||||
|
const { sendMail } = require('./email.js');
|
||||||
|
const { getServer } = require('./listening_server.js');
|
||||||
|
const WebServerPackageName = require('./servers/content/web.js').moduleInfo.packageName;
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const iconv = require('iconv-lite');
|
const iconv = require('iconv-lite');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
exports.moduleInfo = {
|
exports.moduleInfo = {
|
||||||
name : 'User 2FA/OTP Configuration',
|
name : 'User 2FA/OTP Configuration',
|
||||||
|
@ -26,10 +31,10 @@ const FormIds = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const MciViewIds = {
|
const MciViewIds = {
|
||||||
enableToggle : 1,
|
enableToggle : 1,
|
||||||
typeSelection : 2,
|
otpType : 2,
|
||||||
submission : 3,
|
submit : 3,
|
||||||
infoText : 4,
|
infoText : 4,
|
||||||
|
|
||||||
customRangeStart : 10, // 10+ = customs
|
customRangeStart : 10, // 10+ = customs
|
||||||
};
|
};
|
||||||
|
@ -53,10 +58,22 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule {
|
||||||
},
|
},
|
||||||
showBackupCodes : (formData, extraArgs, cb) => {
|
showBackupCodes : (formData, extraArgs, cb) => {
|
||||||
return this.showBackupCodes(cb);
|
return this.showBackupCodes(cb);
|
||||||
|
},
|
||||||
|
saveChanges : (formData, extraArgs, cb) => {
|
||||||
|
return this.saveChanges(formData, cb);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initSequence() {
|
||||||
|
this.webServer = getServer(WebServerPackageName);
|
||||||
|
if(!this.webServer || !this.webServer.instance.isEnabled()) {
|
||||||
|
this.client.log.warn('User 2FA/OTP configuration requires the web server to be enabled!');
|
||||||
|
return this.prevMenu( () => { /* dummy */ } );
|
||||||
|
}
|
||||||
|
return super.initSequence();
|
||||||
|
}
|
||||||
|
|
||||||
mciReady(mciData, cb) {
|
mciReady(mciData, cb) {
|
||||||
super.mciReady(mciData, err => {
|
super.mciReady(mciData, err => {
|
||||||
if(err) {
|
if(err) {
|
||||||
|
@ -71,8 +88,8 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule {
|
||||||
(callback) => {
|
(callback) => {
|
||||||
const requiredCodes = [
|
const requiredCodes = [
|
||||||
MciViewIds.enableToggle,
|
MciViewIds.enableToggle,
|
||||||
MciViewIds.typeSelection,
|
MciViewIds.otpType,
|
||||||
MciViewIds.submission,
|
MciViewIds.submit,
|
||||||
];
|
];
|
||||||
return this.validateMCIByViewIds('menu', requiredCodes, callback);
|
return this.validateMCIByViewIds('menu', requiredCodes, callback);
|
||||||
},
|
},
|
||||||
|
@ -86,19 +103,19 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule {
|
||||||
return this.enableToggleUpdate(idx);
|
return this.enableToggleUpdate(idx);
|
||||||
});
|
});
|
||||||
|
|
||||||
const typeSelectionView = this.getView('menu', MciViewIds.typeSelection);
|
const otpTypeView = this.getView('menu', MciViewIds.otpType);
|
||||||
initialIndex = this.typeSelectionIndexFromUserOTPType();
|
initialIndex = this.otpTypeIndexFromUserOTPType();
|
||||||
typeSelectionView.setFocusItemIndex(initialIndex);
|
otpTypeView.setFocusItemIndex(initialIndex);
|
||||||
|
|
||||||
typeSelectionView.on('index update', idx => {
|
otpTypeView.on('index update', idx => {
|
||||||
return this.typeSelectionUpdate(idx);
|
return this.otpTypeUpdate(idx);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.viewControllers.menu.on('return', view => {
|
this.viewControllers.menu.on('return', view => {
|
||||||
if(view === enableToggleView) {
|
if(view === enableToggleView) {
|
||||||
return this.enableToggleUpdate(enableToggleView.focusedItemIndex);
|
return this.enableToggleUpdate(enableToggleView.focusedItemIndex);
|
||||||
} else if (view === typeSelectionView) {
|
} else if (view === otpTypeView) {
|
||||||
return this.typeSelectionUpdate(typeSelectionView.focusedItemIndex);
|
return this.otpTypeUpdate(otpTypeView.focusedItemIndex);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -169,8 +186,74 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule {
|
||||||
return this.displayDetails(info, cb);
|
return this.displayDetails(info, cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
saveChanges(formData, cb) {
|
||||||
|
const enabled = 1 === _.get(formData, 'value.enableToggle', 0);
|
||||||
|
return enabled ? this.saveChangesEnable(formData, cb) : this.saveChangesDisable(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveChangesEnable(formData, cb) {
|
||||||
|
const otpTypeProp = this.otpTypeFromOTPTypeIndex(_.get(formData, 'value.otpType'));
|
||||||
|
|
||||||
|
// sanity check
|
||||||
|
if(!otpFromType(otpTypeProp)) {
|
||||||
|
return cb(Errors.Invalid('Cannot convert selected index to valid OTP type'));
|
||||||
|
}
|
||||||
|
|
||||||
|
async.waterfall(
|
||||||
|
[
|
||||||
|
(callback) => {
|
||||||
|
return this.removeUserOTPProperties(callback);
|
||||||
|
},
|
||||||
|
(callback) => {
|
||||||
|
return crypto.randomBytes(256, callback);
|
||||||
|
},
|
||||||
|
(token, callback) => {
|
||||||
|
// :TODO: consider temporary tokens table - this has become semi-common
|
||||||
|
// token | timestamp | token_type |
|
||||||
|
// abc | ISO | '2fa_otp_register'
|
||||||
|
token = token.toString('hex');
|
||||||
|
this.client.user.persistProperty(UserProps.AuthFactor2OTPEnableToken, token, err => {
|
||||||
|
return callback(err, token);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
(token, callback) => {
|
||||||
|
const resetUrl = this.webServer.instance.buildUrl(
|
||||||
|
`/enable_2fa_otp?token=&otpType=${otpTypeProp}&token=${token}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// clear any existing (e.g. same as disable) -> send activation email
|
||||||
|
|
||||||
|
return callback(null);
|
||||||
|
}
|
||||||
|
],
|
||||||
|
err => {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeUserOTPProperties(cb) {
|
||||||
|
const props = [
|
||||||
|
UserProps.AuthFactor2OTP,
|
||||||
|
UserProps.AuthFactor2OTPSecret,
|
||||||
|
UserProps.AuthFactor2OTPBackupCodes,
|
||||||
|
];
|
||||||
|
return this.client.user.removeProperties(props, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveChangesDisable(cb) {
|
||||||
|
this.removeUserOTPProperties( err => {
|
||||||
|
if(err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// :TODO: show "saved+disabled" art/message -> prevMenu
|
||||||
|
return cb(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
isOTPEnabledForUser() {
|
isOTPEnabledForUser() {
|
||||||
return this.typeSelectionIndexFromUserOTPType(-1) != -1;
|
return this.otpTypeIndexFromUserOTPType(-1) != -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
getInfoText(key) {
|
getInfoText(key) {
|
||||||
|
@ -185,7 +268,7 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule {
|
||||||
this.updateCustomViewTextsWithFilter('menu', MciViewIds.customRangeStart, { infoText : this.getInfoText(key) } );
|
this.updateCustomViewTextsWithFilter('menu', MciViewIds.customRangeStart, { infoText : this.getInfoText(key) } );
|
||||||
}
|
}
|
||||||
|
|
||||||
typeSelectionIndexFromUserOTPType(defaultIndex = 0) {
|
otpTypeIndexFromUserOTPType(defaultIndex = 0) {
|
||||||
const type = this.client.user.getProperty(UserProps.AuthFactor2OTP);
|
const type = this.client.user.getProperty(UserProps.AuthFactor2OTP);
|
||||||
return {
|
return {
|
||||||
[ OTPTypes.RFC6238_TOTP ] : 0,
|
[ OTPTypes.RFC6238_TOTP ] : 0,
|
||||||
|
@ -194,7 +277,7 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule {
|
||||||
}[type] || defaultIndex;
|
}[type] || defaultIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
otpTypeFromTypeSelectionIndex(idx) {
|
otpTypeFromOTPTypeIndex(idx) {
|
||||||
return {
|
return {
|
||||||
0 : OTPTypes.RFC6238_TOTP,
|
0 : OTPTypes.RFC6238_TOTP,
|
||||||
1 : OTPTypes.RFC4266_HOTP,
|
1 : OTPTypes.RFC4266_HOTP,
|
||||||
|
@ -202,8 +285,8 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule {
|
||||||
}[idx];
|
}[idx];
|
||||||
}
|
}
|
||||||
|
|
||||||
typeSelectionUpdate(idx) {
|
otpTypeUpdate(idx) {
|
||||||
const key = this.otpTypeFromTypeSelectionIndex(idx);
|
const key = this.otpTypeFromOTPTypeIndex(idx);
|
||||||
this.updateCustomViewTextsWithFilter('menu', MciViewIds.customRangeStart, { infoText : this.getInfoText(key) } );
|
this.updateCustomViewTextsWithFilter('menu', MciViewIds.customRangeStart, { infoText : this.getInfoText(key) } );
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -63,5 +63,6 @@ module.exports = {
|
||||||
AuthFactor2OTP : 'auth_factor2_otp', // If present, OTP type for 2FA. See OTPTypes
|
AuthFactor2OTP : 'auth_factor2_otp', // If present, OTP type for 2FA. See OTPTypes
|
||||||
AuthFactor2OTPSecret : 'auth_factor2_otp_secret', // Secret used in conjunction with OTP 2FA
|
AuthFactor2OTPSecret : 'auth_factor2_otp_secret', // Secret used in conjunction with OTP 2FA
|
||||||
AuthFactor2OTPBackupCodes : 'auth_factor2_otp_backup', // JSON array of backup codes
|
AuthFactor2OTPBackupCodes : 'auth_factor2_otp_backup', // JSON array of backup codes
|
||||||
|
AuthFactor2OTPEnableToken : 'auth_factor2_otp_enable_token',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue