diff --git a/MeshCentralServer.njsproj b/MeshCentralServer.njsproj index 65a75ba5..b7cbded0 100644 --- a/MeshCentralServer.njsproj +++ b/MeshCentralServer.njsproj @@ -99,6 +99,7 @@ + diff --git a/firebase.js b/firebase.js index 807dc73a..2bbdabb9 100644 --- a/firebase.js +++ b/firebase.js @@ -90,8 +90,16 @@ module.exports.CreateFirebase = function (parent, senderid, serverkey) { //var payload = { notification: { title: command.title, body: command.msg }, data: { url: obj.msgurl } }; //var options = { priority: 'High', timeToLive: 5 * 60 }; // TTL: 5 minutes, priority 'Normal' or 'High' - // Send an outbound push notification obj.sendToDevice = function (node, payload, options, func) { + if (typeof node == 'string') { + parent.db.Get(node, function (err, docs) { if ((err == null) && (docs != null) && (docs.length == 1)) { obj.sendToDeviceEx(docs[0], payload, options, func); } else { func(0, 'error'); } }) + } else { + obj.sendToDeviceEx(node, payload, options, func); + } + } + + // Send an outbound push notification + obj.sendToDeviceEx = function (node, payload, options, func) { parent.debug('email', 'Firebase-sendToDevice'); if ((node == null) || (typeof node.pmt != 'string')) return; obj.log('sendToDevice, node:' + node._id + ', payload: ' + JSON.stringify(payload) + ', options: ' + JSON.stringify(options)); @@ -270,6 +278,14 @@ module.exports.CreateFirebaseRelay = function (parent, url, key) { } obj.sendToDevice = function (node, payload, options, func) { + if (typeof node == 'string') { + parent.db.Get(node, function (err, docs) { if ((err == null) && (docs != null) && (docs.length == 1)) { obj.sendToDeviceEx(docs[0], payload, options, func); } else { func(0, 'error'); } }) + } else { + obj.sendToDeviceEx(node, payload, options, func); + } + } + + obj.sendToDeviceEx = function (node, payload, options, func) { parent.debug('email', 'Firebase-sendToDevice-webSocket'); if ((node == null) || (typeof node.pmt != 'string')) { func(0, 'error'); return; } obj.log('sendToDevice, node:' + node._id + ', payload: ' + JSON.stringify(payload) + ', options: ' + JSON.stringify(options)); @@ -298,7 +314,16 @@ module.exports.CreateFirebaseRelay = function (parent, url, key) { } else if (relayUrl.protocol == 'https:') { // Send an outbound push notification using an HTTPS POST obj.pushOnly = true; + obj.sendToDevice = function (node, payload, options, func) { + if (typeof node == 'string') { + parent.db.Get(node, function (err, docs) { if ((err == null) && (docs != null) && (docs.length == 1)) { obj.sendToDeviceEx(docs[0], payload, options, func); } else { func(0, 'error'); } }) + } else { + obj.sendToDeviceEx(node, payload, options, func); + } + } + + obj.sendToDeviceEx = function (node, payload, options, func) { parent.debug('email', 'Firebase-sendToDevice-httpPost'); if ((node == null) || (typeof node.pmt != 'string')) return; diff --git a/meshagent.js b/meshagent.js index e4886519..38044ce5 100644 --- a/meshagent.js +++ b/meshagent.js @@ -1556,6 +1556,7 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) { // Complete 2FA checking if (authCookie.a == 'checkAuth') { // TODO + console.log(authCookie); } break; diff --git a/public/images/login/2fa-push-48.png b/public/images/login/2fa-push-48.png new file mode 100644 index 00000000..e962d533 Binary files /dev/null and b/public/images/login/2fa-push-48.png differ diff --git a/public/images/login/2fa-push-96.png b/public/images/login/2fa-push-96.png new file mode 100644 index 00000000..9081f5d3 Binary files /dev/null and b/public/images/login/2fa-push-96.png differ diff --git a/public/images/login/push-150.png b/public/images/login/push-150.png new file mode 100644 index 00000000..b1dfccb4 Binary files /dev/null and b/public/images/login/push-150.png differ diff --git a/public/images/login/push-300.png b/public/images/login/push-300.png new file mode 100644 index 00000000..5bfd952f Binary files /dev/null and b/public/images/login/push-300.png differ diff --git a/public/styles/style.css b/public/styles/style.css index 7bf0802b..5a1c77ac 100644 --- a/public/styles/style.css +++ b/public/styles/style.css @@ -394,7 +394,7 @@ body { color: blue; } -#loginpanel, #createpanel, #resetpanel, #tokenpanel, #resettokenpanel, #resetpasswordpanel, #resetpasswordpanel, #checkemailpanel { +#loginpanel, #createpanel, #resetpanel, #tokenpanel, #resettokenpanel, #resetpasswordpanel, #resetpasswordpanel, #checkemailpanel, #waitpushpanel { display: inline-block; margin: 0; background-color: #979797; diff --git a/views/default.handlebars b/views/default.handlebars index cf08702d..7f67ab08 100644 --- a/views/default.handlebars +++ b/views/default.handlebars @@ -2032,6 +2032,7 @@ QV('authKeySetupCheck', userinfo.otphkeys > 0); QV('authPushAuthDevCheck', (userinfo.otpdev > 0) && ((features2 & 2) != 0)); QV('authCodesSetupCheck', userinfo.otpkeys > 0); + QV('managePushAuthDev', (features2 & 2) && (count2factoraAuths() > 0)); mainUpdate(4 + 128 + 4096); // Check if none or at least 2 factors are enabled. diff --git a/views/login2.handlebars b/views/login2.handlebars index 2bc929f6..75a24e62 100644 --- a/views/login2.handlebars +++ b/views/login2.handlebars @@ -186,6 +186,7 @@ + @@ -219,6 +220,7 @@ + @@ -281,6 +283,22 @@ +
@@ -330,6 +348,7 @@ var publicKeyCredentialRequestOptions = null; var otpemail = (decodeURIComponent('{{{otpemail}}}') === 'true'); var otpsms = (decodeURIComponent('{{{otpsms}}}') === 'true'); + var otppush = (decodeURIComponent('{{{otppush}}}') === 'true'); var twoFactorCookieDays = parseInt('{{{twoFactorCookieDays}}}'); var authStrategies = '{{{authStrategies}}}'.split(','); @@ -342,15 +361,16 @@ // Display the right server message var i; var messageid = parseInt('{{{messageid}}}'); - var okmessages = ['', "If valid, reset mail sent.", "Email sent.", "Email verification required, check your mailbox and click the confirmation link.", "SMS sent."]; - var failmessages = ["Unable to create account.", "Account limit reached.", "Existing account with this email address.", "Invalid account creation token.", "Username already exists.", "Password rejected, use a different one.", "Invalid email.", "Account not found.", "Invalid token, try again.", "Unable to sent email.", "Account locked.", "Access denied.", "Login failed, check username and password.", "Password change requested.", "IP address blocked, try again later.", "Server under maintenance."]; + var okmessages = ['', "If valid, reset mail sent.", "Email sent.", "Email verification required, check your mailbox and click the confirmation link.", "SMS sent.", "Notification sent, {0}."]; + var failmessages = ["Unable to create account.", "Account limit reached.", "Existing account with this email address.", "Invalid account creation token.", "Username already exists.", "Password rejected, use a different one.", "Invalid email.", "Account not found.", "Invalid token, try again.", "Unable to sent email.", "Account locked.", "Access denied.", "Login failed, check username and password.", "Password change requested.", "IP address blocked, try again later.", "Server under maintenance.", "Unable to send device notification."]; if (messageid > 0) { var msg = ''; if ((messageid < 100) && (messageid < okmessages.length)) { msg = okmessages[messageid]; } else if ((messageid >= 100) && ((messageid - 100) < failmessages.length)) { msg = failmessages[messageid - 100]; } + if (messageid == 5) { msg = format(msg, passhint); } if (msg != '') { if (messageid >= 100) { msg = ('' + msg + '

'); } else { msg = ('' + msg + '

'); } - for (i = 1; i < 8; i++) { QH('message' + i, msg); } + for (i = 1; i < 9; i++) { QH('message' + i, msg); } } } @@ -369,8 +389,11 @@ if (twoFactorCookieDays > 0) { QV('tokenInputRememberLabel', true); QH('tokenInputRememberSpan', format("Remember this device for {0} days.", twoFactorCookieDays)); + QV('tokenInputRememberLabel2', true); + QH('tokenInputRememberSpan2', format("Remember this device for {0} days.", twoFactorCookieDays)); } else { QV('tokenInputRememberLabel', false); + QV('tokenInputRememberLabel2', false); } // If URL arguments are provided, add them to form posts @@ -443,10 +466,12 @@ var twofakey = (hardwareKeyChallenge != null) && (hardwareKeyChallenge.type == 'webAuthn'); var emailkey = otpemail && (messageid != 2) && (messageid != 4); var smskey = otpsms && (messageid != 2) && (messageid != 4); + var pushkey = otppush && (messageid != 2) && (messageid != 4); QV('securityKeyButton', twofakey); QV('emailKeyButton', emailkey); QV('smsKeyButton', smskey); - QV('2farow', twofakey || emailkey || smskey); + QV('pushKeyButton', pushkey); + QV('2farow', twofakey || emailkey || smskey || pushkey); } if (loginMode == '5') { @@ -454,10 +479,12 @@ var twofakey = (hardwareKeyChallenge != null) && (hardwareKeyChallenge.type == 'webAuthn'); var emailkey = otpemail && (messageid != 2) && (messageid != 4); var smskey = otpsms && (messageid != 2) && (messageid != 4); + var pushkey = otppush && (messageid != 2) && (messageid != 4); QV('securityKeyButton2', twofakey); QV('emailKeyButton2', emailkey); QV('smsKeyButton2', smskey); - QV('2farow2', twofakey || emailkey || smskey); + QV('pushKeyButton', pushkey); + QV('2farow2', twofakey || emailkey || smskey || pushkey); } /* @@ -565,6 +592,18 @@ } } + function usePushToken(panelAction) { + if (panelAction == 1) { + Q('hwtokenInput').value = '**push**'; + QE('tokenOkButton', true); + Q('tokenOkButton').click(); + } else if (panelAction == 2) { + Q('resetHwtokenInput').value = '**push**'; + QE('resetTokenOkButton', true); + Q('resetTokenOkButton').click(); + } + } + function showPassHint(e) { messagebox("Password Hint", passhint); haltEvent(e); @@ -595,6 +634,7 @@ QV('resettokenpanel', x == 5); QV('resetpasswordpanel', x == 6); QV('checkemailpanel', x == 7); + QV('waitpushpanel', x == 8); if (x == 1) { Q('username').focus(); } if (x == 2) { if (features & 0x200000) { Q('aemail').focus(); } else { Q('ausername').focus(); } } // Email is username if (x == 3) { Q('remail').focus(); } diff --git a/webserver.js b/webserver.js index dbb7155c..70a9607e 100644 --- a/webserver.js +++ b/webserver.js @@ -924,6 +924,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.email != null) && (user.emailVerified == true) && (user.otpekey != null)); var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null)); + var push2fa = ((parent.firebase != null) && (user.otpdev != null)); // Check if this user has 2-step login active if ((req.session.loginmode != '6') && checkUserOneTimePasswordRequired(domain, user, req)) { @@ -951,6 +952,29 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { return; } + if ((req.body.hwtoken == '**push**') && push2fa) { + // Cause push notification to device + const logincode = obj.common.zeroPad(getRandomSixDigitInteger(), 6); + const code = Buffer.from(logincode).toString('base64'); + const authCookie = parent.encodeCookie({ a: 'checkAuth', c: code, u: user._id, n: user.otpdev }); + var payload = { notification: { title: "MeshCentral", body: user.name + " authentication" }, data: { url: '2fa://auth?code=' + code + '&c=' + authCookie } }; + var options = { priority: 'High', timeToLive: 60 }; // TTL: 1 minute + parent.firebase.sendToDevice(user.otpdev, payload, options, function (id, err, errdesc) { + if (err == null) { + // Request that the login page wait for device auth + req.session.messageid = 5; // "Notification sent." message + req.session.passhint = logincode; + req.session.loginmode = '8'; + } else { + // Indicate the push notification failed + req.session.messageid = 116; // "Unable to send device notification." message + req.session.loginmode = '4'; + } + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } + }); + return; + } + checkUserOneTimePassword(req, domain, user, req.body.token, req.body.hwtoken, function (result) { if (result == false) { var randomWaitTime = 0; @@ -973,6 +997,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { req.session.loginmode = '4'; req.session.tokenemail = ((user.email != null) && (user.emailVerified == true) && (domain.mailserver != null) && (user.otpekey != null)); req.session.tokensms = ((user.phone != null) && (parent.smsserver != null)); + req.session.tokenpush = ((user.otpdev != null) && (parent.firebase != null)); req.session.tokenuserid = userid; req.session.tokenusername = xusername; req.session.tokenpassword = xpassword; @@ -1097,6 +1122,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { delete req.session.tokenpassword; delete req.session.tokenemail; delete req.session.tokensms; + delete req.session.tokenpush; delete req.session.messageid; delete req.session.passhint; delete req.session.cuserid; @@ -1301,6 +1327,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { delete req.session.resettokenpassword; delete req.session.tokenemail; delete req.session.tokensms; + delete req.session.tokenpush; delete req.session.messageid; delete req.session.passhint; delete req.session.cuserid; @@ -1382,6 +1409,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { delete req.session.resettokenpassword; delete req.session.tokenemail; delete req.session.tokensms; + delete req.session.tokenpush; delete req.session.messageid; delete req.session.passhint; delete req.session.cuserid; @@ -2638,7 +2666,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { var passhint = null, msgid = 0; if (req.session != null) { msgid = req.session.messageid; - if ((loginmode == '7') || ((domain.passwordrequirements != null) && (domain.passwordrequirements.hint === true))) { passhint = EscapeHtml(req.session.passhint); } + if ((msgid == 5) || (loginmode == '7') || ((domain.passwordrequirements != null) && (domain.passwordrequirements.hint === true))) { passhint = EscapeHtml(req.session.passhint); } delete req.session.messageid; delete req.session.passhint; } @@ -2658,6 +2686,8 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.email2factor == false)) { otpemail = false; } var otpsms = (parent.smsserver != null) && (req.session != null) && (req.session.tokensms == true); if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.sms2factor == false)) { otpsms = false; } + var otppush = (parent.firebase != null) && (req.session != null) && (req.session.tokenpush == true); + //if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.push2factor == false)) { otppush = false; } // See if we support two-factor trusted cookies var twoFactorCookieDays = 30; @@ -2704,6 +2734,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { hwstate: hwstate, otpemail: otpemail, otpsms: otpsms, + otppush: otppush, twoFactorCookieDays: twoFactorCookieDays, authStrategies: authStrategies.join(','), loginpicture: (typeof domain.loginpicture == 'string') @@ -5844,7 +5875,8 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { // Figure out if email 2FA is allowed var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.otpekey != null)); var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null)); - if ((typeof command.token != 'string') || (command.token == '**email**') || (command.token == '**sms**')) { + //var push2fa = ((parent.firebase != null) && (user.otpdev != null)); + if ((typeof command.token != 'string') || (command.token == '**email**') || (command.token == '**sms**')/* || (command.token == '**push**')*/) { if ((command.token == '**email**') && (email2fa == true)) { // Cause a token to be sent to the user's registered email user.otpekey = { k: obj.common.zeroPad(getRandomEightDigitInteger(), 8), d: Date.now() }; @@ -5861,6 +5893,17 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { parent.smsserver.sendToken(domain, user.phone, user.otpsms.k, obj.getLanguageCodes(req)); // Ask for a login token & confirm sms was sent try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, sms2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { } + /* + } else if ((command.token == '**push**') && (push2fa == true)) { + // Cause push notification to device + const code = Buffer.from(obj.common.zeroPad(getRandomSixDigitInteger(), 6)).toString('base64'); + const authCookie = parent.encodeCookie({ a: 'checkAuth', c: code, u: user._id, n: user.otpdev }); + var payload = { notification: { title: "MeshCentral", body: user.name + " authentication" }, data: { url: '2fa://auth?code=' + code + '&c=' + authCookie } }; + var options = { priority: 'High', timeToLive: 60 }; // TTL: 1 minute + parent.firebase.sendToDevice(user.otpdev, payload, options, function (id, err, errdesc) { + if (err == null) { parent.debug('email', 'Successfully auth check send push message to device'); } else { parent.debug('email', 'Failed auth check push message to device, error: ' + errdesc); } + }); + */ } else { // Ask for a login token parent.debug('web', 'Asking for login token'); @@ -5965,7 +6008,8 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { // Figure out if email 2FA is allowed var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.otpekey != null)); var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null)); - if ((typeof req.query.token != 'string') || (req.query.token == '**email**') || (req.query.token == '**sms**')) { + //var push2fa = ((parent.firebase != null) && (user.otpdev != null)); + if ((typeof req.query.token != 'string') || (req.query.token == '**email**') || (req.query.token == '**sms**')/* || (req.query.token == '**push**')*/) { if ((req.query.token == '**email**') && (email2fa == true)) { // Cause a token to be sent to the user's registered email user.otpekey = { k: obj.common.zeroPad(getRandomEightDigitInteger(), 8), d: Date.now() }; @@ -5982,6 +6026,17 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { parent.smsserver.sendToken(domain, user.phone, user.otpsms.k, obj.getLanguageCodes(req)); // Ask for a login token & confirm sms was sent try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, sms2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { } + /* + } else if ((command.token == '**push**') && (push2fa == true)) { + // Cause push notification to device + const code = Buffer.from(obj.common.zeroPad(getRandomSixDigitInteger(), 6)).toString('base64'); + const authCookie = parent.encodeCookie({ a: 'checkAuth', c: code, u: user._id, n: user.otpdev }); + var payload = { notification: { title: "MeshCentral", body: user.name + " authentication" }, data: { url: '2fa://auth?code=' + code + '&c=' + authCookie } }; + var options = { priority: 'High', timeToLive: 60 }; // TTL: 1 minute + parent.firebase.sendToDevice(user.otpdev, payload, options, function (id, err, errdesc) { + if (err == null) { parent.debug('email', 'Successfully auth check send push message to device'); } else { parent.debug('email', 'Failed auth check push message to device, error: ' + errdesc); } + }); + */ } else { // Ask for a login token parent.debug('web', 'Asking for login token');