Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NEW] Two Factor Auth #6476

Merged
merged 24 commits into from
Mar 29, 2017
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
5079f5d
Initial implementation of two-factor authentication
sampaiodiego Mar 24, 2017
843d061
Init TOTP login verification
rodrigok Mar 24, 2017
5e94f40
Implement intial 2FA validation
rodrigok Mar 24, 2017
749c3c6
Add verification step for two-factor
sampaiodiego Mar 24, 2017
43a3021
Add ability to disable two-factor authentication
sampaiodiego Mar 24, 2017
afa8489
Integrate login with 2FA TOTP
rodrigok Mar 24, 2017
7b62d7a
Merge branch 'improvements/2fa-implementation' of https://github.com/…
rodrigok Mar 24, 2017
ba74f9b
Improve 2FA interface
rodrigok Mar 24, 2017
1617fde
Validates current two-factor for disabling
sampaiodiego Mar 24, 2017
4bf9e16
Merge remote-tracking branch 'origin/develop' into improvements/2fa-i…
rodrigok Mar 24, 2017
5bd7d0f
Fix ESLint
rodrigok Mar 24, 2017
590a66a
Fix eslint
sampaiodiego Mar 24, 2017
56aa03b
Add warning for native apps
rodrigok Mar 24, 2017
e0614dd
Merge branch 'improvements/2fa-implementation' of https://github.com/…
rodrigok Mar 24, 2017
e93b39e
Fix Swal alignment
rodrigok Mar 24, 2017
9b9df51
Implement 2FA backup codes
sampaiodiego Mar 24, 2017
3b960d4
Merge branch 'improvements/2fa-implementation' of github.com:RocketCh…
sampaiodiego Mar 24, 2017
aacf485
Lint fixes
rodrigok Mar 25, 2017
23d1b94
Add UI to regenerate 2FA backup codes
sampaiodiego Mar 27, 2017
8f3ebac
Rename 2FA methods
sampaiodiego Mar 27, 2017
77c239f
Add mention to backup notes to translation
sampaiodiego Mar 28, 2017
8abc137
Fix backup codes remaining not showing
sampaiodiego Mar 29, 2017
2641efd
Merge remote-tracking branch 'origin/develop' into improvements/2fa-i…
rodrigok Mar 29, 2017
af873c3
Add to HISTORY.md
rodrigok Mar 29, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -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,
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 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we change this a bit? Maybe something like:

WARNING: Please make sure your mobile app supports 2FA authentication before enabling. See <a href="https://github.com/RocketChat/Rocket.Chat.iOS/issues/375">Rocket.Chat+ iOS</a> or <a href="https://github.com/RocketChat/Rocket.Chat.Android/issues/248">Rocket.Chat+ Android</a> for current status.

WARNING: Please make sure your mobile app supports 2FA authentication before enabling. See Rocket.Chat+ iOS or Rocket.Chat+ Android for current status.

Feels less critical :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this was only meant to be temporary until the mobile applications actually support it, which is why it isn't translated and is only in english.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not too worried about the translation. If for what ever reason this makes it to a release before its supported. This is much better lingo to have its less critical toward the mobile guys :)

</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>
140 changes: 140 additions & 0 deletions packages/rocketchat-2fa/client/accountSecurity.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
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
});
};

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