From 5079f5d53a2e2692b88e2772ea2f2a5da89de993 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Fri, 24 Mar 2017 14:03:52 -0300 Subject: [PATCH 01/19] Initial implementation of two-factor authentication --- .../rocketchat-2fa/.npm/package/.gitignore | 1 + packages/rocketchat-2fa/.npm/package/README | 7 ++++ .../.npm/package/npm-shrinkwrap.json | 14 +++++++ packages/rocketchat-2fa/README.md | 0 .../client/accountSecurity.html | 37 +++++++++++++++++++ .../rocketchat-2fa/client/accountSecurity.js | 6 +++ packages/rocketchat-2fa/package.js | 31 ++++++++++++++++ .../client/accountFlex.html | 15 +++++--- 8 files changed, 105 insertions(+), 6 deletions(-) create mode 100644 packages/rocketchat-2fa/.npm/package/.gitignore create mode 100644 packages/rocketchat-2fa/.npm/package/README create mode 100644 packages/rocketchat-2fa/.npm/package/npm-shrinkwrap.json create mode 100644 packages/rocketchat-2fa/README.md create mode 100644 packages/rocketchat-2fa/client/accountSecurity.html create mode 100644 packages/rocketchat-2fa/client/accountSecurity.js create mode 100644 packages/rocketchat-2fa/package.js diff --git a/packages/rocketchat-2fa/.npm/package/.gitignore b/packages/rocketchat-2fa/.npm/package/.gitignore new file mode 100644 index 000000000000..3c3629e647f5 --- /dev/null +++ b/packages/rocketchat-2fa/.npm/package/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/packages/rocketchat-2fa/.npm/package/README b/packages/rocketchat-2fa/.npm/package/README new file mode 100644 index 000000000000..3d492553a438 --- /dev/null +++ b/packages/rocketchat-2fa/.npm/package/README @@ -0,0 +1,7 @@ +This directory and the files immediately inside it are automatically generated +when you change this package's NPM dependencies. Commit the files in this +directory (npm-shrinkwrap.json, .gitignore, and this README) to source control +so that others run the same versions of sub-dependencies. + +You should NOT check in the node_modules directory that Meteor automatically +creates; if you are using git, the .gitignore file tells git to ignore it. diff --git a/packages/rocketchat-2fa/.npm/package/npm-shrinkwrap.json b/packages/rocketchat-2fa/.npm/package/npm-shrinkwrap.json new file mode 100644 index 000000000000..d9a9781d1968 --- /dev/null +++ b/packages/rocketchat-2fa/.npm/package/npm-shrinkwrap.json @@ -0,0 +1,14 @@ +{ + "dependencies": { + "base32.js": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/base32.js/-/base32.js-0.0.1.tgz", + "from": "base32.js@0.0.1" + }, + "speakeasy": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/speakeasy/-/speakeasy-2.0.0.tgz", + "from": "speakeasy@2.0.0" + } + } +} diff --git a/packages/rocketchat-2fa/README.md b/packages/rocketchat-2fa/README.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/rocketchat-2fa/client/accountSecurity.html b/packages/rocketchat-2fa/client/accountSecurity.html new file mode 100644 index 000000000000..fe6875faba20 --- /dev/null +++ b/packages/rocketchat-2fa/client/accountSecurity.html @@ -0,0 +1,37 @@ + diff --git a/packages/rocketchat-2fa/client/accountSecurity.js b/packages/rocketchat-2fa/client/accountSecurity.js new file mode 100644 index 000000000000..bfa194eca3fe --- /dev/null +++ b/packages/rocketchat-2fa/client/accountSecurity.js @@ -0,0 +1,6 @@ +Template.accountSecurity.events({ + 'click .enable-2fa'() { + console.log('enable it'); + + } +}); diff --git a/packages/rocketchat-2fa/package.js b/packages/rocketchat-2fa/package.js new file mode 100644 index 000000000000..72bbbd80b33d --- /dev/null +++ b/packages/rocketchat-2fa/package.js @@ -0,0 +1,31 @@ +Package.describe({ + name: 'rocketchat:2fa', + version: '0.0.1', + // Brief, one-line summary of the package. + summary: '', + // URL to the Git repository containing the source code for this package. + git: '', + // By default, Meteor will default to using README.md for documentation. + // To avoid submitting documentation, set this field to null. + documentation: 'README.md' +}); + +Npm.depends({ + speakeasy: '2.0.0' +}); + +Package.onUse(function(api) { + // api.versionsFrom('1.4.3.1'); + // api.use('ecmascript'); + // api.mainModule('rocketchat-2fa.js'); + + api.use([ + 'ecmascript', + 'templating', + 'rocketchat:lib' + ]); + api.addFiles('client/accountSecurity.html', 'client'); + api.addFiles('client/accountSecurity.js', 'client'); + + api.addFiles('server/methods/enable2fa.js', 'server'); +}); diff --git a/packages/rocketchat-ui-account/client/accountFlex.html b/packages/rocketchat-ui-account/client/accountFlex.html index 6c3e46ddcd6f..e8f433806c48 100644 --- a/packages/rocketchat-ui-account/client/accountFlex.html +++ b/packages/rocketchat-ui-account/client/accountFlex.html @@ -10,15 +10,18 @@

{{_ "My_Account"}}

  • + {{#if allowUserProfileChange}}
  • - {{#if allowUserProfileChange}} - - {{/if}} +
  • + {{/if}} + {{#if allowUserAvatarChange}}
  • - {{#if allowUserAvatarChange}} - - {{/if}} + +
  • + {{/if}} +
  • +
  • From 843d06196a7164931b2f10cd734235dcfeaf7de4 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Fri, 24 Mar 2017 14:34:25 -0300 Subject: [PATCH 02/19] Init TOTP login verification --- .meteor/packages | 1 + .meteor/versions | 1 + .../rocketchat-2fa/client/TOTPPassword.js | 27 +++++++++++++++++++ packages/rocketchat-2fa/package.js | 3 ++- packages/rocketchat-i18n/i18n/en.i18n.json | 3 +++ packages/rocketchat-ui-login/package.js | 3 ++- server/lib/accounts.js | 6 +++++ 7 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 packages/rocketchat-2fa/client/TOTPPassword.js diff --git a/.meteor/packages b/.meteor/packages index 4863b12e029e..905b829d46f2 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -38,6 +38,7 @@ standard-minifier-css@1.3.3 standard-minifier-js@1.2.2 tracker@1.1.2 +rocketchat:2fa rocketchat:action-links rocketchat:analytics rocketchat:api diff --git a/.meteor/versions b/.meteor/versions index da42836cd55d..d6517147238f 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -119,6 +119,7 @@ reactive-dict@1.1.8 reactive-var@1.0.11 reload@1.1.11 retry@1.0.9 +rocketchat:2fa@0.0.1 rocketchat:action-links@0.0.1 rocketchat:analytics@0.0.2 rocketchat:api@0.0.1 diff --git a/packages/rocketchat-2fa/client/TOTPPassword.js b/packages/rocketchat-2fa/client/TOTPPassword.js new file mode 100644 index 000000000000..0bbed5c73e2e --- /dev/null +++ b/packages/rocketchat-2fa/client/TOTPPassword.js @@ -0,0 +1,27 @@ +const loginWithPassword = Meteor.loginWithPassword; + +Meteor.loginWithPassword = function(email, password, cb) { + loginWithPassword(email, password, (error) => { + console.log(error); + if (!error || error.error !== 'totp-required') { + return cb(error); + } + + swal({ + title: t('Two-factor_authentication'), + text: t('Open_your_authentication_app_and_enter_the_code'), + type: 'input', + inputType: 'text', + showCancelButton: true, + closeOnConfirm: false, + confirmButtonText: t('Verify'), + cancelButtonText: t('Cancel') + }, (code, ...args) => { + if (code === false) { + return cb(); + } + + console.log(code, ...args); + }); + }); +}; diff --git a/packages/rocketchat-2fa/package.js b/packages/rocketchat-2fa/package.js index 72bbbd80b33d..3ad7c993b9aa 100644 --- a/packages/rocketchat-2fa/package.js +++ b/packages/rocketchat-2fa/package.js @@ -26,6 +26,7 @@ Package.onUse(function(api) { ]); api.addFiles('client/accountSecurity.html', 'client'); api.addFiles('client/accountSecurity.js', 'client'); + api.addFiles('client/TOTPPassword.js', 'client'); - api.addFiles('server/methods/enable2fa.js', 'server'); + // api.addFiles('server/methods/enable2fa.js', 'server'); }); diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index face84e3194c..568cfcb8ca86 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -1094,6 +1094,7 @@ "optional": "optional", "Use_minor_colors": "Use minor color palette (defaults inherit major colors)", "or": "or", + "Open_your_authentication_app_and_enter_the_code": "Open your authentication app and enter the code", "Order": "Order", "OS_Arch": "OS Arch", "OS_Cpus": "OS CPU Count", @@ -1475,6 +1476,7 @@ "This_is_a_push_test_messsage": "This is a push test message", "This_room_has_been_archived_by__username_": "This room has been archived by __username__", "This_room_has_been_unarchived_by__username_": "This room has been unarchived by __username__", + "Two-factor_authentication": "Two-factor authentication", "Thursday": "Thursday", "Time_in_seconds": "Time in seconds", "Title": "Title", @@ -1596,6 +1598,7 @@ "Verification_Email_Subject": "[Site_Name] - Verify your account", "Verification_Email": "Click here to verify your account.", "Verified": "Verified", + "Verify": "Verify", "Version": "Version", "Video_Chat_Window": "Video Chat", "Video_Conference": "Video Conference", diff --git a/packages/rocketchat-ui-login/package.js b/packages/rocketchat-ui-login/package.js index bd0efa48cbc1..4c8f8647f17b 100644 --- a/packages/rocketchat-ui-login/package.js +++ b/packages/rocketchat-ui-login/package.js @@ -17,7 +17,8 @@ Package.onUse(function(api) { 'coffeescript', 'underscore', 'rocketchat:lib', - 'rocketchat:assets' + 'rocketchat:assets', + 'rocketchat:2fa' ]); api.use('kadira:flow-router', 'client'); diff --git a/server/lib/accounts.js b/server/lib/accounts.js index 5dad2be2517c..def259f9460c 100644 --- a/server/lib/accounts.js +++ b/server/lib/accounts.js @@ -164,6 +164,12 @@ Accounts.validateLoginAttempt(function(login) { } } + if (login.type === 'password' && login.user.services && login.user.services.totp && login.user.services.totp.enabled === true) { + console.log(login); + // Verify OTP + throw new Meteor.Error('totp-required', 'TOTP Required'); + } + RocketChat.models.Users.updateLastLoginById(login.user._id); Meteor.defer(function() { return RocketChat.callbacks.run('afterValidateLogin', login); From 5e94f40537628ef34c2276ddd890ff4f1d398b85 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Fri, 24 Mar 2017 15:38:05 -0300 Subject: [PATCH 03/19] Implement intial 2FA validation --- .../rocketchat-2fa/client/TOTPPassword.js | 53 +++++++++++++++++-- packages/rocketchat-2fa/package.js | 2 + .../rocketchat-2fa/server/loginHandler.js | 7 +++ packages/rocketchat-i18n/i18n/en.i18n.json | 1 + server/lib/accounts.js | 12 +++-- 5 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 packages/rocketchat-2fa/server/loginHandler.js diff --git a/packages/rocketchat-2fa/client/TOTPPassword.js b/packages/rocketchat-2fa/client/TOTPPassword.js index 0bbed5c73e2e..1c365271b78b 100644 --- a/packages/rocketchat-2fa/client/TOTPPassword.js +++ b/packages/rocketchat-2fa/client/TOTPPassword.js @@ -1,8 +1,46 @@ +import toastr from 'toastr'; + +function reportError(error, callback) { + if (callback) { + callback(error); + } else { + throw error; + } +} + +Meteor.loginWithPasswordAndTOTP = function(selector, password, code, callback) { + if (typeof selector === 'string') { + if (selector.indexOf('@') === -1) { + selector = {username: selector}; + } else { + selector = {email: selector}; + } + } + + Accounts.callLoginMethod({ + methodArguments: [{ + totp: { + login: { + user: selector, + password: Accounts._hashPassword(password) + }, + code + } + }], + userCallback: function(error) { + if (error) { + reportError(error, callback); + } else { + callback && callback(); + } + } + }); +}; + const loginWithPassword = Meteor.loginWithPassword; Meteor.loginWithPassword = function(email, password, cb) { loginWithPassword(email, password, (error) => { - console.log(error); if (!error || error.error !== 'totp-required') { return cb(error); } @@ -13,15 +51,22 @@ Meteor.loginWithPassword = function(email, password, cb) { type: 'input', inputType: 'text', showCancelButton: true, - closeOnConfirm: false, + closeOnConfirm: true, confirmButtonText: t('Verify'), cancelButtonText: t('Cancel') - }, (code, ...args) => { + }, (code) => { if (code === false) { return cb(); } - console.log(code, ...args); + Meteor.loginWithPasswordAndTOTP(email, password, code, (error) => { + if (error && error.error === 'totp-invalid') { + toastr.error(t('Invalid_two_factor_code')); + cb(); + } else { + cb(error); + } + }); }); }); }; diff --git a/packages/rocketchat-2fa/package.js b/packages/rocketchat-2fa/package.js index 3ad7c993b9aa..99df5dc795c9 100644 --- a/packages/rocketchat-2fa/package.js +++ b/packages/rocketchat-2fa/package.js @@ -20,6 +20,7 @@ Package.onUse(function(api) { // api.mainModule('rocketchat-2fa.js'); api.use([ + 'accounts-base', 'ecmascript', 'templating', 'rocketchat:lib' @@ -29,4 +30,5 @@ Package.onUse(function(api) { api.addFiles('client/TOTPPassword.js', 'client'); // api.addFiles('server/methods/enable2fa.js', 'server'); + api.addFiles('server/loginHandler.js', 'server'); }); diff --git a/packages/rocketchat-2fa/server/loginHandler.js b/packages/rocketchat-2fa/server/loginHandler.js new file mode 100644 index 000000000000..f32472dde312 --- /dev/null +++ b/packages/rocketchat-2fa/server/loginHandler.js @@ -0,0 +1,7 @@ +Accounts.registerLoginHandler('totp', function(options) { + if (!options.totp || !options.totp.code) { + return; + } + + return Accounts._runLoginHandlers(this, options.totp.login); +}); diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 568cfcb8ca86..ebc2a85f9d01 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -739,6 +739,7 @@ "Invalid_room_name": "%s is not a valid room name,
    use only letters, numbers, hyphens and underscores", "Invalid_secret_URL_message": "The URL provided is invalid.", "Invalid_setting_s": "Invalid setting: %s", + "Invalid_two_factor_code": "Invalid two factor code", "invisible": "invisible", "Invisible": "Invisible", "Invitation": "Invitation", diff --git a/server/lib/accounts.js b/server/lib/accounts.js index def259f9460c..13544b90e6d3 100644 --- a/server/lib/accounts.js +++ b/server/lib/accounts.js @@ -165,9 +165,15 @@ Accounts.validateLoginAttempt(function(login) { } if (login.type === 'password' && login.user.services && login.user.services.totp && login.user.services.totp.enabled === true) { - console.log(login); - // Verify OTP - throw new Meteor.Error('totp-required', 'TOTP Required'); + const { totp } = login.methodArguments[0]; + if (totp && totp.code) { + //TODO Verify OTP + if (totp.code !== '123') { + throw new Meteor.Error('totp-invalid', 'TOTP Invalid'); + } + } else { + throw new Meteor.Error('totp-required', 'TOTP Required'); + } } RocketChat.models.Users.updateLastLoginById(login.user._id); From 749c3c6625270cef22493141610adeeccbf1efaa Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Fri, 24 Mar 2017 15:46:38 -0300 Subject: [PATCH 04/19] Add verification step for two-factor --- .../.npm/package/npm-shrinkwrap.json | 5 ++ .../client/accountSecurity.html | 17 ++++++- .../rocketchat-2fa/client/accountSecurity.js | 51 ++++++++++++++++++- packages/rocketchat-2fa/package.js | 7 ++- .../server/methods/enable2fa.js | 22 ++++++++ .../server/methods/verifyTemp2FAToken.js | 27 ++++++++++ .../rocketchat-2fa/server/models/users.js | 26 ++++++++++ server/publications/userData.js | 1 + 8 files changed, 151 insertions(+), 5 deletions(-) create mode 100644 packages/rocketchat-2fa/server/methods/enable2fa.js create mode 100644 packages/rocketchat-2fa/server/methods/verifyTemp2FAToken.js create mode 100644 packages/rocketchat-2fa/server/models/users.js diff --git a/packages/rocketchat-2fa/.npm/package/npm-shrinkwrap.json b/packages/rocketchat-2fa/.npm/package/npm-shrinkwrap.json index d9a9781d1968..df69ce869e8b 100644 --- a/packages/rocketchat-2fa/.npm/package/npm-shrinkwrap.json +++ b/packages/rocketchat-2fa/.npm/package/npm-shrinkwrap.json @@ -9,6 +9,11 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/speakeasy/-/speakeasy-2.0.0.tgz", "from": "speakeasy@2.0.0" + }, + "yaqrcode": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/yaqrcode/-/yaqrcode-0.2.1.tgz", + "from": "yaqrcode@0.2.1" } } } diff --git a/packages/rocketchat-2fa/client/accountSecurity.html b/packages/rocketchat-2fa/client/accountSecurity.html index fe6875faba20..2be2ac2f4cbe 100644 --- a/packages/rocketchat-2fa/client/accountSecurity.html +++ b/packages/rocketchat-2fa/client/accountSecurity.html @@ -12,9 +12,22 @@

    {{_ "Two-Factor_Authentication_App"}}

    - {{_ "Two-factor_authentication_is_currently_disabled"}} + {{#if isEnabled}} + done. + {{else}} + {{#unless isRegistering}} + {{_ "Two-factor_authentication_is_currently_disabled"}} - + + {{else}} + + +
    + + +
    + {{/unless}} + {{/if}}
    diff --git a/packages/rocketchat-2fa/client/accountSecurity.js b/packages/rocketchat-2fa/client/accountSecurity.js index bfa194eca3fe..a7a6638c3426 100644 --- a/packages/rocketchat-2fa/client/accountSecurity.js +++ b/packages/rocketchat-2fa/client/accountSecurity.js @@ -1,6 +1,55 @@ +import qrcode from 'yaqrcode'; + +window.qrcode = qrcode; + +Template.accountSecurity.helpers({ + showImage() { + return Template.instance().showImage.get(); + }, + imageData() { + return Template.instance().imageData.get(); + }, + isEnabled() { + return false; + }, + isRegistering() { + return Template.instance().state.get() === 'registering'; + } +}); + Template.accountSecurity.events({ - 'click .enable-2fa'() { + 'click .enable-2fa'(event, instance) { console.log('enable it'); + Meteor.call('enable2fa', (error, result) => { + // instance.showImage.set(true); + + console.log('result ->', result); + + instance.imageData.set(qrcode(result.url, { size: 200 })); + + instance.state.set('registering'); + }); + }, + + 'submit .verify-code'(event, instance) { + event.preventDefault(); + + Meteor.call('verifyTemp2FAToken', instance.find('#testCode').value, (error, result) => { + // instance.showImage.set(true); + if (error) { + + } + if (result) { + swal('ok'); + } + }); } }); + +Template.accountSecurity.onCreated(function() { + this.showImage = new ReactiveVar(false); + this.imageData = new ReactiveVar(); + + this.state = new ReactiveVar(); +}); diff --git a/packages/rocketchat-2fa/package.js b/packages/rocketchat-2fa/package.js index 99df5dc795c9..f56eebe6b123 100644 --- a/packages/rocketchat-2fa/package.js +++ b/packages/rocketchat-2fa/package.js @@ -11,7 +11,8 @@ Package.describe({ }); Npm.depends({ - speakeasy: '2.0.0' + speakeasy: '2.0.0', + yaqrcode: '0.2.1' }); Package.onUse(function(api) { @@ -29,6 +30,8 @@ Package.onUse(function(api) { api.addFiles('client/accountSecurity.js', 'client'); api.addFiles('client/TOTPPassword.js', 'client'); - // api.addFiles('server/methods/enable2fa.js', 'server'); + api.addFiles('server/methods/enable2fa.js', 'server'); + api.addFiles('server/methods/verifyTemp2FAToken.js', 'server'); + api.addFiles('server/models/users.js', 'server'); api.addFiles('server/loginHandler.js', 'server'); }); diff --git a/packages/rocketchat-2fa/server/methods/enable2fa.js b/packages/rocketchat-2fa/server/methods/enable2fa.js new file mode 100644 index 000000000000..635fbce193ec --- /dev/null +++ b/packages/rocketchat-2fa/server/methods/enable2fa.js @@ -0,0 +1,22 @@ +const speakeasy = Npm.require('speakeasy'); + +Meteor.methods({ + enable2fa() { + if (!Meteor.userId()) { + throw new Meteor.Error('not-authorized'); + } + + const user = Meteor.user(); + + const secret = speakeasy.generateSecret(); + + RocketChat.models.Users.disable2FAAndSetTempSecretByUserId(Meteor.userId(), secret.base32); + + return { + url: speakeasy.otpauthURL({ + secret: secret.ascii, + label: `Rocket.Chat:${ user.username }` + }) + }; + } +}); diff --git a/packages/rocketchat-2fa/server/methods/verifyTemp2FAToken.js b/packages/rocketchat-2fa/server/methods/verifyTemp2FAToken.js new file mode 100644 index 000000000000..04a5d112409f --- /dev/null +++ b/packages/rocketchat-2fa/server/methods/verifyTemp2FAToken.js @@ -0,0 +1,27 @@ +const speakeasy = Npm.require('speakeasy'); + +Meteor.methods({ + verifyTemp2FAToken(userToken) { + if (!Meteor.userId()) { + throw new Meteor.Error('not-authorized'); + } + + const user = Meteor.user(); + + if (!user.services || !user.services.totp || !user.services.totp.tempSecret) { + console.error('errour'); + return false; + } + + const verified = speakeasy.totp.verify({ + secret: user.services.totp.tempSecret, + encoding: 'base32', + token: userToken + }); + + if (verified) { + RocketChat.models.Users.enable2FAAndSetSecretByUserId(Meteor.userId(), user.services.totp.tempSecret); + } + return verified; + } +}); diff --git a/packages/rocketchat-2fa/server/models/users.js b/packages/rocketchat-2fa/server/models/users.js new file mode 100644 index 000000000000..cfdd65f3bb86 --- /dev/null +++ b/packages/rocketchat-2fa/server/models/users.js @@ -0,0 +1,26 @@ +RocketChat.models.Users.disable2FAAndSetTempSecretByUserId = function(userId, tempToken) { + return this.update({ + _id: userId + }, { + $set: { + 'services.totp': { + enabled: false, + tempSecret: tempToken + } + } + }); +}; + +RocketChat.models.Users.enable2FAAndSetSecretByUserId = function(userId, secret) { + return this.update({ + _id: userId + }, { + $set: { + 'services.totp.enabled': true, + 'services.totp.secret': secret + }, + $unset: { + 'services.totp.tempSecret': 1 + } + }); +}; diff --git a/server/publications/userData.js b/server/publications/userData.js index 2d12096f0210..cec86d01cb3c 100644 --- a/server/publications/userData.js +++ b/server/publications/userData.js @@ -23,6 +23,7 @@ Meteor.publish('userData', function() { requirePasswordChange: 1, requirePasswordChangeReason: 1, 'services.password.bcrypt': 1, + 'services.totp.enabled': 1, statusLivechat: 1 } }); From 43a302135db09a999aee6aaa02847730442f233c Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Fri, 24 Mar 2017 16:00:22 -0300 Subject: [PATCH 05/19] Add ability to disable two-factor authentication --- .../client/accountSecurity.html | 12 +-------- .../rocketchat-2fa/client/accountSecurity.js | 25 ++++++++++++------- packages/rocketchat-2fa/package.js | 1 + .../server/methods/disable2fa.js | 10 ++++++++ .../rocketchat-2fa/server/models/users.js | 12 +++++++++ 5 files changed, 40 insertions(+), 20 deletions(-) create mode 100644 packages/rocketchat-2fa/server/methods/disable2fa.js diff --git a/packages/rocketchat-2fa/client/accountSecurity.html b/packages/rocketchat-2fa/client/accountSecurity.html index 2be2ac2f4cbe..780967e56bff 100644 --- a/packages/rocketchat-2fa/client/accountSecurity.html +++ b/packages/rocketchat-2fa/client/accountSecurity.html @@ -13,7 +13,7 @@

    {{_ "Two-Factor_Authentication_App"}}

    {{#if isEnabled}} - done. + {{else}} {{#unless isRegistering}} {{_ "Two-factor_authentication_is_currently_disabled"}} @@ -30,16 +30,6 @@

    {{_ "Two-Factor_Authentication_App"}}

    {{/if}}
    -
    -

    {{_ "Backup_Codes"}}

    -
    -
    - -
    -
    -
    -
    -
    diff --git a/packages/rocketchat-2fa/client/accountSecurity.js b/packages/rocketchat-2fa/client/accountSecurity.js index a7a6638c3426..db7d25014ad6 100644 --- a/packages/rocketchat-2fa/client/accountSecurity.js +++ b/packages/rocketchat-2fa/client/accountSecurity.js @@ -10,7 +10,8 @@ Template.accountSecurity.helpers({ return Template.instance().imageData.get(); }, isEnabled() { - return false; + const user = Meteor.user(); + return user && user.services && user.services.totp && user.services.totp.enabled; }, isRegistering() { return Template.instance().state.get() === 'registering'; @@ -22,13 +23,21 @@ Template.accountSecurity.events({ console.log('enable it'); Meteor.call('enable2fa', (error, result) => { - // instance.showImage.set(true); - - console.log('result ->', result); - instance.imageData.set(qrcode(result.url, { size: 200 })); instance.state.set('registering'); + + Meteor.defer(() => { + instance.find('#testCode').focus(); + }); + }); + }, + + 'click .disable-2fa'() { + Meteor.call('disable2fa', (error, result) => { + if (result) { + swal('disabled'); + } }); }, @@ -36,11 +45,9 @@ Template.accountSecurity.events({ event.preventDefault(); Meteor.call('verifyTemp2FAToken', instance.find('#testCode').value, (error, result) => { - // instance.showImage.set(true); - if (error) { - - } if (result) { + instance.find('#testCode').value = ''; + instance.state.set(); swal('ok'); } }); diff --git a/packages/rocketchat-2fa/package.js b/packages/rocketchat-2fa/package.js index f56eebe6b123..d0ef58201f5b 100644 --- a/packages/rocketchat-2fa/package.js +++ b/packages/rocketchat-2fa/package.js @@ -30,6 +30,7 @@ Package.onUse(function(api) { api.addFiles('client/accountSecurity.js', 'client'); api.addFiles('client/TOTPPassword.js', 'client'); + api.addFiles('server/methods/disable2fa.js', 'server'); api.addFiles('server/methods/enable2fa.js', 'server'); api.addFiles('server/methods/verifyTemp2FAToken.js', 'server'); api.addFiles('server/models/users.js', 'server'); diff --git a/packages/rocketchat-2fa/server/methods/disable2fa.js b/packages/rocketchat-2fa/server/methods/disable2fa.js new file mode 100644 index 000000000000..a3d8938b244a --- /dev/null +++ b/packages/rocketchat-2fa/server/methods/disable2fa.js @@ -0,0 +1,10 @@ +Meteor.methods({ + disable2fa() { + + if (!Meteor.userId()) { + throw new Meteor.Error('not-authorized'); + } + + return RocketChat.models.Users.disable2FAByUserId(Meteor.userId()); + } +}); diff --git a/packages/rocketchat-2fa/server/models/users.js b/packages/rocketchat-2fa/server/models/users.js index cfdd65f3bb86..470209c82dfd 100644 --- a/packages/rocketchat-2fa/server/models/users.js +++ b/packages/rocketchat-2fa/server/models/users.js @@ -24,3 +24,15 @@ RocketChat.models.Users.enable2FAAndSetSecretByUserId = function(userId, secret) } }); }; + +RocketChat.models.Users.disable2FAByUserId = function(userId) { + return this.update({ + _id: userId + }, { + $set: { + 'services.totp': { + enabled: false + } + } + }); +}; From afa84899a1ae725ae77b8a07c2e01d548271e451 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Fri, 24 Mar 2017 16:00:27 -0300 Subject: [PATCH 06/19] Integrate login with 2FA TOTP --- .../rocketchat-2fa/server/loginHandler.js | 22 +++++++++++++++++++ server/lib/accounts.js | 12 +--------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/packages/rocketchat-2fa/server/loginHandler.js b/packages/rocketchat-2fa/server/loginHandler.js index f32472dde312..621e072834a9 100644 --- a/packages/rocketchat-2fa/server/loginHandler.js +++ b/packages/rocketchat-2fa/server/loginHandler.js @@ -1,3 +1,5 @@ +import speakeasy from 'speakeasy'; + Accounts.registerLoginHandler('totp', function(options) { if (!options.totp || !options.totp.code) { return; @@ -5,3 +7,23 @@ Accounts.registerLoginHandler('totp', function(options) { return Accounts._runLoginHandlers(this, options.totp.login); }); + +RocketChat.callbacks.add('onValidateLogin', (login) => { + if (login.type === 'password' && login.user.services && login.user.services.totp && login.user.services.totp.enabled === true) { + const { totp } = login.methodArguments[0]; + + if (!totp || !totp.code) { + throw new Meteor.Error('totp-required', 'TOTP Required'); + } + + const verified = speakeasy.totp.verify({ + secret: login.user.services.totp.secret, + encoding: 'base32', + token: totp.code + }); + + if (verified !== true) { + throw new Meteor.Error('totp-invalid', 'TOTP Invalid'); + } + } +}); diff --git a/server/lib/accounts.js b/server/lib/accounts.js index 13544b90e6d3..970013154429 100644 --- a/server/lib/accounts.js +++ b/server/lib/accounts.js @@ -164,17 +164,7 @@ Accounts.validateLoginAttempt(function(login) { } } - if (login.type === 'password' && login.user.services && login.user.services.totp && login.user.services.totp.enabled === true) { - const { totp } = login.methodArguments[0]; - if (totp && totp.code) { - //TODO Verify OTP - if (totp.code !== '123') { - throw new Meteor.Error('totp-invalid', 'TOTP Invalid'); - } - } else { - throw new Meteor.Error('totp-required', 'TOTP Required'); - } - } + login = RocketChat.callbacks.run('onValidateLogin', login); RocketChat.models.Users.updateLastLoginById(login.user._id); Meteor.defer(function() { From ba74f9b262c307d01984ed4a33a5274b0a87cd57 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Fri, 24 Mar 2017 16:14:50 -0300 Subject: [PATCH 07/19] Improve 2FA interface --- packages/rocketchat-2fa/client/accountSecurity.html | 5 +---- packages/rocketchat-2fa/client/accountSecurity.js | 9 +++++---- packages/rocketchat-i18n/i18n/en.i18n.json | 5 +++++ 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/rocketchat-2fa/client/accountSecurity.html b/packages/rocketchat-2fa/client/accountSecurity.html index 780967e56bff..f748a31bf64a 100644 --- a/packages/rocketchat-2fa/client/accountSecurity.html +++ b/packages/rocketchat-2fa/client/accountSecurity.html @@ -10,7 +10,7 @@

    -

    {{_ "Two-Factor_Authentication_App"}}

    +

    {{_ "Two-factor_authentication"}}

    {{#if isEnabled}} @@ -31,9 +31,6 @@

    {{_ "Two-Factor_Authentication_App"}}

    -
    - -

    diff --git a/packages/rocketchat-2fa/client/accountSecurity.js b/packages/rocketchat-2fa/client/accountSecurity.js index db7d25014ad6..45c2f47def5d 100644 --- a/packages/rocketchat-2fa/client/accountSecurity.js +++ b/packages/rocketchat-2fa/client/accountSecurity.js @@ -1,3 +1,4 @@ +import toastr from 'toastr'; import qrcode from 'yaqrcode'; window.qrcode = qrcode; @@ -34,9 +35,9 @@ Template.accountSecurity.events({ }, 'click .disable-2fa'() { - Meteor.call('disable2fa', (error, result) => { - if (result) { - swal('disabled'); + Meteor.call('disable2fa', (error) => { + if (error) { + toastr.error(t(error.error)); } }); }, @@ -48,7 +49,7 @@ Template.accountSecurity.events({ if (result) { instance.find('#testCode').value = ''; instance.state.set(); - swal('ok'); + toastr.success(t('Two-factor_authentication_enabled')); } }); } diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index ebc2a85f9d01..290b3cf2ba83 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -404,6 +404,7 @@ "Desktop_Notifications_Enabled": "Desktop Notifications are Enabled", "Direct_message_someone": "Direct message someone", "Direct_Messages": "Direct Messages", + "Disable_two-factor_authentication": "Disable two-factor authentication", "Display_offline_form": "Display offline form", "Displays_action_text": "Displays action text", "Do_you_want_to_change_to_s_question": "Do you want to change to %s?", @@ -446,6 +447,7 @@ "Empty_title": "Empty title", "Enable": "Enable", "Enable_Desktop_Notifications": "Enable Desktop Notifications", + "Enable_two-factor_authentication": "Enable two-factor authentication", "Enabled": "Enabled", "Enable_Svg_Favicon": "Enable SVG favicon", "Encrypted_message": "Encrypted message", @@ -1284,6 +1286,7 @@ "Search_Private_Groups": "Search Private Groups", "seconds": "seconds", "Secret_token": "Secret token", + "Security": "Security", "Select_a_department": "Select a department", "Select_a_user": "Select a user", "Select_an_avatar": "Select an avatar", @@ -1478,6 +1481,8 @@ "This_room_has_been_archived_by__username_": "This room has been archived by __username__", "This_room_has_been_unarchived_by__username_": "This room has been unarchived by __username__", "Two-factor_authentication": "Two-factor authentication", + "Two-factor_authentication_enabled": "Two-factor authentication enabled", + "Two-factor_authentication_is_currently_disabled": "Two-factor authentication is currently disabled", "Thursday": "Thursday", "Time_in_seconds": "Time in seconds", "Title": "Title", From 1617fde192de993c0761742d262f699b55324836 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Fri, 24 Mar 2017 16:42:48 -0300 Subject: [PATCH 08/19] Validates current two-factor for disabling --- .../client/accountSecurity.html | 10 +++--- .../rocketchat-2fa/client/accountSecurity.js | 31 ++++++++++++++++--- packages/rocketchat-2fa/package.js | 4 +++ packages/rocketchat-2fa/server/lib/totp.js | 22 +++++++++++++ .../rocketchat-2fa/server/loginHandler.js | 8 +---- .../server/methods/disable2fa.js | 11 +++++-- .../server/methods/enable2fa.js | 9 ++---- packages/rocketchat-i18n/i18n/en.i18n.json | 3 ++ 8 files changed, 73 insertions(+), 25 deletions(-) create mode 100644 packages/rocketchat-2fa/server/lib/totp.js diff --git a/packages/rocketchat-2fa/client/accountSecurity.html b/packages/rocketchat-2fa/client/accountSecurity.html index f748a31bf64a..b1cee56e51f7 100644 --- a/packages/rocketchat-2fa/client/accountSecurity.html +++ b/packages/rocketchat-2fa/client/accountSecurity.html @@ -16,15 +16,17 @@

    {{_ "Two-factor_authentication"}}

    {{else}} {{#unless isRegistering}} - {{_ "Two-factor_authentication_is_currently_disabled"}} +

    {{_ "Two-factor_authentication_is_currently_disabled"}}

    {{else}} +

    {{_ "Scan_QR_code"}}

    + -
    - - + + +
    {{/unless}} {{/if}} diff --git a/packages/rocketchat-2fa/client/accountSecurity.js b/packages/rocketchat-2fa/client/accountSecurity.js index 45c2f47def5d..5ef8b925acb8 100644 --- a/packages/rocketchat-2fa/client/accountSecurity.js +++ b/packages/rocketchat-2fa/client/accountSecurity.js @@ -21,8 +21,6 @@ Template.accountSecurity.helpers({ Template.accountSecurity.events({ 'click .enable-2fa'(event, instance) { - console.log('enable it'); - Meteor.call('enable2fa', (error, result) => { instance.imageData.set(qrcode(result.url, { size: 200 })); @@ -35,10 +33,31 @@ Template.accountSecurity.events({ }, 'click .disable-2fa'() { - Meteor.call('disable2fa', (error) => { - if (error) { - toastr.error(t(error.error)); + swal({ + title: t('Two-factor_authentication'), + text: t('Open_your_authentication_app_and_enter_the_code'), + type: 'input', + inputType: 'text', + showCancelButton: true, + closeOnConfirm: true, + confirmButtonText: t('Verify'), + cancelButtonText: t('Cancel') + }, (code) => { + if (code === false) { + return; } + + Meteor.call('disable2fa', code, (error, result) => { + if (error) { + return toastr.error(t(error.error)); + } + + if (result) { + toastr.success(t('Two-factor_authentication_disabled')); + } else { + return toastr.error(t('Invalid_two_factor_code')); + } + }); }); }, @@ -50,6 +69,8 @@ Template.accountSecurity.events({ instance.find('#testCode').value = ''; instance.state.set(); toastr.success(t('Two-factor_authentication_enabled')); + } else { + toastr.error(t('Invalid_two_factor_code')); } }); } diff --git a/packages/rocketchat-2fa/package.js b/packages/rocketchat-2fa/package.js index d0ef58201f5b..0d44f6fbfbf3 100644 --- a/packages/rocketchat-2fa/package.js +++ b/packages/rocketchat-2fa/package.js @@ -30,9 +30,13 @@ Package.onUse(function(api) { api.addFiles('client/accountSecurity.js', 'client'); api.addFiles('client/TOTPPassword.js', 'client'); + api.addFiles('server/lib/totp.js', 'server'); + api.addFiles('server/methods/disable2fa.js', 'server'); api.addFiles('server/methods/enable2fa.js', 'server'); api.addFiles('server/methods/verifyTemp2FAToken.js', 'server'); + api.addFiles('server/models/users.js', 'server'); + api.addFiles('server/loginHandler.js', 'server'); }); diff --git a/packages/rocketchat-2fa/server/lib/totp.js b/packages/rocketchat-2fa/server/lib/totp.js new file mode 100644 index 000000000000..37c9c84d75b0 --- /dev/null +++ b/packages/rocketchat-2fa/server/lib/totp.js @@ -0,0 +1,22 @@ +import speakeasy from 'speakeasy'; + +RocketChat.TOTP = { + generateSecret() { + return speakeasy.generateSecret(); + }, + + generateOtpauthURL(secret, username) { + return speakeasy.otpauthURL({ + secret: secret.ascii, + label: `Rocket.Chat:${ username }` + }); + }, + + verify(secret, token) { + return speakeasy.totp.verify({ + secret: secret, + encoding: 'base32', + token: token + }); + } +}; diff --git a/packages/rocketchat-2fa/server/loginHandler.js b/packages/rocketchat-2fa/server/loginHandler.js index 621e072834a9..1b486538d95a 100644 --- a/packages/rocketchat-2fa/server/loginHandler.js +++ b/packages/rocketchat-2fa/server/loginHandler.js @@ -1,5 +1,3 @@ -import speakeasy from 'speakeasy'; - Accounts.registerLoginHandler('totp', function(options) { if (!options.totp || !options.totp.code) { return; @@ -16,11 +14,7 @@ RocketChat.callbacks.add('onValidateLogin', (login) => { throw new Meteor.Error('totp-required', 'TOTP Required'); } - const verified = speakeasy.totp.verify({ - secret: login.user.services.totp.secret, - encoding: 'base32', - token: totp.code - }); + const verified = RocketChat.TOTP.verify(login.user.services.totp.secret, totp.code); if (verified !== true) { throw new Meteor.Error('totp-invalid', 'TOTP Invalid'); diff --git a/packages/rocketchat-2fa/server/methods/disable2fa.js b/packages/rocketchat-2fa/server/methods/disable2fa.js index a3d8938b244a..47e9512dba50 100644 --- a/packages/rocketchat-2fa/server/methods/disable2fa.js +++ b/packages/rocketchat-2fa/server/methods/disable2fa.js @@ -1,10 +1,17 @@ Meteor.methods({ - disable2fa() { - + disable2fa(code) { if (!Meteor.userId()) { throw new Meteor.Error('not-authorized'); } + const user = Meteor.user(); + + const verified = RocketChat.TOTP.verify(user.services.totp.secret, code); + + if (!verified) { + return false; + } + return RocketChat.models.Users.disable2FAByUserId(Meteor.userId()); } }); diff --git a/packages/rocketchat-2fa/server/methods/enable2fa.js b/packages/rocketchat-2fa/server/methods/enable2fa.js index 635fbce193ec..f1d765a34458 100644 --- a/packages/rocketchat-2fa/server/methods/enable2fa.js +++ b/packages/rocketchat-2fa/server/methods/enable2fa.js @@ -1,5 +1,3 @@ -const speakeasy = Npm.require('speakeasy'); - Meteor.methods({ enable2fa() { if (!Meteor.userId()) { @@ -8,15 +6,12 @@ Meteor.methods({ const user = Meteor.user(); - const secret = speakeasy.generateSecret(); + const secret = RocketChat.TOTP.generateSecret(); RocketChat.models.Users.disable2FAAndSetTempSecretByUserId(Meteor.userId(), secret.base32); return { - url: speakeasy.otpauthURL({ - secret: secret.ascii, - label: `Rocket.Chat:${ user.username }` - }) + url: RocketChat.TOTP.generateOtpauthURL(secret, user.username) }; } }); diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 290b3cf2ba83..f97d74111bcf 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -452,6 +452,7 @@ "Enable_Svg_Favicon": "Enable SVG favicon", "Encrypted_message": "Encrypted message", "End_OTR": "End OTR", + "Enter_authentication_code": "Enter authentication code", "Enter_Alternative": "Alternative mode (send with Enter + Ctrl/Alt/Shift/CMD)", "Enter_a_regex": "Enter a regex", "Enter_a_room_name": "Enter a room name", @@ -1277,6 +1278,7 @@ "Save_to_enable_this_action": "Save to enable this action", "Saved": "Saved", "Saving": "Saving", + "Scan_QR_code": "Using an authenticator app like Google Authenticator, Authy or Duo, scan the QR code. It will display a 6 digit code which you need to enter below.", "Scope": "Scope", "Screen_Share": "Screen Share", "Script_Enabled": "Script Enabled", @@ -1481,6 +1483,7 @@ "This_room_has_been_archived_by__username_": "This room has been archived by __username__", "This_room_has_been_unarchived_by__username_": "This room has been unarchived by __username__", "Two-factor_authentication": "Two-factor authentication", + "Two-factor_authentication_disabled": "Two-factor authentication disabled", "Two-factor_authentication_enabled": "Two-factor authentication enabled", "Two-factor_authentication_is_currently_disabled": "Two-factor authentication is currently disabled", "Thursday": "Thursday", From 5bd7d0f75d488fc540eed2fd42da3733fec6ce80 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Fri, 24 Mar 2017 16:51:45 -0300 Subject: [PATCH 09/19] Fix ESLint --- packages/rocketchat-2fa/client/TOTPPassword.js | 2 +- packages/rocketchat-2fa/package.js | 9 +-------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/packages/rocketchat-2fa/client/TOTPPassword.js b/packages/rocketchat-2fa/client/TOTPPassword.js index 1c365271b78b..2fb6fa0e171e 100644 --- a/packages/rocketchat-2fa/client/TOTPPassword.js +++ b/packages/rocketchat-2fa/client/TOTPPassword.js @@ -27,7 +27,7 @@ Meteor.loginWithPasswordAndTOTP = function(selector, password, code, callback) { code } }], - userCallback: function(error) { + userCallback(error) { if (error) { reportError(error, callback); } else { diff --git a/packages/rocketchat-2fa/package.js b/packages/rocketchat-2fa/package.js index 0d44f6fbfbf3..78bd78e7e703 100644 --- a/packages/rocketchat-2fa/package.js +++ b/packages/rocketchat-2fa/package.js @@ -1,12 +1,8 @@ Package.describe({ name: 'rocketchat:2fa', version: '0.0.1', - // Brief, one-line summary of the package. summary: '', - // URL to the Git repository containing the source code for this package. git: '', - // By default, Meteor will default to using README.md for documentation. - // To avoid submitting documentation, set this field to null. documentation: 'README.md' }); @@ -16,16 +12,13 @@ Npm.depends({ }); Package.onUse(function(api) { - // api.versionsFrom('1.4.3.1'); - // api.use('ecmascript'); - // api.mainModule('rocketchat-2fa.js'); - api.use([ 'accounts-base', 'ecmascript', 'templating', 'rocketchat:lib' ]); + api.addFiles('client/accountSecurity.html', 'client'); api.addFiles('client/accountSecurity.js', 'client'); api.addFiles('client/TOTPPassword.js', 'client'); From 590a66a26d02b61ce9c03abbd879193bf47637f9 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Fri, 24 Mar 2017 16:59:40 -0300 Subject: [PATCH 10/19] Fix eslint --- packages/rocketchat-2fa/server/lib/totp.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/rocketchat-2fa/server/lib/totp.js b/packages/rocketchat-2fa/server/lib/totp.js index 37c9c84d75b0..0b673e817b03 100644 --- a/packages/rocketchat-2fa/server/lib/totp.js +++ b/packages/rocketchat-2fa/server/lib/totp.js @@ -14,9 +14,9 @@ RocketChat.TOTP = { verify(secret, token) { return speakeasy.totp.verify({ - secret: secret, + secret, encoding: 'base32', - token: token + token }); } }; From 56aa03b718bda7dca27ccbee5b9a820898e17cfa Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Fri, 24 Mar 2017 17:07:53 -0300 Subject: [PATCH 11/19] Add warning for native apps --- packages/rocketchat-2fa/client/accountSecurity.html | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/rocketchat-2fa/client/accountSecurity.html b/packages/rocketchat-2fa/client/accountSecurity.html index b1cee56e51f7..96c9a2633bdb 100644 --- a/packages/rocketchat-2fa/client/accountSecurity.html +++ b/packages/rocketchat-2fa/client/accountSecurity.html @@ -12,6 +12,11 @@

    {{_ "Two-factor_authentication"}}

    +
    + + WARNING: Once you enable this, you will not be able to login on the native mobile apps (Rocket.Chat+) using your password until they implement the 2FA. + +
    {{#if isEnabled}} {{else}} From e93b39e92fea8f4b81bab753f4e08b66271da195 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Fri, 24 Mar 2017 17:47:59 -0300 Subject: [PATCH 12/19] Fix Swal alignment --- packages/rocketchat-theme/client/imports/base.less | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/rocketchat-theme/client/imports/base.less b/packages/rocketchat-theme/client/imports/base.less index 84f01c6201fe..db911aaea06e 100644 --- a/packages/rocketchat-theme/client/imports/base.less +++ b/packages/rocketchat-theme/client/imports/base.less @@ -4783,8 +4783,11 @@ body:not(.is-cordova) { } } -.sweet-alert .sa-input-error { - top: 19px; +.sweet-alert { + margin-left: -239px !important; + .sa-input-error { + top: 19px; + } } .one-passsword { From 9b9df512768b6db0d686c5ba70992e01ae72909b Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Fri, 24 Mar 2017 18:31:06 -0300 Subject: [PATCH 13/19] Implement 2FA backup codes --- .eslintrc | 2 +- .../rocketchat-2fa/client/accountSecurity.js | 5 +++++ packages/rocketchat-2fa/package.js | 4 +++- .../server/methods/verifyTemp2FAToken.js | 16 ++++++++++++---- packages/rocketchat-2fa/server/models/users.js | 5 +++-- 5 files changed, 24 insertions(+), 8 deletions(-) diff --git a/.eslintrc b/.eslintrc index 0dd9f5a3deb2..bad1f2e3cecc 100644 --- a/.eslintrc +++ b/.eslintrc @@ -71,7 +71,7 @@ "curly": [2, "all"], "eqeqeq": [2, "allow-null"], "new-cap": [2, { - "capIsNewExceptions": ["Match.Optional", "Match.Maybe", "Match.ObjectIncluding", "Push.Configure"] + "capIsNewExceptions": ["Match.Optional", "Match.Maybe", "Match.ObjectIncluding", "Push.Configure", "SHA256"] }], "use-isnan": 2, "valid-typeof": 2, diff --git a/packages/rocketchat-2fa/client/accountSecurity.js b/packages/rocketchat-2fa/client/accountSecurity.js index 5ef8b925acb8..c54dcaccec72 100644 --- a/packages/rocketchat-2fa/client/accountSecurity.js +++ b/packages/rocketchat-2fa/client/accountSecurity.js @@ -66,6 +66,11 @@ Template.accountSecurity.events({ Meteor.call('verifyTemp2FAToken', instance.find('#testCode').value, (error, result) => { if (result) { + swal({ + title: t('Backup_codes'), + text: t('Make_sure_you_have_a_copy_of_your_codes') + '\n' + result.codes.join(' ') + }); + instance.find('#testCode').value = ''; instance.state.set(); toastr.success(t('Two-factor_authentication_enabled')); diff --git a/packages/rocketchat-2fa/package.js b/packages/rocketchat-2fa/package.js index 78bd78e7e703..3bc768179b1a 100644 --- a/packages/rocketchat-2fa/package.js +++ b/packages/rocketchat-2fa/package.js @@ -16,7 +16,9 @@ Package.onUse(function(api) { 'accounts-base', 'ecmascript', 'templating', - 'rocketchat:lib' + 'rocketchat:lib', + 'sha', + 'random' ]); api.addFiles('client/accountSecurity.html', 'client'); diff --git a/packages/rocketchat-2fa/server/methods/verifyTemp2FAToken.js b/packages/rocketchat-2fa/server/methods/verifyTemp2FAToken.js index 04a5d112409f..d42a7089bb8b 100644 --- a/packages/rocketchat-2fa/server/methods/verifyTemp2FAToken.js +++ b/packages/rocketchat-2fa/server/methods/verifyTemp2FAToken.js @@ -9,8 +9,7 @@ Meteor.methods({ const user = Meteor.user(); if (!user.services || !user.services.totp || !user.services.totp.tempSecret) { - console.error('errour'); - return false; + throw new Meteor.Error('invalid-totp'); } const verified = speakeasy.totp.verify({ @@ -20,8 +19,17 @@ Meteor.methods({ }); if (verified) { - RocketChat.models.Users.enable2FAAndSetSecretByUserId(Meteor.userId(), user.services.totp.tempSecret); + // generate 10 backup codes + const codes = []; + const hashedCodes = []; + for (let i = 0; i < 10; i++) { + const code = Random.id(8); + codes.push(code); + hashedCodes.push(SHA256(code)); + } + + RocketChat.models.Users.enable2FAAndSetSecretAndCodesByUserId(Meteor.userId(), user.services.totp.tempSecret, hashedCodes); + return { codes }; } - return verified; } }); diff --git a/packages/rocketchat-2fa/server/models/users.js b/packages/rocketchat-2fa/server/models/users.js index 470209c82dfd..d6102c276dee 100644 --- a/packages/rocketchat-2fa/server/models/users.js +++ b/packages/rocketchat-2fa/server/models/users.js @@ -11,13 +11,14 @@ RocketChat.models.Users.disable2FAAndSetTempSecretByUserId = function(userId, te }); }; -RocketChat.models.Users.enable2FAAndSetSecretByUserId = function(userId, secret) { +RocketChat.models.Users.enable2FAAndSetSecretAndCodesByUserId = function(userId, secret, backupCodes) { return this.update({ _id: userId }, { $set: { 'services.totp.enabled': true, - 'services.totp.secret': secret + 'services.totp.secret': secret, + 'services.totp.hashedBackup': backupCodes }, $unset: { 'services.totp.tempSecret': 1 From aacf48518f9754ac7761670cf0ba0870b9d201e0 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Sat, 25 Mar 2017 18:30:19 -0300 Subject: [PATCH 14/19] Lint fixes --- packages/rocketchat-2fa/client/accountSecurity.js | 2 +- packages/rocketchat-theme/client/imports/base.less | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/rocketchat-2fa/client/accountSecurity.js b/packages/rocketchat-2fa/client/accountSecurity.js index c54dcaccec72..558bae3fea24 100644 --- a/packages/rocketchat-2fa/client/accountSecurity.js +++ b/packages/rocketchat-2fa/client/accountSecurity.js @@ -68,7 +68,7 @@ Template.accountSecurity.events({ if (result) { swal({ title: t('Backup_codes'), - text: t('Make_sure_you_have_a_copy_of_your_codes') + '\n' + result.codes.join(' ') + text: `${ t('Make_sure_you_have_a_copy_of_your_codes') }\n${ result.codes.join(' ') }` }); instance.find('#testCode').value = ''; diff --git a/packages/rocketchat-theme/client/imports/base.less b/packages/rocketchat-theme/client/imports/base.less index db911aaea06e..98caaa7528d0 100644 --- a/packages/rocketchat-theme/client/imports/base.less +++ b/packages/rocketchat-theme/client/imports/base.less @@ -4785,6 +4785,7 @@ body:not(.is-cordova) { .sweet-alert { margin-left: -239px !important; + .sa-input-error { top: 19px; } From 23d1b9418a79231f653d0a0e0c07b66ae58c3dc8 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Mon, 27 Mar 2017 16:37:11 -0300 Subject: [PATCH 15/19] Add UI to regenerate 2FA backup codes --- .../client/accountSecurity.html | 13 ++++ .../rocketchat-2fa/client/accountSecurity.js | 59 +++++++++++++++++-- packages/rocketchat-2fa/package.js | 2 + packages/rocketchat-2fa/server/lib/totp.js | 44 ++++++++++++-- .../rocketchat-2fa/server/loginHandler.js | 7 ++- .../server/methods/checkCodesRemaining.js | 17 ++++++ .../server/methods/disable2fa.js | 7 ++- .../server/methods/regenerateCodes.js | 27 +++++++++ .../server/methods/verifyTemp2FAToken.js | 14 +---- .../rocketchat-2fa/server/models/users.js | 10 ++++ packages/rocketchat-i18n/i18n/en.i18n.json | 4 ++ 11 files changed, 180 insertions(+), 24 deletions(-) create mode 100644 packages/rocketchat-2fa/server/methods/checkCodesRemaining.js create mode 100644 packages/rocketchat-2fa/server/methods/regenerateCodes.js diff --git a/packages/rocketchat-2fa/client/accountSecurity.html b/packages/rocketchat-2fa/client/accountSecurity.html index 96c9a2633bdb..9ed51e09d061 100644 --- a/packages/rocketchat-2fa/client/accountSecurity.html +++ b/packages/rocketchat-2fa/client/accountSecurity.html @@ -38,6 +38,19 @@

    {{_ "Two-factor_authentication"}}

    + + + {{#if isEnabled}} +
    +
    +

    {{_ "Backup_codes"}}

    +
    +

    {{codesRemaining}}

    + +
    +
    +
    + {{/if}} diff --git a/packages/rocketchat-2fa/client/accountSecurity.js b/packages/rocketchat-2fa/client/accountSecurity.js index 558bae3fea24..40e61acc259a 100644 --- a/packages/rocketchat-2fa/client/accountSecurity.js +++ b/packages/rocketchat-2fa/client/accountSecurity.js @@ -16,6 +16,11 @@ Template.accountSecurity.helpers({ }, isRegistering() { return Template.instance().state.get() === 'registering'; + }, + codesRemaining() { + if (Template.instance().codesRemaining.get()) { + return t('You_have_n_codes_remaining', { number: Template.instance().codesRemaining.get() }); + } } }); @@ -66,10 +71,7 @@ Template.accountSecurity.events({ Meteor.call('verifyTemp2FAToken', instance.find('#testCode').value, (error, result) => { if (result) { - swal({ - title: t('Backup_codes'), - text: `${ t('Make_sure_you_have_a_copy_of_your_codes') }\n${ result.codes.join(' ') }` - }); + instance.showBackupCodes(result.codes); instance.find('#testCode').value = ''; instance.state.set(); @@ -78,6 +80,35 @@ Template.accountSecurity.events({ toastr.error(t('Invalid_two_factor_code')); } }); + }, + + 'click .regenerate-codes'(event, instance) { + swal({ + title: t('Two-factor_authentication'), + text: t('Open_your_authentication_app_and_enter_the_code'), + type: 'input', + inputType: 'text', + showCancelButton: true, + closeOnConfirm: false, + confirmButtonText: t('Verify'), + cancelButtonText: t('Cancel') + }, (code) => { + if (code === false) { + return; + } + + Meteor.call('2fa:regenerateCodes', code, (error, result) => { + if (error) { + return toastr.error(t(error.error)); + } + + if (result) { + instance.showBackupCodes(result.codes); + } else { + return toastr.error(t('Invalid_two_factor_code')); + } + }); + }); } }); @@ -86,4 +117,24 @@ Template.accountSecurity.onCreated(function() { this.imageData = new ReactiveVar(); this.state = new ReactiveVar(); + + this.codesRemaining = new ReactiveVar(); + + this.showBackupCodes = (userCodes) => { + const backupCodes = userCodes.map((value, index) => { + return (index + 1) % 4 === 0 && index < 11 ? `${ value }\n` : `${ value } `; + }).join(''); + const codes = `${ backupCodes }`; + swal({ + title: t('Backup_codes'), + text: `${ t('Make_sure_you_have_a_copy_of_your_codes', { codes }) }`, + html: true + }); + }; + + Meteor.call('2fa:checkCodesRemaining', (error, result) => { + if (result) { + this.codesRemaining.set(result.remaining); + } + }); }); diff --git a/packages/rocketchat-2fa/package.js b/packages/rocketchat-2fa/package.js index 3bc768179b1a..b9d27effa327 100644 --- a/packages/rocketchat-2fa/package.js +++ b/packages/rocketchat-2fa/package.js @@ -27,8 +27,10 @@ Package.onUse(function(api) { api.addFiles('server/lib/totp.js', 'server'); + api.addFiles('server/methods/checkCodesRemaining.js', 'server'); api.addFiles('server/methods/disable2fa.js', 'server'); api.addFiles('server/methods/enable2fa.js', 'server'); + api.addFiles('server/methods/regenerateCodes.js', 'server'); api.addFiles('server/methods/verifyTemp2FAToken.js', 'server'); api.addFiles('server/models/users.js', 'server'); diff --git a/packages/rocketchat-2fa/server/lib/totp.js b/packages/rocketchat-2fa/server/lib/totp.js index 0b673e817b03..bf81dd043cd0 100644 --- a/packages/rocketchat-2fa/server/lib/totp.js +++ b/packages/rocketchat-2fa/server/lib/totp.js @@ -12,11 +12,43 @@ RocketChat.TOTP = { }); }, - verify(secret, token) { - return speakeasy.totp.verify({ - secret, - encoding: 'base32', - token - }); + verify({ secret, token, backupTokens, userId }) { + let verified; + + // validates a backup code + if (token.length === 8 && backupTokens) { + const hashedCode = SHA256(token); + const usedCode = backupTokens.indexOf(hashedCode); + + if (usedCode !== -1) { + verified = true; + + backupTokens.splice(usedCode, 1); + + // mark the code as used (remove it from the list) + RocketChat.models.Users.update2FABackupCodesByUserId(userId, backupTokens); + } + } else { + verified = speakeasy.totp.verify({ + secret, + encoding: 'base32', + token + }); + } + + return verified; + }, + + generateCodes() { + // generate 12 backup codes + const codes = []; + const hashedCodes = []; + for (let i = 0; i < 12; i++) { + const code = Random.id(8); + codes.push(code); + hashedCodes.push(SHA256(code)); + } + + return { codes, hashedCodes }; } }; diff --git a/packages/rocketchat-2fa/server/loginHandler.js b/packages/rocketchat-2fa/server/loginHandler.js index 1b486538d95a..650b1f9d6df5 100644 --- a/packages/rocketchat-2fa/server/loginHandler.js +++ b/packages/rocketchat-2fa/server/loginHandler.js @@ -14,7 +14,12 @@ RocketChat.callbacks.add('onValidateLogin', (login) => { throw new Meteor.Error('totp-required', 'TOTP Required'); } - const verified = RocketChat.TOTP.verify(login.user.services.totp.secret, totp.code); + const verified = RocketChat.TOTP.verify({ + secret: login.user.services.totp.secret, + token: totp.code, + userId: login.user._id, + backupTokens: login.user.services.totp.hashedBackup + }); if (verified !== true) { throw new Meteor.Error('totp-invalid', 'TOTP Invalid'); diff --git a/packages/rocketchat-2fa/server/methods/checkCodesRemaining.js b/packages/rocketchat-2fa/server/methods/checkCodesRemaining.js new file mode 100644 index 000000000000..8d2224d61726 --- /dev/null +++ b/packages/rocketchat-2fa/server/methods/checkCodesRemaining.js @@ -0,0 +1,17 @@ +Meteor.methods({ + '2fa:checkCodesRemaining'() { + if (!Meteor.userId()) { + throw new Meteor.Error('not-authorized'); + } + + const user = Meteor.user(); + + if (!user.services || !user.services.totp || !user.services.totp.enabled) { + throw new Meteor.Error('invalid-totp'); + } + + return { + remaining: user.services.totp.hashedBackup.length + }; + } +}); diff --git a/packages/rocketchat-2fa/server/methods/disable2fa.js b/packages/rocketchat-2fa/server/methods/disable2fa.js index 47e9512dba50..0fdeca3c0f79 100644 --- a/packages/rocketchat-2fa/server/methods/disable2fa.js +++ b/packages/rocketchat-2fa/server/methods/disable2fa.js @@ -6,7 +6,12 @@ Meteor.methods({ const user = Meteor.user(); - const verified = RocketChat.TOTP.verify(user.services.totp.secret, code); + const verified = RocketChat.TOTP.verify({ + secret: user.services.totp.secret, + token: code, + userId: Meteor.userId(), + backupTokens: user.services.totp.hashedBackup + }); if (!verified) { return false; diff --git a/packages/rocketchat-2fa/server/methods/regenerateCodes.js b/packages/rocketchat-2fa/server/methods/regenerateCodes.js new file mode 100644 index 000000000000..24f9a4e5973f --- /dev/null +++ b/packages/rocketchat-2fa/server/methods/regenerateCodes.js @@ -0,0 +1,27 @@ +Meteor.methods({ + '2fa:regenerateCodes'(userToken) { + if (!Meteor.userId()) { + throw new Meteor.Error('not-authorized'); + } + + const user = Meteor.user(); + + if (!user.services || !user.services.totp || !user.services.totp.enabled) { + throw new Meteor.Error('invalid-totp'); + } + + const verified = RocketChat.TOTP.verify({ + secret: user.services.totp.secret, + token: userToken, + userId: Meteor.userId(), + backupTokens: user.services.totp.hashedBackup + }); + + if (verified) { + const { codes, hashedCodes } = RocketChat.TOTP.generateCodes(); + + RocketChat.models.Users.update2FABackupCodesByUserId(Meteor.userId(), hashedCodes); + return { codes }; + } + } +}); diff --git a/packages/rocketchat-2fa/server/methods/verifyTemp2FAToken.js b/packages/rocketchat-2fa/server/methods/verifyTemp2FAToken.js index d42a7089bb8b..eaa6c6f91b55 100644 --- a/packages/rocketchat-2fa/server/methods/verifyTemp2FAToken.js +++ b/packages/rocketchat-2fa/server/methods/verifyTemp2FAToken.js @@ -1,5 +1,3 @@ -const speakeasy = Npm.require('speakeasy'); - Meteor.methods({ verifyTemp2FAToken(userToken) { if (!Meteor.userId()) { @@ -12,21 +10,13 @@ Meteor.methods({ throw new Meteor.Error('invalid-totp'); } - const verified = speakeasy.totp.verify({ + const verified = RocketChat.TOTP.verify({ secret: user.services.totp.tempSecret, - encoding: 'base32', token: userToken }); if (verified) { - // generate 10 backup codes - const codes = []; - const hashedCodes = []; - for (let i = 0; i < 10; i++) { - const code = Random.id(8); - codes.push(code); - hashedCodes.push(SHA256(code)); - } + const { codes, hashedCodes } = RocketChat.TOTP.generateCodes(); RocketChat.models.Users.enable2FAAndSetSecretAndCodesByUserId(Meteor.userId(), user.services.totp.tempSecret, hashedCodes); return { codes }; diff --git a/packages/rocketchat-2fa/server/models/users.js b/packages/rocketchat-2fa/server/models/users.js index d6102c276dee..ba90d2673861 100644 --- a/packages/rocketchat-2fa/server/models/users.js +++ b/packages/rocketchat-2fa/server/models/users.js @@ -37,3 +37,13 @@ RocketChat.models.Users.disable2FAByUserId = function(userId) { } }); }; + +RocketChat.models.Users.update2FABackupCodesByUserId = function(userId, backupCodes) { + return this.update({ + _id: userId + }, { + $set: { + 'services.totp.hashedBackup': backupCodes + } + }); +}; diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index f97d74111bcf..6e235d038253 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -244,6 +244,7 @@ "Back_to_integration_detail": "Back to the integration detail", "Back_to_login": "Back to login", "Back_to_permissions": "Back to permissions", + "Backup_codes": "Backup codes", "Beta_feature_Depends_on_Video_Conference_to_be_enabled": "Beta feature. Depends on Video Conference to be enabled.", "Block_User": "Block User", "Body": "Body", @@ -927,6 +928,7 @@ "Mailer_body_tags": "You must use [unsubscribe] for the unsubscription link.
    You may use [name], [fname], [lname] for the user's full name, first name or last name, respectively.
    You may use [email] for the user's email.", "Mailing": "Mailing", "Make_Admin": "Make Admin", + "Make_sure_you_have_a_copy_of_your_codes": "Make sure you have a copy of your codes: __codes__ If you lose access to your authenticator app, you can use one of these codes to log in.", "Manager_added": "Manager added", "Manager_removed": "Manager removed", "Managing_assets": "Managing assets", @@ -1200,6 +1202,7 @@ "Refresh_oauth_services": "Refresh OAuth Services", "Refresh_keys": "Refresh keys", "Refresh_your_page_after_install_to_enable_screen_sharing": "Refresh your page after install to enable screen sharing", + "Regenerate_codes": "Regenerate codes", "Register": "Register a new account", "Registration": "Registration", "Registration_Succeeded": "Registration Succeeded", @@ -1662,6 +1665,7 @@ "You_can_use_webhooks_to_easily_integrate_livechat_with_your_CRM": "You can use webhooks to easily integrate livechat with your CRM.", "You_cant_leave_a_livechat_room_Please_use_the_close_button": "You can't leave a livechat room. Please, use the close button.", "You_have_been_muted": "You have been muted and cannot speak in this room", + "You_have_n_codes_remaining": "You have __number__ codes remaining.", "You_have_not_verified_your_email": "You have not verified your email.", "You_have_successfully_unsubscribed": "You have successfully unsubscribed from our Mailling List.", "You_must_join_to_view_messages_in_this_channel": "You must join to view messages in this channel", From 8f3ebaccc346863bff3680a41977b114667a23cb Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Mon, 27 Mar 2017 17:46:24 -0300 Subject: [PATCH 16/19] Rename 2FA methods --- packages/rocketchat-2fa/client/accountSecurity.js | 6 +++--- packages/rocketchat-2fa/package.js | 6 +++--- .../server/methods/{disable2fa.js => disable.js} | 2 +- .../server/methods/{enable2fa.js => enable.js} | 2 +- .../methods/{verifyTemp2FAToken.js => validateTempToken.js} | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) rename packages/rocketchat-2fa/server/methods/{disable2fa.js => disable.js} (94%) rename packages/rocketchat-2fa/server/methods/{enable2fa.js => enable.js} (95%) rename packages/rocketchat-2fa/server/methods/{verifyTemp2FAToken.js => validateTempToken.js} (94%) diff --git a/packages/rocketchat-2fa/client/accountSecurity.js b/packages/rocketchat-2fa/client/accountSecurity.js index 40e61acc259a..943c0235a1b4 100644 --- a/packages/rocketchat-2fa/client/accountSecurity.js +++ b/packages/rocketchat-2fa/client/accountSecurity.js @@ -26,7 +26,7 @@ Template.accountSecurity.helpers({ Template.accountSecurity.events({ 'click .enable-2fa'(event, instance) { - Meteor.call('enable2fa', (error, result) => { + Meteor.call('2fa:enable', (error, result) => { instance.imageData.set(qrcode(result.url, { size: 200 })); instance.state.set('registering'); @@ -52,7 +52,7 @@ Template.accountSecurity.events({ return; } - Meteor.call('disable2fa', code, (error, result) => { + Meteor.call('2fa:disable', code, (error, result) => { if (error) { return toastr.error(t(error.error)); } @@ -69,7 +69,7 @@ Template.accountSecurity.events({ 'submit .verify-code'(event, instance) { event.preventDefault(); - Meteor.call('verifyTemp2FAToken', instance.find('#testCode').value, (error, result) => { + Meteor.call('2fa:validateTempToken', instance.find('#testCode').value, (error, result) => { if (result) { instance.showBackupCodes(result.codes); diff --git a/packages/rocketchat-2fa/package.js b/packages/rocketchat-2fa/package.js index b9d27effa327..3a8bdc696b82 100644 --- a/packages/rocketchat-2fa/package.js +++ b/packages/rocketchat-2fa/package.js @@ -28,10 +28,10 @@ Package.onUse(function(api) { api.addFiles('server/lib/totp.js', 'server'); api.addFiles('server/methods/checkCodesRemaining.js', 'server'); - api.addFiles('server/methods/disable2fa.js', 'server'); - api.addFiles('server/methods/enable2fa.js', 'server'); + api.addFiles('server/methods/disable.js', 'server'); + api.addFiles('server/methods/enable.js', 'server'); api.addFiles('server/methods/regenerateCodes.js', 'server'); - api.addFiles('server/methods/verifyTemp2FAToken.js', 'server'); + api.addFiles('server/methods/validateTempToken.js', 'server'); api.addFiles('server/models/users.js', 'server'); diff --git a/packages/rocketchat-2fa/server/methods/disable2fa.js b/packages/rocketchat-2fa/server/methods/disable.js similarity index 94% rename from packages/rocketchat-2fa/server/methods/disable2fa.js rename to packages/rocketchat-2fa/server/methods/disable.js index 0fdeca3c0f79..f8cb63d3dbef 100644 --- a/packages/rocketchat-2fa/server/methods/disable2fa.js +++ b/packages/rocketchat-2fa/server/methods/disable.js @@ -1,5 +1,5 @@ Meteor.methods({ - disable2fa(code) { + '2fa:disable'(code) { if (!Meteor.userId()) { throw new Meteor.Error('not-authorized'); } diff --git a/packages/rocketchat-2fa/server/methods/enable2fa.js b/packages/rocketchat-2fa/server/methods/enable.js similarity index 95% rename from packages/rocketchat-2fa/server/methods/enable2fa.js rename to packages/rocketchat-2fa/server/methods/enable.js index f1d765a34458..3c690089b7cf 100644 --- a/packages/rocketchat-2fa/server/methods/enable2fa.js +++ b/packages/rocketchat-2fa/server/methods/enable.js @@ -1,5 +1,5 @@ Meteor.methods({ - enable2fa() { + '2fa:enable'() { if (!Meteor.userId()) { throw new Meteor.Error('not-authorized'); } diff --git a/packages/rocketchat-2fa/server/methods/verifyTemp2FAToken.js b/packages/rocketchat-2fa/server/methods/validateTempToken.js similarity index 94% rename from packages/rocketchat-2fa/server/methods/verifyTemp2FAToken.js rename to packages/rocketchat-2fa/server/methods/validateTempToken.js index eaa6c6f91b55..482ccfde0098 100644 --- a/packages/rocketchat-2fa/server/methods/verifyTemp2FAToken.js +++ b/packages/rocketchat-2fa/server/methods/validateTempToken.js @@ -1,5 +1,5 @@ Meteor.methods({ - verifyTemp2FAToken(userToken) { + '2fa:validateTempToken'(userToken) { if (!Meteor.userId()) { throw new Meteor.Error('not-authorized'); } From 77c239f3b7fc09b4c88fd149132e0ff95c4e973e Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Tue, 28 Mar 2017 08:53:17 -0300 Subject: [PATCH 17/19] Add mention to backup notes to translation --- packages/rocketchat-i18n/i18n/en.i18n.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 6e235d038253..6c4a9b53e75a 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -1100,7 +1100,7 @@ "optional": "optional", "Use_minor_colors": "Use minor color palette (defaults inherit major colors)", "or": "or", - "Open_your_authentication_app_and_enter_the_code": "Open your authentication app and enter the code", + "Open_your_authentication_app_and_enter_the_code": "Open your authentication app and enter the code. You can also use one of your backup codes.", "Order": "Order", "OS_Arch": "OS Arch", "OS_Cpus": "OS CPU Count", From 8abc137a75aa127de0b78f388c475407e46240a7 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Wed, 29 Mar 2017 16:08:18 -0300 Subject: [PATCH 18/19] Fix backup codes remaining not showing --- packages/rocketchat-2fa/client/accountSecurity.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/rocketchat-2fa/client/accountSecurity.js b/packages/rocketchat-2fa/client/accountSecurity.js index 943c0235a1b4..40543df58297 100644 --- a/packages/rocketchat-2fa/client/accountSecurity.js +++ b/packages/rocketchat-2fa/client/accountSecurity.js @@ -132,9 +132,14 @@ Template.accountSecurity.onCreated(function() { }); }; - Meteor.call('2fa:checkCodesRemaining', (error, result) => { - if (result) { - this.codesRemaining.set(result.remaining); + this.autorun(() => { + const user = Meteor.user(); + if (user && user.services && user.services.totp && user.services.totp.enabled) { + Meteor.call('2fa:checkCodesRemaining', (error, result) => { + if (result) { + this.codesRemaining.set(result.remaining); + } + }); } }); }); From af873c32104fd0e0a9b7b8a2cbf20ea337d3e230 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Wed, 29 Mar 2017 17:12:07 -0300 Subject: [PATCH 19/19] Add to HISTORY.md --- HISTORY.md | 1 + 1 file changed, 1 insertion(+) diff --git a/HISTORY.md b/HISTORY.md index d1dbaa0b2f32..f9d83f5e4040 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -4,6 +4,7 @@ - [NEW] Permission `join-without-join-code` assigned to admins and bots by default (#6139) - [NEW] Integrations, both incoming and outgoing, now have access to the models. Example: `Users.findOneById(id)` (#6336) +- [NEW] Option to enable `Two Factor Authentication` in user's account preference - [FIX] Incoming integrations would break when trying to use the `Store` feature. ## 0.54.2 - 2017-Mar-24