Skip to content

Commit

Permalink
Merge pull request #6476 from RocketChat/improvements/2fa-implementation
Browse files Browse the repository at this point in the history
[NEW] Two Factor Auth
  • Loading branch information
engelgabriel authored Mar 29, 2017
2 parents 732feb6 + af873c3 commit 7ff735d
Show file tree
Hide file tree
Showing 26 changed files with 619 additions and 10 deletions.
2 changes: 1 addition & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,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,
Expand Down
1 change: 1 addition & 0 deletions .meteor/packages
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions .meteor/versions
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
- [FIX] Outgoing webhooks which have an error and they're retrying would still retry even if the integration was disabled. (#4835)

Expand Down
1 change: 1 addition & 0 deletions packages/rocketchat-2fa/.npm/package/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
7 changes: 7 additions & 0 deletions packages/rocketchat-2fa/.npm/package/README
Original file line number Diff line number Diff line change
@@ -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.
19 changes: 19 additions & 0 deletions packages/rocketchat-2fa/.npm/package/npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Empty file.
72 changes: 72 additions & 0 deletions packages/rocketchat-2fa/client/TOTPPassword.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
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(error) {
if (error) {
reportError(error, callback);
} else {
callback && callback();
}
}
});
};

const loginWithPassword = Meteor.loginWithPassword;

Meteor.loginWithPassword = function(email, password, cb) {
loginWithPassword(email, password, (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: true,
confirmButtonText: t('Verify'),
cancelButtonText: t('Cancel')
}, (code) => {
if (code === false) {
return cb();
}

Meteor.loginWithPasswordAndTOTP(email, password, code, (error) => {
if (error && error.error === 'totp-invalid') {
toastr.error(t('Invalid_two_factor_code'));
cb();
} else {
cb(error);
}
});
});
});
};
57 changes: 57 additions & 0 deletions packages/rocketchat-2fa/client/accountSecurity.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<template name="accountSecurity">
<section class="page-container page-home page-static">
<header class="fixed-title border-component-color">
{{> burger}}
<h2>
<span class="room-title">{{_ "Security"}}</span>
</h2>
</header>
<div class="content">
<div class="rocket-form">
<fieldset>
<div class="section">
<h1>{{_ "Two-factor_authentication"}}</h1>
<div class="section-content border-component-color">
<div class="alert pending-background pending-color pending-border">
<strong>
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.
</strong>
</div>
{{#if isEnabled}}
<button class="button danger disable-2fa">{{_ "Disable_two-factor_authentication"}}</button>
{{else}}
{{#unless isRegistering}}
<p>{{_ "Two-factor_authentication_is_currently_disabled"}}</p>

<button class="button primary enable-2fa">{{_ "Enable_two-factor_authentication"}}</button>
{{else}}
<p>{{_ "Scan_QR_code"}}</p>

<img src="{{imageData}}">

<form class="inline verify-code">
<input type="text" id="testCode" placeholder="{{_ "Enter_authentication_code"}}">
<button type="submit" class="button primary">{{_ "Verify"}}</button>
</form>
{{/unless}}
{{/if}}
</div>
</div>
</fieldset>


{{#if isEnabled}}
<fieldset>
<div class="section">
<h1>{{_ "Backup_codes"}}</h1>
<div class="section-content border-component-color">
<p>{{codesRemaining}}</p>
<button class="button regenerate-codes">{{_ "Regenerate_codes"}}</button>
</div>
</div>
</fieldset>
{{/if}}
</div>
</div>
</section>
</template>
145 changes: 145 additions & 0 deletions packages/rocketchat-2fa/client/accountSecurity.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import toastr from 'toastr';
import qrcode from 'yaqrcode';

window.qrcode = qrcode;

Template.accountSecurity.helpers({
showImage() {
return Template.instance().showImage.get();
},
imageData() {
return Template.instance().imageData.get();
},
isEnabled() {
const user = Meteor.user();
return user && user.services && user.services.totp && user.services.totp.enabled;
},
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() });
}
}
});

Template.accountSecurity.events({
'click .enable-2fa'(event, instance) {
Meteor.call('2fa:enable', (error, result) => {
instance.imageData.set(qrcode(result.url, { size: 200 }));

instance.state.set('registering');

Meteor.defer(() => {
instance.find('#testCode').focus();
});
});
},

'click .disable-2fa'() {
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('2fa:disable', 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'));
}
});
});
},

'submit .verify-code'(event, instance) {
event.preventDefault();

Meteor.call('2fa:validateTempToken', instance.find('#testCode').value, (error, result) => {
if (result) {
instance.showBackupCodes(result.codes);

instance.find('#testCode').value = '';
instance.state.set();
toastr.success(t('Two-factor_authentication_enabled'));
} else {
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'));
}
});
});
}
});

Template.accountSecurity.onCreated(function() {
this.showImage = new ReactiveVar(false);
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 = `<code class="text-center allow-text-selection">${ backupCodes }</code>`;
swal({
title: t('Backup_codes'),
text: `${ t('Make_sure_you_have_a_copy_of_your_codes', { codes }) }`,
html: true
});
};

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);
}
});
}
});
});
39 changes: 39 additions & 0 deletions packages/rocketchat-2fa/package.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
Package.describe({
name: 'rocketchat:2fa',
version: '0.0.1',
summary: '',
git: '',
documentation: 'README.md'
});

Npm.depends({
speakeasy: '2.0.0',
yaqrcode: '0.2.1'
});

Package.onUse(function(api) {
api.use([
'accounts-base',
'ecmascript',
'templating',
'rocketchat:lib',
'sha',
'random'
]);

api.addFiles('client/accountSecurity.html', 'client');
api.addFiles('client/accountSecurity.js', 'client');
api.addFiles('client/TOTPPassword.js', 'client');

api.addFiles('server/lib/totp.js', 'server');

api.addFiles('server/methods/checkCodesRemaining.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/validateTempToken.js', 'server');

api.addFiles('server/models/users.js', 'server');

api.addFiles('server/loginHandler.js', 'server');
});
Loading

0 comments on commit 7ff735d

Please sign in to comment.