[NEW] Two Factor authentication via email (#15949)

pull/16557/head^2
Rodrigo Nascimento 6 years ago committed by GitHub
parent 93d1014759
commit 3813aefc8a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 23
      .eslintrc
  2. 42
      app/2fa/client/TOTPPassword.js
  3. 74
      app/2fa/client/accountSecurity.html
  4. 28
      app/2fa/client/accountSecurity.js
  5. 91
      app/2fa/client/callWithTwoFactorRequired.js
  6. 1
      app/2fa/client/index.js
  7. 17
      app/2fa/server/MethodInvocationOverride.js
  8. 138
      app/2fa/server/code/EmailCheck.ts
  9. 17
      app/2fa/server/code/ICodeCheck.ts
  10. 38
      app/2fa/server/code/PasswordCheckFallback.ts
  11. 36
      app/2fa/server/code/TOTPCheck.ts
  12. 156
      app/2fa/server/code/index.ts
  13. 1
      app/2fa/server/index.js
  14. 25
      app/2fa/server/loginHandler.js
  15. 29
      app/2fa/server/startup/settings.js
  16. 36
      app/2fa/server/twoFactorRequired.ts
  17. 36
      app/api/server/api.js
  18. 49
      app/api/server/v1/users.js
  19. 4
      app/blockstack/server/userHandler.js
  20. 5
      app/cas/server/cas_server.js
  21. 2
      app/crowd/server/crowd.js
  22. 6
      app/ldap/server/sync.js
  23. 4
      app/lib/client/lib/index.js
  24. 5
      app/lib/server/methods/insertOrUpdateUser.js
  25. 3
      app/lib/server/startup/settings.js
  26. 5
      app/meteor-accounts-saml/server/saml_server.js
  27. 72
      app/models/server/models/Users.js
  28. 3
      app/settings/server/functions/settings.d.ts
  29. 3
      app/ui-utils/client/lib/modal.html
  30. 4
      app/ui-utils/client/lib/modal.js
  31. 38
      app/utils/client/lib/RestApiClient.js
  32. 7
      app/utils/client/lib/handleError.js
  33. 1
      app/utils/server/functions/getDefaultUserFields.js
  34. 18
      client/components/setupWizard/steps/SettingsBasedStep.js
  35. 16
      definition/IMethodThisType.ts
  36. 5
      definition/IMethodType.ts
  37. 100
      definition/IUser.ts
  38. 19
      imports/personal-access-tokens/client/personalAccessTokens.html
  39. 15
      imports/personal-access-tokens/client/personalAccessTokens.js
  40. 6
      imports/personal-access-tokens/server/api/methods/generateToken.js
  41. 7
      imports/personal-access-tokens/server/api/methods/regenerateToken.js
  42. 5
      imports/personal-access-tokens/server/api/methods/removeToken.js
  43. 361
      package-lock.json
  44. 6
      package.json
  45. 32
      packages/rocketchat-i18n/i18n/en.i18n.json
  46. 32
      packages/rocketchat-i18n/i18n/pt-BR.i18n.json
  47. 2
      packages/rocketchat-i18n/i18n/pt.i18n.json
  48. 3
      server/configuration/accounts_meld.js
  49. 12
      server/lib/accounts.js
  50. 40
      server/main.d.ts
  51. 5
      server/methods/saveUserProfile.js
  52. 4
      server/startup/initialData.js
  53. 1
      server/startup/migrations/index.js
  54. 1
      server/startup/migrations/v172.js
  55. 16
      server/startup/migrations/v177.js

@ -24,12 +24,21 @@
"react/jsx-fragments": [
"error",
"syntax"
],
]
},
"settings": {
"react": {
"version": "detect",
"import/resolver": {
"node": {
"extensions": [
".js",
".ts",
".tsx"
]
}
},
"react": {
"version": "detect"
}
},
"overrides": [
{
@ -38,9 +47,9 @@
"**/*.tsx"
],
"extends": [
"@rocket.chat/eslint-config",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/eslint-recommended"
"plugin:@typescript-eslint/eslint-recommended",
"@rocket.chat/eslint-config"
],
"globals": {
"Atomics": "readonly",
@ -73,6 +82,10 @@
"syntax"
],
"@typescript-eslint/ban-ts-ignore": "off",
"@typescript-eslint/indent": [
"error",
"tab"
],
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/interface-name-prefix": [
"error",

@ -2,8 +2,8 @@ import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import toastr from 'toastr';
import { modal } from '../../ui-utils';
import { t } from '../../utils';
import { process2faReturn } from './callWithTwoFactorRequired';
function reportError(error, callback) {
if (callback) {
@ -46,32 +46,20 @@ const { loginWithPassword } = Meteor;
Meteor.loginWithPassword = function(email, password, cb) {
loginWithPassword(email, password, (error) => {
if (!error || error.error !== 'totp-required') {
return cb(error);
}
modal.open({
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);
}
});
process2faReturn({
error,
originalCallback: cb,
emailOrUsername: email,
onCode: (code) => {
Meteor.loginWithPasswordAndTOTP(email, password, code, (error) => {
if (error && error.error === 'totp-invalid') {
toastr.error(t('Invalid_two_factor_code'));
cb();
} else {
cb(error);
}
});
},
});
});
};

@ -4,42 +4,56 @@
<div class="preferences-page__content">
<form id="security" autocomplete="off" class="container">
{{# if isAllowed}}
<div class="section">
<h1>{{_ "Two-factor_authentication"}}</h1>
<div class="section-content border-component-color">
{{#if isEnabled}}
<button class="rc-button rc-button--cancel disable-2fa">{{_ "Disable_two-factor_authentication"}}</button>
{{else}}
{{#unless isRegistering}}
<p>{{_ "Two-factor_authentication_is_currently_disabled"}}</p>
<div class="section">
<h1>{{_ "Two-factor_authentication"}}</h1>
<div class="section-content border-component-color">
{{#if isEnabled}}
<button class="rc-button rc-button--cancel disable-2fa">{{_ "Disable_two-factor_authentication"}}</button>
{{else}}
{{#unless isRegistering}}
<p>{{_ "Two-factor_authentication_is_currently_disabled"}}</p>
<button class="rc-button rc-button--primary enable-2fa">{{_ "Enable_two-factor_authentication"}}</button>
{{else}}
<p>{{_ "Scan_QR_code"}}</p>
<p>{{_ "Scan_QR_code_alternative_s" code=imageSecret}}</p>
<button class="rc-button rc-button--primary enable-2fa">{{_ "Enable_two-factor_authentication"}}</button>
{{else}}
<p>{{_ "Scan_QR_code"}}</p>
<p>{{_ "Scan_QR_code_alternative_s" code=imageSecret}}</p>
<img src="{{imageData}}">
<img src="{{imageData}}">
<form>
<div class="input-line double-col">
<input type="text" class="rc-input__element" id="testCode" placeholder="{{_ "Enter_authentication_code"}}">
<button class="rc-button rc-button--primary setting-action verify-code">{{_ "Verify"}}</button>
</div>
</form>
{{/unless}}
{{/if}}
<form>
<div class="input-line double-col">
<input type="text" class="rc-input__element" id="testCode" placeholder="{{_ "Enter_authentication_code"}}">
<button class="rc-button rc-button--primary setting-action verify-code">{{_ "Verify"}}</button>
</div>
</form>
{{/unless}}
{{/if}}
</div>
</div>
</div>
{{#if isEnabled}}
<div class="section">
<h1>{{_ "Backup_codes"}}</h1>
<div class="section-content border-component-color">
<p>{{codesRemaining}}</p>
<button class="rc-button rc-button--secondary regenerate-codes">{{_ "Regenerate_codes"}}</button>
{{#if isEnabled}}
<div class="section">
<h1>{{_ "Backup_codes"}}</h1>
<div class="section-content border-component-color">
<p>{{codesRemaining}}</p>
<button class="rc-button rc-button--secondary regenerate-codes">{{_ "Regenerate_codes"}}</button>
</div>
</div>
{{/if}}
<div class="section">
<h1>{{_ "Two-factor_authentication_email"}}</h1>
<div class="section-content border-component-color">
{{#if isEmailEnabled}}
<button class="rc-button rc-button--cancel disable-2fa-email">{{_ "Disable_two-factor_authentication_email"}}</button>
{{else}}
<p>{{_ "Two-factor_authentication_email_is_currently_disabled"}}</p>
<button class="rc-button rc-button--primary enable-2fa-email">{{_ "Enable_two-factor_authentication_email"}}</button>
{{/if}}
</div>
</div>
</div>
{{/if}}
{{/if}}
</form>
</div>

@ -5,7 +5,7 @@ import toastr from 'toastr';
import { modal } from '../../ui-utils';
import { settings } from '../../settings';
import { t } from '../../utils';
import { t, handleError, APIClient } from '../../utils/client';
Template.accountSecurity.helpers({
showImage() {
@ -32,6 +32,10 @@ Template.accountSecurity.helpers({
return t('You_have_n_codes_remaining', { number: Template.instance().codesRemaining.get() });
}
},
isEmailEnabled() {
const user = Meteor.user();
return user && user.services && user.services.email2fa && user.services.email2fa.enabled;
},
});
Template.accountSecurity.events({
@ -82,6 +86,28 @@ Template.accountSecurity.events({
});
},
async 'click .enable-2fa-email'(event) {
event.preventDefault();
try {
await APIClient.v1.post('users.2fa.enableEmail');
toastr.success(t('Two-factor_authentication_enabled'));
} catch (error) {
handleError(error);
}
},
async 'click .disable-2fa-email'(event) {
event.preventDefault();
try {
await APIClient.v1.post('users.2fa.disableEmail');
toastr.success(t('Two-factor_authentication_disabled'));
} catch (error) {
handleError(error);
}
},
'click .verify-code'(event, instance) {
event.preventDefault();

@ -0,0 +1,91 @@
import { Meteor } from 'meteor/meteor';
import { SHA256 } from 'meteor/sha';
import toastr from 'toastr';
import { modal } from '../../ui-utils/client';
import { t, APIClient } from '../../utils/client';
const methods = {
totp: {
text: 'Open_your_authentication_app_and_enter_the_code',
},
email: {
text: 'Verify_your_email_for_the_code_we_sent',
html: true,
},
password: {
title: 'Please_enter_your_password',
text: 'For_your_security_you_must_enter_your_current_password_to_continue',
inputType: 'password',
},
};
export function process2faReturn({ error, result, originalCallback, onCode, emailOrUsername }) {
if (!error || (error.error !== 'totp-required' && error.errorType !== 'totp-required')) {
return originalCallback(error, result);
}
const method = error.details && error.details.method;
if (!emailOrUsername && Meteor.user()) {
emailOrUsername = Meteor.user().username;
}
modal.open({
title: t(methods[method].title || 'Two Factor Authentication'),
text: t(methods[method].text),
html: methods[method].html,
type: 'input',
inputActionText: method === 'email' && t('Send_me_the_code_again'),
async inputAction(e) {
const { value } = e.currentTarget;
e.currentTarget.value = t('Sending');
await APIClient.v1.post('users.2fa.sendEmailCode', { emailOrUsername });
e.currentTarget.value = value;
},
inputType: methods[method].inputType || 'text',
showCancelButton: true,
closeOnConfirm: true,
confirmButtonText: t('Verify'),
cancelButtonText: t('Cancel'),
}, (code) => {
if (code === false) {
return originalCallback(new Meteor.Error('totp-canceled'));
}
if (method === 'password') {
code = SHA256(code);
}
onCode(code, method);
});
}
const { call } = Meteor;
Meteor.call = function(ddpMethod, ...args) {
let callback = args.pop();
if (typeof callback !== 'function') {
args.push(callback);
callback = () => {};
}
return call(ddpMethod, ...args, function(error, result) {
process2faReturn({
error,
result,
originalCallback: callback,
onCode: (code, method) => {
call(ddpMethod, ...args, { twoFactorCode: code, twoFactorMethod: method }, (error, result) => {
if (error && error.error === 'totp-invalid') {
error.toastrShowed = true;
toastr.error(t('Invalid_two_factor_code'));
return callback(error);
}
callback(error, result);
});
},
});
});
};

@ -1,3 +1,4 @@
import './accountSecurity.html';
import './accountSecurity';
import './callWithTwoFactorRequired';
import './TOTPPassword';

@ -0,0 +1,17 @@
import { DDPCommon } from 'meteor/ddp-common';
import { DDP } from 'meteor/ddp';
class MethodInvocation extends DDPCommon.MethodInvocation {
constructor(options) {
const result = super(options);
const currentInvocation = DDP._CurrentInvocation.get();
if (currentInvocation) {
this.twoFactorChecked = currentInvocation.twoFactorChecked;
}
return result;
}
}
DDPCommon.MethodInvocation = MethodInvocation;

@ -0,0 +1,138 @@
import { Random } from 'meteor/random';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { Accounts } from 'meteor/accounts-base';
import bcrypt from 'bcrypt';
import { settings } from '../../../settings/server';
import * as Mailer from '../../../mailer';
import { Users } from '../../../models/server';
import { ICodeCheck, IProcessInvalidCodeResult } from './ICodeCheck';
import { IUser } from '../../../../definition/IUser';
export class EmailCheck implements ICodeCheck {
public readonly name = 'email';
private getUserVerifiedEmails(user: IUser): string[] {
if (!Array.isArray(user.emails)) {
return [];
}
return user.emails.filter(({ verified }) => verified).map((e) => e.address);
}
public isEnabled(user: IUser): boolean {
if (!settings.get('Accounts_TwoFactorAuthentication_By_Email_Enabled')) {
return false;
}
if (!user.services?.email2fa?.enabled) {
return false;
}
return this.getUserVerifiedEmails(user).length > 0;
}
private send2FAEmail(address: string, random: string, user: IUser): void {
const language = user.language || settings.get('Language') || 'en';
const t = (s: string): string => TAPi18n.__(s, { lng: language });
Mailer.send({
to: address,
from: settings.get('From_Email'),
subject: 'Authentication code',
replyTo: undefined,
data: {
code: random.replace(/^(\d{3})/, '$1-'),
},
headers: undefined,
text: `
${ t('Here_is_your_authentication_code') }
__code__
${ t('Do_not_provide_this_code_to_anyone') }
${ t('If_you_didnt_try_to_login_in_your_account_please_ignore_this_email') }
`,
html: `
<p>${ t('Here_is_your_authentication_code') }</p>
<p style="font-size: 30px;">
<b>__code__</b>
</p>
<p>${ t('Do_not_provide_this_code_to_anyone') }</p>
<p>${ t('If_you_didnt_try_to_login_in_your_account_please_ignore_this_email') }</p>
`,
});
}
public verify(user: IUser, codeFromEmail: string): boolean {
if (!this.isEnabled(user)) {
return false;
}
if (!user.services || !Array.isArray(user.services?.emailCode)) {
return false;
}
// Remove non digits
codeFromEmail = codeFromEmail.replace(/([^\d])/g, '');
Users.removeExpiredEmailCodesOfUserId(user._id);
const valid = user.services.emailCode.find(({ code, expire }) => {
if (expire < new Date()) {
return false;
}
if (bcrypt.compareSync(codeFromEmail, code)) {
Users.removeEmailCodeByUserIdAndCode(user._id, code);
return true;
}
return false;
});
return !!valid;
}
public sendEmailCode(user: IUser): string[] {
const emails = this.getUserVerifiedEmails(user);
const random = Random._randomString(6, '0123456789');
const encryptedRandom = bcrypt.hashSync(random, Accounts._bcryptRounds());
const expire = new Date();
const expirationInSeconds = parseInt(settings.get('Accounts_TwoFactorAuthentication_By_Email_Code_Expiration'));
expire.setSeconds(expire.getSeconds() + expirationInSeconds);
Users.addEmailCodeByUserId(user._id, encryptedRandom, expire);
for (const address of emails) {
this.send2FAEmail(address, random, user);
}
return emails;
}
public processInvalidCode(user: IUser): IProcessInvalidCodeResult {
Users.removeExpiredEmailCodesOfUserId(user._id);
// Generate new code if the there isn't any code with more than 5 minutes to expire
const expireWithDelta = new Date();
expireWithDelta.setMinutes(expireWithDelta.getMinutes() - 5);
const hasValidCode = user.services?.emailCode?.filter(({ expire }) => expire > expireWithDelta);
if (hasValidCode?.length) {
return {
codeGenerated: false,
codeCount: hasValidCode.length,
codeExpires: hasValidCode.map((i) => i.expire),
};
}
this.sendEmailCode(user);
return {
codeGenerated: true,
};
}
}

@ -0,0 +1,17 @@
import { IUser } from '../../../../definition/IUser';
export interface IProcessInvalidCodeResult {
codeGenerated: boolean;
codeCount?: number;
codeExpires?: Date[];
}
export interface ICodeCheck {
readonly name: string;
isEnabled(user: IUser): boolean;
verify(user: IUser, code: string): boolean;
processInvalidCode(user: IUser): IProcessInvalidCodeResult;
}

@ -0,0 +1,38 @@
import { Accounts } from 'meteor/accounts-base';
import { ICodeCheck, IProcessInvalidCodeResult } from './ICodeCheck';
import { IUser } from '../../../../definition/IUser';
export class PasswordCheckFallback implements ICodeCheck {
public readonly name = 'password';
public isEnabled(user: IUser): boolean {
// TODO: Uncomment for version 4.0 forcing the
// password fallback for who has password set.
// return user.services?.password?.bcrypt != null;
return !user;
}
public verify(user: IUser, code: string): boolean {
if (!this.isEnabled(user)) {
return false;
}
const passCheck = Accounts._checkPassword(user, {
digest: code.toLowerCase(),
algorithm: 'sha-256',
});
if (passCheck.error) {
return false;
}
return true;
}
public processInvalidCode(): IProcessInvalidCodeResult {
return {
codeGenerated: false,
};
}
}

@ -0,0 +1,36 @@
import { TOTP } from '../lib/totp';
import { IUser } from '../../../../definition/IUser';
import { settings } from '../../../settings/server';
import { ICodeCheck, IProcessInvalidCodeResult } from './ICodeCheck';
export class TOTPCheck implements ICodeCheck {
public readonly name = 'totp';
public isEnabled(user: IUser): boolean {
if (!settings.get('Accounts_TwoFactorAuthentication_Enabled')) {
return false;
}
return user.services?.totp?.enabled === true;
}
public verify(user: IUser, code: string): boolean {
if (!this.isEnabled(user)) {
return false;
}
return TOTP.verify({
secret: user.services?.totp?.secret,
token: code,
userId: user._id,
backupTokens: user.services?.totp?.hashedBackup,
});
}
public processInvalidCode(): IProcessInvalidCodeResult {
// Nothing to do
return {
codeGenerated: false,
};
}
}

@ -0,0 +1,156 @@
import crypto from 'crypto';
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import { settings } from '../../../settings/server';
import { TOTPCheck } from './TOTPCheck';
import { EmailCheck } from './EmailCheck';
import { PasswordCheckFallback } from './PasswordCheckFallback';
import { IUser } from '../../../../definition/IUser';
import { ICodeCheck } from './ICodeCheck';
import { Users } from '../../../models/server';
import { IMethodConnection } from '../../../../definition/IMethodThisType';
export interface ITwoFactorOptions {
disablePasswordFallback?: boolean;
disableRememberMe?: boolean;
}
export const totpCheck = new TOTPCheck();
export const emailCheck = new EmailCheck();
export const passwordCheckFallback = new PasswordCheckFallback();
export const checkMethods = new Map<string, ICodeCheck>();
checkMethods.set(totpCheck.name, totpCheck);
checkMethods.set(emailCheck.name, emailCheck);
export function getMethodByNameOrFirstActiveForUser(user: IUser, name?: string): ICodeCheck | undefined {
if (name && checkMethods.has(name)) {
return checkMethods.get(name);
}
return Array.from(checkMethods.values()).find((method) => method.isEnabled(user));
}
export function getAvailableMethodNames(user: IUser): string[] | [] {
return Array.from(checkMethods).filter(([, method]) => method.isEnabled(user)).map(([name]) => name) || [];
}
export function getUserForCheck(userId: string): IUser {
return Users.findOneById(userId, {
fields: {
emails: 1,
language: 1,
'services.totp': 1,
'services.email2fa': 1,
'services.emailCode': 1,
'services.password': 1,
'services.resume.loginTokens': 1,
},
});
}
export function getFingerprintFromConnection(connection: IMethodConnection): string {
const data = JSON.stringify({
userAgent: connection.httpHeaders['user-agent'],
clientAddress: connection.clientAddress,
});
return crypto.createHash('md5').update(data).digest('hex');
}
export function isAuthorizedForToken(connection: IMethodConnection, user: IUser, options: ITwoFactorOptions): boolean {
const currentToken = Accounts._getLoginToken(connection.id);
const tokenObject = user.services?.resume?.loginTokens?.find((i) => i.hashedToken === currentToken);
if (!tokenObject) {
return false;
}
if (tokenObject.bypassTwoFactor === true) {
return true;
}
if (options.disableRememberMe === true) {
return false;
}
if (!tokenObject.twoFactorAuthorizedUntil || !tokenObject.twoFactorAuthorizedHash) {
return false;
}
if (tokenObject.twoFactorAuthorizedUntil < new Date()) {
return false;
}
if (tokenObject.twoFactorAuthorizedHash !== getFingerprintFromConnection(connection)) {
return false;
}
return true;
}
export function rememberAuthorization(connection: IMethodConnection, user: IUser): void {
const currentToken = Accounts._getLoginToken(connection.id);
const rememberFor = parseInt(settings.get('Accounts_TwoFactorAuthentication_RememberFor'));
if (rememberFor <= 0) {
return;
}
const expires = new Date();
expires.setSeconds(expires.getSeconds() + rememberFor);
Users.setTwoFactorAuthorizationHashAndUntilForUserIdAndToken(user._id, currentToken, getFingerprintFromConnection(connection), expires);
}
interface ICheckCodeForUser {
user: IUser | string;
code?: string;
method?: string;
options?: ITwoFactorOptions;
connection?: IMethodConnection;
}
function _checkCodeForUser({ user, code, method, options = {}, connection }: ICheckCodeForUser): boolean {
if (typeof user === 'string') {
user = getUserForCheck(user);
}
if (connection && isAuthorizedForToken(connection, user, options)) {
return true;
}
let selectedMethod = getMethodByNameOrFirstActiveForUser(user, method);
if (!selectedMethod) {
if (options.disablePasswordFallback || !passwordCheckFallback.isEnabled(user)) {
return true;
}
selectedMethod = passwordCheckFallback;
}
if (!code) {
const data = selectedMethod.processInvalidCode(user);
const availableMethods = getAvailableMethodNames(user);
throw new Meteor.Error('totp-required', 'TOTP Required', { method: selectedMethod.name, ...data, availableMethods });
}
const valid = selectedMethod.verify(user, code);
if (!valid) {
throw new Meteor.Error('totp-invalid', 'TOTP Invalid', { method: selectedMethod.name });
}
if (options.disableRememberMe !== true && connection) {
rememberAuthorization(connection, user);
}
return true;
}
export const checkCodeForUser = process.env.TEST_MODE ? (): boolean => true : _checkCodeForUser;

@ -1,3 +1,4 @@
import './MethodInvocationOverride';
import './startup/settings';
import './methods/checkCodesRemaining';
import './methods/disable';

@ -1,9 +1,7 @@
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import { TOTP } from './lib/totp';
import { settings } from '../../settings';
import { callbacks } from '../../callbacks';
import { checkCodeForUser } from './code/index';
Accounts.registerLoginHandler('totp', function(options) {
if (!options.totp || !options.totp.code) {
@ -14,26 +12,11 @@ Accounts.registerLoginHandler('totp', function(options) {
});
callbacks.add('onValidateLogin', (login) => {
if (!settings.get('Accounts_TwoFactorAuthentication_Enabled')) {
if (login.type !== 'password') {
return;
}
if (login.type === 'password' && login.user.services && login.user.services.totp && login.user.services.totp.enabled === true) {
const { totp } = login.methodArguments[0];
const { totp } = login.methodArguments[0];
if (!totp || !totp.code) {
throw new Meteor.Error('totp-required', 'TOTP Required');
}
const verified = 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');
}
}
checkCodeForUser({ user: login.user, code: totp && totp.code, options: { disablePasswordFallback: true } });
}, callbacks.priority.MEDIUM, '2fa');

@ -8,12 +8,37 @@ settings.addGroup('Accounts', function() {
});
this.add('Accounts_TwoFactorAuthentication_MaxDelta', 1, {
type: 'int',
public: true,
i18nLabel: 'Accounts_TwoFactorAuthentication_MaxDelta',
enableQuery: {
_id: 'Accounts_TwoFactorAuthentication_Enabled',
value: true,
},
});
this.add('Accounts_TwoFactorAuthentication_By_Email_Enabled', true, {
type: 'boolean',
public: true,
});
this.add('Accounts_TwoFactorAuthentication_By_Email_Auto_Opt_In', true, {
type: 'boolean',
enableQuery: {
_id: 'Accounts_TwoFactorAuthentication_By_Email_Enabled',
value: true,
},
wizard: {
step: 3,
order: 3,
},
});
this.add('Accounts_TwoFactorAuthentication_By_Email_Code_Expiration', 3600, {
type: 'int',
enableQuery: {
_id: 'Accounts_TwoFactorAuthentication_By_Email_Enabled',
value: true,
},
});
this.add('Accounts_TwoFactorAuthentication_RememberFor', 300, {
type: 'int',
});
});
});

@ -0,0 +1,36 @@
import { Meteor } from 'meteor/meteor';
import { checkCodeForUser, ITwoFactorOptions } from './code/index';
import { IMethodThisType } from '../../../definition/IMethodThisType';
export function twoFactorRequired(fn: Function, options: ITwoFactorOptions): Function {
return function(this: IMethodThisType, ...args: any[]): any {
if (!this.userId) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'twoFactorRequired' });
}
// get two factor options from last item of args and remove it
const twoFactor = args.pop();
if (twoFactor) {
if (twoFactor.twoFactorCode && twoFactor.twoFactorMethod) {
checkCodeForUser({
user: this.userId,
connection: this.connection || undefined,
code: twoFactor.twoFactorCode,
method: twoFactor.twoFactorMethod,
options,
});
this.twoFactorChecked = true;
} else {
// if it was not two factor options, put it back
args.push(twoFactor);
}
}
if (!this.twoFactorChecked) {
checkCodeForUser({ user: this.userId, connection: this.connection || undefined, options });
}
return fn.apply(this, args);
};
}

@ -12,6 +12,7 @@ import { settings } from '../../settings';
import { metrics } from '../../metrics';
import { hasPermission, hasAllPermission } from '../../authorization';
import { getDefaultUserFields } from '../../utils/server/functions/getDefaultUserFields';
import { checkCodeForUser } from '../../2fa/server/code';
const logger = new Logger('API', {});
@ -110,7 +111,7 @@ export class APIClass extends Restivus {
return result;
}
failure(result, errorType, stack) {
failure(result, errorType, stack, error) {
if (_.isObject(result)) {
result.success = false;
} else {
@ -123,6 +124,14 @@ export class APIClass extends Restivus {
if (errorType) {
result.errorType = errorType;
}
if (error && error.details) {
try {
result.details = JSON.parse(error.details);
} catch (e) {
result.details = error.details;
}
}
}
result = {
@ -245,6 +254,15 @@ export class APIClass extends Restivus {
.map(addRateLimitRuleToEveryRoute);
}
processTwoFactor({ userId, request, invocation, options, connection }) {
const code = request.headers['x-2fa-code'];
const method = request.headers['x-2fa-method'];
checkCodeForUser({ user: userId, code, method, options, connection });
invocation.twoFactorChecked = true;
}
getFullRouteName(route, method, apiVersion = null) {
let prefix = `/${ this.apiPath || '' }`;
if (apiVersion) {
@ -318,6 +336,8 @@ export class APIClass extends Restivus {
id: Random.id(),
close() {},
token: this.token,
httpHeaders: this.request.headers,
clientAddress: requestIp,
};
try {
@ -340,6 +360,10 @@ export class APIClass extends Restivus {
};
Accounts._setAccountData(connection.id, 'loginToken', this.token);
if (options.twoFactorRequired) {
api.processTwoFactor({ userId: this.userId, request: this.request, invocation, options: options.twoFactorOptions, connection });
}
result = DDP._CurrentInvocation.withValue(invocation, () => originalAction.apply(this));
} catch (e) {
logger.debug(`${ method } ${ route } threw an error:`, e.stack);
@ -349,7 +373,7 @@ export class APIClass extends Restivus {
'error-unauthorized': 'unauthorized',
}[e.error] || 'failure';
result = API.v1[apiMethod](typeof e === 'string' ? e : e.message, e.error);
result = API.v1[apiMethod](typeof e === 'string' ? e : e.message, e.error, undefined, e);
} finally {
delete Accounts._accountData[connection.id];
}
@ -378,9 +402,9 @@ export class APIClass extends Restivus {
}
_initAuth() {
const loginCompatibility = (bodyParams) => {
const loginCompatibility = (bodyParams, request) => {
// Grab the username or email that the user is logging in with
const { user, username, email, password, code } = bodyParams;
const { user, username, email, password, code: bodyCode } = bodyParams;
if (password == null) {
return bodyParams;
@ -390,6 +414,8 @@ export class APIClass extends Restivus {
return bodyParams;
}
const code = bodyCode || request.headers['x-2fa-code'];
const auth = {
password,
};
@ -429,7 +455,7 @@ export class APIClass extends Restivus {
this.addRoute('login', { authRequired: false }, {
post() {
const args = loginCompatibility(this.bodyParams);
const args = loginCompatibility(this.bodyParams, this.request);
const getUserInfo = self.getHelperMethod('getUserInfo');
const invocation = new DDPCommon.MethodInvocation({

@ -20,6 +20,7 @@ import { getFullUserData, getFullUserDataById } from '../../../lib/server/functi
import { API } from '../api';
import { setStatusText } from '../../../lib/server';
import { findUsersToAutocomplete } from '../lib/users';
import { getUserForCheck, emailCheck } from '../../../2fa/server/code';
API.v1.addRoute('users.create', { authRequired: true }, {
post() {
@ -426,7 +427,7 @@ API.v1.addRoute('users.setStatus', { authRequired: true }, {
},
});
API.v1.addRoute('users.update', { authRequired: true }, {
API.v1.addRoute('users.update', { authRequired: true, twoFactorRequired: true }, {
post() {
check(this.bodyParams, {
userId: String,
@ -611,19 +612,19 @@ API.v1.addRoute('users.getUsernameSuggestion', { authRequired: true }, {
},
});
API.v1.addRoute('users.generatePersonalAccessToken', { authRequired: true }, {
API.v1.addRoute('users.generatePersonalAccessToken', { authRequired: true, twoFactorRequired: true }, {
post() {
const { tokenName } = this.bodyParams;
const { tokenName, bypassTwoFactor } = this.bodyParams;
if (!tokenName) {
return API.v1.failure('The \'tokenName\' param is required');
}
const token = Meteor.runAsUser(this.userId, () => Meteor.call('personalAccessTokens:generateToken', { tokenName }));
const token = Meteor.runAsUser(this.userId, () => Meteor.call('personalAccessTokens:generateToken', { tokenName, bypassTwoFactor }));
return API.v1.success({ token });
},
});
API.v1.addRoute('users.regeneratePersonalAccessToken', { authRequired: true }, {
API.v1.addRoute('users.regeneratePersonalAccessToken', { authRequired: true, twoFactorRequired: true }, {
post() {
const { tokenName } = this.bodyParams;
if (!tokenName) {
@ -647,6 +648,7 @@ API.v1.addRoute('users.getPersonalAccessTokens', { authRequired: true }, {
name: loginToken.name,
createdAt: loginToken.createdAt,
lastTokenPart: loginToken.lastTokenPart,
bypassTwoFactor: loginToken.bypassTwoFactor,
}));
return API.v1.success({
@ -655,7 +657,7 @@ API.v1.addRoute('users.getPersonalAccessTokens', { authRequired: true }, {
},
});
API.v1.addRoute('users.removePersonalAccessToken', { authRequired: true }, {
API.v1.addRoute('users.removePersonalAccessToken', { authRequired: true, twoFactorRequired: true }, {
post() {
const { tokenName } = this.bodyParams;
if (!tokenName) {
@ -669,6 +671,41 @@ API.v1.addRoute('users.removePersonalAccessToken', { authRequired: true }, {
},
});
API.v1.addRoute('users.2fa.enableEmail', { authRequired: true }, {
post() {
Users.enableEmail2FAByUserId(this.userId);
return API.v1.success();
},
});
API.v1.addRoute('users.2fa.disableEmail', { authRequired: true, twoFactorRequired: true, twoFactorOptions: { disableRememberMe: true } }, {
post() {
Users.disableEmail2FAByUserId(this.userId);
return API.v1.success();
},
});
API.v1.addRoute('users.2fa.sendEmailCode', {
post() {
const { emailOrUsername } = this.bodyParams;
if (!emailOrUsername) {
throw new Meteor.Error('error-parameter-required', 'emailOrUsername is required');
}
const method = emailOrUsername.includes('@') ? 'findOneByEmailAddress' : 'findOneByUsername';
const userId = this.userId || Users[method](emailOrUsername, { fields: { _id: 1 } })?._id;
if (!userId) {
throw new Meteor.Error('error-invalid-user', 'Invalid user');
}
return API.v1.success(emailCheck.sendEmailCode(getUserForCheck(userId)));
},
});
API.v1.addRoute('users.presence', { authRequired: true }, {
get() {
const { from, ids } = this.queryParams;

@ -3,6 +3,7 @@ import { Accounts } from 'meteor/accounts-base';
import { ServiceConfiguration } from 'meteor/service-configuration';
import { logger } from './logger';
import { settings } from '../../settings/server';
import { generateUsernameSuggestion } from '../../lib';
// Updates or creates a user after we authenticate with Blockstack
@ -34,8 +35,9 @@ export const updateOrCreateUser = (serviceData, options) => {
// gaia, encrypting mail for DID user only. @TODO: document this approach.
emails.push({ address: `${ did }@blockstack.email`, verified: false });
} else {
const verified = settings.get('Accounts_Verify_Email_For_External_Accounts');
// Reformat array of emails into expected format if they exist
emails = profile.emails.map((address) => ({ address, verified: true }));
emails = profile.emails.map((address) => ({ address, verified }));
}
const newUser = {

@ -125,6 +125,7 @@ Accounts.registerLoginHandler(function(options) {
const cas_version = parseFloat(settings.get('CAS_version'));
const sync_enabled = settings.get('CAS_Sync_User_Data_Enabled');
const trustUsername = settings.get('CAS_trust_username');
const verified = settings.get('Accounts_Verify_Email_For_External_Accounts');
// We have these
const ext_attrs = {
@ -201,7 +202,7 @@ Accounts.registerLoginHandler(function(options) {
// Update email
if (int_attrs.email) {
Meteor.users.update(user, { $set: { emails: [{ address: int_attrs.email, verified: true }] } });
Meteor.users.update(user, { $set: { emails: [{ address: int_attrs.email, verified }] } });
}
}
} else {
@ -230,7 +231,7 @@ Accounts.registerLoginHandler(function(options) {
// Add email
if (int_attrs.email) {
_.extend(newUser, {
emails: [{ address: int_attrs.email, verified: true }],
emails: [{ address: int_attrs.email, verified }],
});
}

@ -151,7 +151,7 @@ export class CROWD {
crowd_username: crowdUser.crowd_username,
emails: [{
address: crowdUser.email,
verified: true,
verified: settings.get('Accounts_Verify_Email_For_External_Accounts'),
}],
active: crowdUser.active,
crowd: true,

@ -123,12 +123,14 @@ export function getDataToSyncUserData(ldapUser, user) {
return;
}
const verified = settings.get('Accounts_Verify_Email_For_External_Accounts');
if (_.isObject(ldapUser[ldapField])) {
_.map(ldapUser[ldapField], function(item) {
emailList.push({ address: item, verified: true });
emailList.push({ address: item, verified });
});
} else {
emailList.push({ address: ldapUser[ldapField], verified: true });
emailList.push({ address: ldapUser[ldapField], verified });
}
break;

@ -5,6 +5,8 @@
for the *client* pieces of code which does include the shared
library files.
*/
import * as DateFormat from './formatDate';
export { RocketChatAnnouncement } from './RocketChatAnnouncement';
export { LoginPresence } from './LoginPresence';
export * as DateFormat from './formatDate';
export { DateFormat };

@ -2,9 +2,10 @@ import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import { saveUser } from '../functions';
import { twoFactorRequired } from '../../../2fa/server/twoFactorRequired';
Meteor.methods({
insertOrUpdateUser(userData) {
insertOrUpdateUser: twoFactorRequired(function(userData) {
check(userData, Object);
if (!Meteor.userId()) {
@ -12,5 +13,5 @@ Meteor.methods({
}
return saveUser(Meteor.userId(), userData);
},
}),
});

@ -144,6 +144,9 @@ settings.addGroup('Accounts', function() {
},
},
});
this.add('Accounts_Verify_Email_For_External_Accounts', true, {
type: 'boolean',
});
this.add('Accounts_ManuallyApproveNewUsers', false, {
public: true,
type: 'boolean',

@ -10,6 +10,7 @@ import _ from 'underscore';
import s from 'underscore.string';
import { SAML } from './saml_utils';
import { settings } from '../../settings/server';
import { Rooms, Subscriptions, CredentialTokens } from '../../models';
import { generateUsernameSuggestion } from '../../lib';
import { _setUsername } from '../../lib/server/functions';
@ -192,7 +193,7 @@ function overwriteData(user, fullName, eppnMatch, emailList) {
$set: {
emails: emailList.map((email) => ({
address: email,
verified: true,
verified: settings.get('Accounts_Verify_Email_For_External_Accounts'),
})),
},
});
@ -294,7 +295,7 @@ Accounts.registerLoginHandler(function(loginRequest) {
const emails = emailList.map((email) => ({
address: email,
verified: true,
verified: settings.get('Accounts_Verify_Email_For_External_Accounts'),
}));
let globalRoles;

@ -398,6 +398,32 @@ export class Users extends Base {
});
}
enableEmail2FAByUserId(userId) {
return this.update({
_id: userId,
}, {
$set: {
'services.email2fa': {
enabled: true,
changedAt: new Date(),
},
},
});
}
disableEmail2FAByUserId(userId) {
return this.update({
_id: userId,
}, {
$set: {
'services.email2fa': {
enabled: false,
changedAt: new Date(),
},
},
});
}
findByIdsWithPublicE2EKey(ids, options) {
const query = {
_id: {
@ -419,6 +445,40 @@ export class Users extends Base {
});
}
removeExpiredEmailCodesOfUserId(userId) {
this.update({ _id: userId }, {
$pull: {
'services.emailCode': {
expire: { $lt: new Date() },
},
},
});
}
removeEmailCodeByUserIdAndCode(userId, code) {
this.update({ _id: userId }, {
$pull: {
'services.emailCode': {
code,
},
},
});
}
addEmailCodeByUserId(userId, code, expire) {
this.update({ _id: userId }, {
$push: {
'services.emailCode': {
$each: [{
code,
expire,
}],
$slice: -5,
},
},
});
}
findUsersInRoles(roles, scope, options) {
roles = [].concat(roles);
@ -1074,6 +1134,18 @@ export class Users extends Base {
return this.update(_id, update);
}
setTwoFactorAuthorizationHashAndUntilForUserIdAndToken(_id, token, hash, until) {
return this.update({
_id,
'services.resume.loginTokens.hashedToken': token,
}, {
$set: {
'services.resume.loginTokens.$.twoFactorAuthorizedHash': hash,
'services.resume.loginTokens.$.twoFactorAuthorizedUntil': until,
},
});
}
setUtcOffset(_id, utcOffset) {
const query = {
_id,

@ -0,0 +1,3 @@
export namespace settings {
export function get(name: string): string;
}

@ -47,6 +47,9 @@
</div>
<div class="rc-modal__content-error"></div>
{{/if}}
{{#if inputActionText}}
<input class="rc-button rc-button--nude js-input-action" type="submit" data-button="input-action" value="{{inputActionText}}">
{{/if}}
{{#if dontAskAgain}}
<label class="rc-checkbox">
<input type="checkbox" id="dont-ask-me-again" class="rc-checkbox__input js-modal-dont-ask">

@ -233,6 +233,10 @@ Template.rc_modal.events({
event.stopPropagation();
this.close();
},
'click .js-input-action'(e, instance) {
!this.inputAction || this.inputAction.call(instance.data.data, e, instance);
e.stopPropagation();
},
'click .js-close'(e) {
e.preventDefault();
e.stopPropagation();

@ -2,6 +2,7 @@ import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import { baseURI } from './baseuri';
import { process2faReturn } from '../../../2fa/client/callWithTwoFactorRequired';
export const APIClient = {
delete(endpoint, params) {
@ -43,26 +44,34 @@ export const APIClient = {
return query;
},
_jqueryCall(method, endpoint, params, body) {
_jqueryCall(method, endpoint, params, body, headers = {}) {
const query = APIClient._generateQueryFromParams(params);
return new Promise(function _rlRestApiGet(resolve, reject) {
jQuery.ajax({
method,
url: `${ baseURI }api/${ endpoint }${ query }`,
headers: {
headers: Object.assign({
'Content-Type': 'application/json',
'X-User-Id': Meteor._localStorage.getItem(Accounts.USER_ID_KEY),
'X-Auth-Token': Meteor._localStorage.getItem(Accounts.LOGIN_TOKEN_KEY),
},
}, headers),
data: JSON.stringify(body),
success: function _rlGetSuccess(result) {
resolve(result);
},
error: function _rlGetFailure(xhr, status, errorThrown) {
const error = new Error(errorThrown);
error.xhr = xhr;
reject(error);
APIClient.processTwoFactorError({
xhr,
params: [method, endpoint, params, body, headers],
resolve,
reject,
originalCallback() {
const error = new Error(errorThrown);
error.xhr = xhr;
reject(error);
},
});
},
});
});
@ -116,6 +125,23 @@ export const APIClient = {
return ret;
},
processTwoFactorError({ xhr, params, originalCallback, resolve, reject }) {
if (!xhr.responseJSON || !xhr.responseJSON.errorType) {
return originalCallback();
}
process2faReturn({
error: xhr.responseJSON,
originalCallback,
onCode(code, method) {
const headers = params[params.length - 1];
headers['x-2fa-code'] = code;
headers['x-2fa-method'] = method;
APIClient._jqueryCall(...params).then(resolve).catch(reject);
},
});
},
v1: {
delete(endpoint, params) {
return APIClient.delete(`v1/${ endpoint }`, params);

@ -4,6 +4,10 @@ import s from 'underscore.string';
import toastr from 'toastr';
export const handleError = function(error, useToastr = true) {
if (error.xhr) {
error = error.xhr.responseJSON || {};
}
if (_.isObject(error.details)) {
for (const key in error.details) {
if (error.details.hasOwnProperty(key)) {
@ -13,6 +17,9 @@ export const handleError = function(error, useToastr = true) {
}
if (useToastr) {
if (error.toastrShowed) {
return;
}
const details = Object.entries(error.details || {})
.reduce((obj, [key, value]) => ({ ...obj, [key]: s.escapeHTML(value) }), {});
const message = TAPi18n.__(error.error, details);

@ -24,6 +24,7 @@ export const getDefaultUserFields = () => ({
'services.blockstack': 1,
'services.password.bcrypt': 1,
'services.totp.enabled': 1,
'services.email2fa.enabled': 1,
statusLivechat: 1,
banners: 1,
'oauth.authorizedClients': 1,

@ -53,9 +53,9 @@ export function SettingsBasedStep({ step, title, active }) {
resetFields(
settings
.filter(({ wizard }) => wizard.step === step)
.filter(({ type }) => ['string', 'select', 'language'].includes(type))
.filter(({ type }) => ['string', 'select', 'language', 'boolean'].includes(type))
.sort(({ wizard: { order: a } }, { wizard: { order: b } }) => a - b)
.map(({ value, ...field }) => ({ ...field, value: value || '' })),
.map(({ value, ...field }) => ({ ...field, value: value != null ? value : '' })),
);
}, [settings, currentStep]);
@ -135,6 +135,20 @@ export function SettingsBasedStep({ step, title, active }) {
options={values.map(({ i18nLabel, key }) => [key, t(i18nLabel)])}
/>}
{type === 'boolean' && <Select
type='select'
data-qa={_id}
id={_id}
name={_id}
ref={i === 0 ? autoFocusRef : undefined}
value={String(value)}
onChange={(value) => setFieldValue(_id, value === 'true')}
options={[
['true', t('Yes')],
['false', t('No')],
]}
/>}
{type === 'language' && <Select
type='select'
data-qa={_id}

@ -0,0 +1,16 @@
export interface IMethodConnection {
id: string;
close: Function;
onClose: Function;
clientAddress: string;
httpHeaders: Record<string, any>;
}
export interface IMethodThisType {
isSimulation: boolean;
userId: string | null;
connection: IMethodConnection | null;
setUserId(userId: string): void;
unblock(): void;
twoFactorChecked: boolean | undefined;
}

@ -0,0 +1,5 @@
import { IMethodThisType } from './IMethodThisType';
export interface IMethodType {
[key: string]: (this: IMethodThisType, ...args: any[]) => any;
}

@ -0,0 +1,100 @@
export interface ILoginToken {
hashedToken: string;
twoFactorAuthorizedUntil?: Date;
twoFactorAuthorizedHash?: string;
}
export interface IMeteorLoginToken extends ILoginToken {
when: Date;
}
export interface IPersonalAccessToken extends ILoginToken {
type: 'personalAccessToken';
createdAt: Date;
lastTokenPart: string;
name?: string;
bypassTwoFactor?: boolean;
}
export interface IUserEmailVerificationToken {
token: string;
address: string;
when: Date;
}
export interface IUserEmailCode {
code: string;
expire: Date;
}
type LoginToken = ILoginToken & IPersonalAccessToken;
export interface IUserServices {
password?: {
bcrypt: string;
};
email?: {
verificationTokens?: IUserEmailVerificationToken[];
};
resume?: {
loginTokens?: LoginToken[];
};
google?: any;
facebook?: any;
github?: any;
totp?: {
enabled: boolean;
hashedBackup: string[];
secret: string;
};
email2fa?: {
enabled: boolean;
changedAt: Date;
};
emailCode: IUserEmailCode[];
}
export interface IUserEmail {
address: string;
verified: boolean;
}
export interface IUserSettings {
profile: any;
preferences: {
[key: string]: any;
};
}
export interface IUser {
_id: string;
createdAt: Date;
roles: string[];
type: string;
active: boolean;
username?: string;
name?: string;
services?: IUserServices;
emails?: IUserEmail[];
status?: string;
statusConnection?: string;
lastLogin?: Date;
avatarOrigin?: string;
utcOffset?: number;
language?: string;
statusDefault?: string;
oauth?: {
authorizedClients: string[];
};
_updatedAt?: Date;
statusLivechat?: string;
e2e?: {
private_key: string;
public_key: string;
};
requirePasswordChange?: boolean;
customFields?: {
[key: string]: any;
};
settings?: IUserSettings;
}

@ -8,9 +8,18 @@
<div class="section-content border-component-color">
<div class="input-line double-col">
<label for="tokenName" class="setting-label">{{_ "API_Personal_Access_Tokens_To_REST_API"}}</label>
<div class="setting-field">
<input type="text" class="rc-input__element js-search" name="tokenName" id="tokenName"
placeholder={{_ "API_Add_Personal_Access_Token"}} autocomplete="off" />
<div style="display: flex;">
<div class="setting-field" style="flex-grow: 1;">
<input type="text" class="rc-input__element js-search" name="tokenName" id="tokenName"
placeholder={{_ "API_Add_Personal_Access_Token"}} autocomplete="off" />
</div>
<div class="rc-select setting-field">
<select id="bypassTwoFactor" class="required rc-select__element">
<option value="false" selected dir="auto">{{_ "Require"}} ({{_ "Two Factor Authentication"}})</option>
<option value="true" dir="auto">{{_ "Ignore"}} ({{_ "Two Factor Authentication"}})</option>
</select>
{{> icon block="rc-select__arrow" icon="arrow-down"}}
</div>
</div>
<button name="add" class="rc-button rc-button--primary setting-action save-token">{{_ "Add"}}</button>
</div>
@ -29,6 +38,9 @@
<th>
<div class="table-fake-th">{{_ "Last_token_part"}}</div>
</th>
<th>
<div class="table-fake-th">{{_ "Two Factor Authentication"}}</div>
</th>
<th></th>
</tr>
</thead>
@ -42,6 +54,7 @@
</td>
<td>{{dateFormated createdAt}}</td>
<td>...{{lastTokenPart}}</td>
<td>{{twoFactor bypassTwoFactor}}</td>
<td><button class="regenerate-personal-access-token"><i class="icon-ccw"></i></button></td>
<td><button class="remove-personal-access-token"><i class="icon-block"></i></button></td>
</tr>

@ -9,7 +9,7 @@ import { t } from '../../../app/utils';
import { modal, SideNav } from '../../../app/ui-utils';
import { hasAllPermission } from '../../../app/authorization';
import './personalAccessTokens.html';
import { APIClient } from '../../../app/utils/client';
import { APIClient, handleError } from '../../../app/utils/client';
const loadTokens = async (instance) => {
const { tokens } = await APIClient.v1.get('users.getPersonalAccessTokens');
@ -26,6 +26,9 @@ Template.accountTokens.helpers({
dateFormated(date) {
return moment(date).format('L LT');
},
twoFactor(bypassTwoFactor) {
return bypassTwoFactor ? t('Ignore') : t('Require');
},
});
const showSuccessModal = (token) => {
@ -40,6 +43,7 @@ const showSuccessModal = (token) => {
}, () => {
});
};
Template.accountTokens.events({
'submit #form-tokens'(e, instance) {
e.preventDefault();
@ -47,9 +51,10 @@ Template.accountTokens.events({
if (tokenName === '') {
return toastr.error(t('Please_fill_a_token_name'));
}
Meteor.call('personalAccessTokens:generateToken', { tokenName }, (error, token) => {
const bypassTwoFactor = $('#bypassTwoFactor').val() === 'true';
Meteor.call('personalAccessTokens:generateToken', { tokenName, bypassTwoFactor }, (error, token) => {
if (error) {
return toastr.error(t(error.error));
return handleError(error);
}
showSuccessModal(token);
loadTokens(instance);
@ -72,7 +77,7 @@ Template.accountTokens.events({
tokenName: this.name,
}, (error) => {
if (error) {
return toastr.error(t(error.error));
return handleError(error);
}
loadTokens(instance);
toastr.success(t('Removed'));
@ -95,7 +100,7 @@ Template.accountTokens.events({
tokenName: this.name,
}, (error, token) => {
if (error) {
return toastr.error(t(error.error));
return handleError(error);
}
loadTokens(instance);
showSuccessModal(token);

@ -4,9 +4,10 @@ import { Accounts } from 'meteor/accounts-base';
import { hasPermission } from '../../../../../app/authorization';
import { Users } from '../../../../../app/models';
import { twoFactorRequired } from '../../../../../app/2fa/server/twoFactorRequired';
Meteor.methods({
'personalAccessTokens:generateToken'({ tokenName }) {
'personalAccessTokens:generateToken': twoFactorRequired(function({ tokenName, bypassTwoFactor }) {
if (!Meteor.userId()) {
throw new Meteor.Error('not-authorized', 'Not Authorized', { method: 'personalAccessTokens:generateToken' });
}
@ -31,8 +32,9 @@ Meteor.methods({
createdAt: new Date(),
lastTokenPart: token.slice(-6),
name: tokenName,
bypassTwoFactor,
},
});
return token;
},
}),
});

@ -2,9 +2,10 @@ import { Meteor } from 'meteor/meteor';
import { hasPermission } from '../../../../../app/authorization';
import { Users } from '../../../../../app/models';
import { twoFactorRequired } from '../../../../../app/2fa/server/twoFactorRequired';
Meteor.methods({
'personalAccessTokens:regenerateToken'({ tokenName }) {
'personalAccessTokens:regenerateToken': twoFactorRequired(function({ tokenName }) {
if (!Meteor.userId()) {
throw new Meteor.Error('not-authorized', 'Not Authorized', { method: 'personalAccessTokens:regenerateToken' });
}
@ -21,6 +22,6 @@ Meteor.methods({
}
Meteor.call('personalAccessTokens:removeToken', { tokenName });
return Meteor.call('personalAccessTokens:generateToken', { tokenName });
},
return Meteor.call('personalAccessTokens:generateToken', { tokenName, bypassTwoFactor: tokenExist.bypassTwoFactor });
}),
});

@ -2,9 +2,10 @@ import { Meteor } from 'meteor/meteor';
import { hasPermission } from '../../../../../app/authorization';
import { Users } from '../../../../../app/models';
import { twoFactorRequired } from '../../../../../app/2fa/server/twoFactorRequired';
Meteor.methods({
'personalAccessTokens:removeToken'({ tokenName }) {
'personalAccessTokens:removeToken': twoFactorRequired(function({ tokenName }) {
if (!Meteor.userId()) {
throw new Meteor.Error('not-authorized', 'Not Authorized', { method: 'personalAccessTokens:removeToken' });
}
@ -25,5 +26,5 @@ Meteor.methods({
name: tokenName,
},
});
},
}),
});

361
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -26,8 +26,9 @@
"debug": "meteor run --inspect",
"debug-brk": "meteor run --inspect-brk",
"lint": "meteor npm run stylelint && meteor npm run eslint",
"jslint": "npm run eslint",
"eslint": "eslint --ext .js,.ts,.jsx,.tsx .",
"jslint": "eslint --ext .js,.jsx .",
"tslint": "eslint --ext .ts,.tsx .",
"eslint": "meteor npm run jslint && meteor npm run tslint",
"stylelint": "stylelint \"app/**/*.css\" \"client/**/*.css\"",
"deploy": "npm run build && pm2 startOrRestart pm2.json",
"postinstall": "node .scripts/npm-postinstall.js",
@ -156,6 +157,7 @@
"csv-parse": "^4.0.1",
"emailreplyparser": "^0.0.5",
"emojione": "^4.5.0",
"eslint-plugin-import": "^2.19.1",
"express": "^4.16.4",
"express-session": "^1.15.4",
"fibers": "4.0.3",

@ -203,11 +203,21 @@
"Accounts_Directory_DefaultView": "Default Directory Listing",
"Accounts_SetDefaultAvatar": "Set Default Avatar",
"Accounts_SetDefaultAvatar_Description": "Tries to determine default avatar based on OAuth Account or Gravatar",
"Accounts_Set_Email_Of_External_Accounts_as_Verified": "Set email of external accounts as verified",
"Accounts_Set_Email_Of_External_Accounts_as_Verified_Description": "Accounts created from external services, like LDAP, OAth, etc, will have their emails verified automatically",
"Accounts_ShowFormLogin": "Show Default Login Form",
"Accounts_TwoFactorAuthentication_Enabled": "Enable Two Factor Authentication",
"Accounts_TwoFactorAuthentication_By_Email_Enabled": "Enable Two Factor Authentication via Email",
"Accounts_TwoFactorAuthentication_By_Email_Enabled_Description": "Users with email verified and the option enabled in their profile page will receive an email with a temporary code to authorize certain actions like login, save the profile, etc.",
"Accounts_TwoFactorAuthentication_By_Email_Auto_Opt_In": "Auto opt in new users for Two Factor via Email",
"Accounts_TwoFactorAuthentication_By_Email_Auto_Opt_In_Description": "New users will have the Two Factor Authentication via Email enabled by default. They will be able to disable it in their profile page.",
"Accounts_TwoFactorAuthentication_By_Email_Code_Expiration": "Time to expire the code sent via email in seconds",
"Accounts_TwoFactorAuthentication_Enabled": "Enable Two Factor Authentication via TOTP",
"Accounts_TwoFactorAuthentication_Enabled_Description": "Users can setup their Two Factor Authentication using any TOTP app, like Google Authenticator or Authy",
"Accounts_TwoFactorAuthentication_MaxDelta": "Maximum Delta",
"Accounts_UserAddedEmail_Default": "<h1>Welcome to <strong>[Site_Name]</strong></h1><p>Go to <a href=\"[Site_URL]\">[Site_URL]</a> and try the best open source chat solution available today!</p><p>You may login using your email: [email] and password: [password]. You may be required to change it after your first login.",
"Accounts_TwoFactorAuthentication_MaxDelta_Description": "The Maximum Delta determines how many tokens are valid at any given time. Tokens are generated every 30 seconds, and are valid for (30 * Maximum Delta) seconds. <br/>Example: With a Maximum Delta set to 10, each token can be used up to 300 seconds before or after it's timestamp. This is useful when the client's clock is not properly synced with the server.",
"Accounts_TwoFactorAuthentication_RememberFor": "Remember Two Factor for (seconds)",
"Accounts_TwoFactorAuthentication_RememberFor_Description": "Do not request two factor authorization code if it was already provided before in the given time.",
"Accounts_UseDefaultBlockedDomainsList": "Use Default Blocked Domains List",
"Accounts_UseDNSDomainCheck": "Use DNS Domain Check",
"Accounts_UserAddedEmailSubject_Default": "You have been added to [Site_Name]",
@ -1141,7 +1151,8 @@
"Directory": "Directory",
"Disable_Facebook_integration": "Disable Facebook integration",
"Disable_Notifications": "Disable Notifications",
"Disable_two-factor_authentication": "Disable two-factor authentication",
"Disable_two-factor_authentication": "Disable two-factor authentication via TOTP",
"Disable_two-factor_authentication_email": "Disable two-factor authentication via Email",
"Disabled": "Disabled",
"Disallow_reacting": "Disallow Reacting",
"Disallow_reacting_Description": "Disallows reacting",
@ -1178,6 +1189,7 @@
"Download_My_Data": "Download My Data (HTML)",
"Download_Pending_Files": "Download Pending Files",
"Download_Snippet": "Download",
"Do_not_provide_this_code_to_anyone": "Do not provide this code to anyone.",
"Drop_to_upload_file": "Drop to upload file",
"Dry_run": "Dry run",
"Dry_run_description": "Will only send one email, to the same address as in From. The email must belong to a valid user.",
@ -1258,7 +1270,8 @@
"Enable_Desktop_Notifications": "Enable Desktop Notifications",
"Enable_inquiry_fetch_by_stream": "Enable inquiry data fetch from server using a stream",
"Enable_Svg_Favicon": "Enable SVG favicon",
"Enable_two-factor_authentication": "Enable two-factor authentication",
"Enable_two-factor_authentication": "Enable two-factor authentication via TOTP",
"Enable_two-factor_authentication_email": "Enable two-factor authentication via Email",
"Enabled": "Enabled",
"Encrypted": "Encrypted",
"Encrypted_channel_Description": "End to end encrypted channel. Search will not work with encrypted channels and notifications may not show the messages content.",
@ -1623,6 +1636,7 @@
"Header": "Header",
"Header_and_Footer": "Header and Footer",
"Healthcare_and_Pharmaceutical": "Healthcare/Pharmaceutical",
"Here_is_your_authentication_code": "Here is your authentication code:",
"Help_Center": "Help Center",
"Helpers": "Helpers",
"Hex_Color_Preview": "Hex Color Preview",
@ -1666,6 +1680,7 @@
"If_you_are_sure_type_in_your_username": "If you are sure type in your username:",
"If_you_dont_have_one_send_an_email_to_omni_rocketchat_to_get_yours": "If you don't have one send an email to [omni@rocket.chat](mailto:omni@rocket.chat) to get yours.",
"If_you_didnt_ask_for_reset_ignore_this_email": "If you didn't ask for your password reset, you can ignore this email.",
"If_you_didnt_try_to_login_in_your_account_please_ignore_this_email": "If you didn't try to login in your account please ignore this email.",
"Iframe_Integration": "Iframe Integration",
"Iframe_Integration_receive_enable": "Enable Receive",
"Iframe_Integration_receive_enable_Description": "Allow parent window to send commands to Rocket.Chat.",
@ -2746,6 +2761,7 @@
"Request_comment_when_closing_conversation": "Request comment when closing conversation",
"Request_comment_when_closing_conversation_description": "If enabled, the agent will need to set a comment before the conversation is closed.",
"Request_tag_before_closing_chat": "Request tag(s) before closing conversation",
"Require": "Require",
"Require_all_tokens": "Require all tokens",
"Require_any_token": "Require any token",
"Require_password_change": "Require password change",
@ -2925,6 +2941,7 @@
"Send_invitation_email_error": "You haven't provided any valid email address.",
"Send_invitation_email_info": "You can send multiple email invitations at once.",
"Send_invitation_email_success": "You have successfully sent an invitation email to the following addresses:",
"Send_me_the_code_again": "Send me the code again",
"Send_request_on_agent_message": "Send Request on Agent Messages",
"Send_request_on_chat_close": "Send Request on Chat Close",
"Send_request_on_lead_capture": "Send request on lead capture",
@ -3269,6 +3286,8 @@
"Total_messages": "Total Messages",
"Total_Threads": "Total Threads",
"Total_visitors": "Total Visitors",
"totp-invalid": "Code or password invalid",
"TOTP Invalid [totp-invalid]": "Code or password invalid",
"Tourism": "Tourism",
"Transcript_Enabled": "Ask Visitor if They Would Like a Transcript After Chat Closed",
"Transcript_message": "Message to Show When Asking About Transcript",
@ -3286,11 +3305,13 @@
"Turn_OFF": "Turn OFF",
"Turn_ON": "Turn ON",
"Two Factor Authentication": "Two Factor Authentication",
"Two-factor_authentication": "Two-factor authentication",
"Two-factor_authentication": "Two-factor authentication via TOTP",
"Two-factor_authentication_email": "Two-factor authentication via Email",
"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",
"Two-factor_authentication_is_currently_disabled": "Two-factor authentication via TOTP is currently disabled",
"Two-factor_authentication_native_mobile_app_warning": "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.",
"Two-factor_authentication_email_is_currently_disabled": "Two-factor authentication via Email is currently disabled",
"Type": "Type",
"Type_your_email": "Type your email",
"Type_your_job_title": "Type your job title",
@ -3471,6 +3492,7 @@
"Verified": "Verified",
"Verify": "Verify",
"Verify_your_email": "Verify your email",
"Verify_your_email_for_the_code_we_sent": "Verify your email for the code we sent",
"Version": "Version",
"Videos": "Videos",
"Video Conference": "Video Conference",

@ -196,10 +196,18 @@
"Accounts_SetDefaultAvatar": "Definir Avatar Padrão",
"Accounts_SetDefaultAvatar_Description": "Tenta determinar o avatar padrão com base em OAuth Account ou Gravatar",
"Accounts_ShowFormLogin": "Mostrar formulário de login padrão",
"Accounts_TwoFactorAuthentication_Enabled": "Ativar autenticação de dois fatores",
"Accounts_TwoFactorAuthentication_By_Email_Enabled": "Ativar autenticação de dois fatores por Email",
"Accounts_TwoFactorAuthentication_By_Email_Enabled_Description": "Usuários com email verificado e com a opção habilitada em seu perfil receberão um email com um código temporário para autorizar certas ações como o login, salvar o perfil, etc.",
"Accounts_TwoFactorAuthentication_By_Email_Auto_Opt_In": "Auto ativar a autenticação de duas etapas via email para novos usuários",
"Accounts_TwoFactorAuthentication_By_Email_Auto_Opt_In_Description": "Novos usuários terão a autenticação por duas etapas via email ativada por padrão. Eles poderão desabilitá-la em sua página de perfil.",
"Accounts_TwoFactorAuthentication_By_Email_Code_Expiration": "Tempo para expirar o código enviado por email (em segundos)",
"Accounts_TwoFactorAuthentication_Enabled": "Ativar autenticação de dois fatores por TOTP",
"Accounts_TwoFactorAuthentication_Enabled_Description": "Os usuários podem configurar sua autenticação de dois fatores usando qualquer aplicativo de TOTP, como o Google Authenticator ou o Authy",
"Accounts_TwoFactorAuthentication_MaxDelta": "Delta máximo",
"Accounts_UserAddedEmail_Default": "<h1>Bem-vindo ao <strong>[Site_Name]</strong></h1>.<p> Vá para <a href=\"[Site_URL]\">[Site_URL]</a> e experimente a melhor solução de chat com código aberto da atualidade! </p><p> Você pode fazer o login usando seu e-mail: [email] e senha: [password]. Pode ser pedida a mudança da senha após o seu primeiro login.",
"Accounts_TwoFactorAuthentication_MaxDelta_Description": "O Delta máximo determina quantos tokens são válidos em qualquer momento. Os tokens são gerados a cada 30 segundos e são válidos por (30 * Delta máximo) segundos. <br/>Exemplo: com um Delta máximo configurado para 10, cada token pode ser usado até 300 segundos antes ou depois do timestamp. Isso é útil quando o relógio do cliente não está corretamente sincronizado com o servidor.",
"Accounts_TwoFactorAuthentication_RememberFor": "Lembrar a autenticação por (segundos)",
"Accounts_TwoFactorAuthentication_RememberFor_Description": "Não requisitar a autenticação de dois fatores se já requisitada anteriormente dentro do tempo determinado.",
"Accounts_UseDefaultBlockedDomainsList": "Use Lista Padrão de Domínios Bloqueados",
"Accounts_UseDNSDomainCheck": "Use verificação de Domínio DNS",
"Accounts_UserAddedEmailSubject_Default": "Você foi adicionado ao [Site_Name]",
@ -1066,7 +1074,8 @@
"Directory": "Diretório",
"Disable_Facebook_integration": "Desabilitar a integração do Facebook",
"Disable_Notifications": "Desativar as notificações",
"Disable_two-factor_authentication": "Desativar a autenticação de dois fatores",
"Disable_two-factor_authentication": "Desativar a autenticação de dois fatores por TOTP",
"Disable_two-factor_authentication_email": "Desativar a autenticação de dois fatores por Email",
"Disabled": "Desativado",
"Disallow_reacting": "Não permitir reagir",
"Disallow_reacting_Description": "Não permite reagir",
@ -1098,6 +1107,7 @@
"Downloading_file_from_external_URL": "Baixar arquivo de URL externa",
"Download_My_Data": "Baixar meus dados (HTML)",
"Download_Snippet": "Baixar",
"Do_not_provide_this_code_to_anyone": "Não forneça este código para ninguém.",
"Drop_to_upload_file": "Largue para enviar arquivos",
"Dry_run": "Simulação",
"Dry_run_description": "Enviará apenas um e-mail, para o mesmo endereço definido em 'De'. O e-mail deve pertencer a um usuário válido.",
@ -1166,7 +1176,8 @@
"Enable_Desktop_Notifications": "Habilitar Notificações Desktop",
"Enable_inquiry_fetch_by_stream": "Habilitar carga de dados de novos inquéritos de omnichannel utilizando stream",
"Enable_Svg_Favicon": "Ativar SVG favicon",
"Enable_two-factor_authentication": "Ativar autenticação de dois fatores",
"Enable_two-factor_authentication": "Ativar autenticação de dois fatores por TOTP",
"Enable_two-factor_authentication_email": "Enable two-factor authentication por Email",
"Enabled": "Ativado",
"Encrypted": "Criptografado",
"Encrypted_channel_Description": "Canal criptografado de Ponta a ponta. A pesquisa não funcionará com canais criptografados e as notificações podem não mostrar o conteúdo das mensagens.",
@ -1502,6 +1513,7 @@
"Header": "Cabeçalho",
"Header_and_Footer": "Cabeçalho e rodapé",
"Healthcare_and_Pharmaceutical": "Saúde / Farmacêutica",
"Here_is_your_authentication_code": "Aqui está o seu código de autenticação:",
"Help_Center": "Centro de ajuda",
"Helpers": "Ajudantes",
"Hex_Color_Preview": "Hex Color Preview",
@ -1543,6 +1555,7 @@
"If_you_are_sure_type_in_your_username": "Se você tem certeza, digite seu nome de usuário:",
"If_you_dont_have_one_send_an_email_to_omni_rocketchat_to_get_yours": "Se você não tiver um, envie um e-mail para [omni@rocket.chat] (mailto: omni@rocket.chat) para obter o seu.",
"If_you_didnt_ask_for_reset_ignore_this_email": "Se você não solicitou a redefinição de sua senha, ignore este e-mail.",
"If_you_didnt_try_to_login_in_your_account_please_ignore_this_email": "Por favor ignore este email se você não tentou entrar na sua conta.",
"Iframe_Integration": "Integração do Iframe",
"Iframe_Integration_receive_enable": "Habilitar Receber",
"Iframe_Integration_receive_enable_Description": "Permitir que a janela pai envie comandos para Rocket.Chat.",
@ -2506,6 +2519,7 @@
"Request_comment_when_closing_conversation": "Solicitar comentário ao encerrar a conversa",
"Request_comment_when_closing_conversation_description": "Se ativado, o agente precisará informar um comentário antes que a conversa seja encerrada.",
"Request_tag_before_closing_chat": "Solicitar tag(s) antes de encerrar a conversa",
"Require": "Exigir",
"Require_all_tokens": "Exigir todos os tokens",
"Require_any_token": "Exigir qualquer token",
"Require_password_change": "Exigir alteração de senha",
@ -2667,6 +2681,7 @@
"Send_invitation_email_error": "Você não forneceu um e-mail válido.",
"Send_invitation_email_info": "Você pode enviar vários convites por e-mail de uma vez.",
"Send_invitation_email_success": "Você enviou com sucesso um convite por e-mail para os seguintes endereços:",
"Send_me_the_code_again": "Envie-me o código novamente",
"Send_request_on_agent_message": "Enviar requisição para mensagens do Agente",
"Send_request_on_chat_close": "Enviar requisição ao fechar conversa",
"Send_request_on_lead_capture": "Enviar solicitação sobre a captura de chumbo",
@ -2980,6 +2995,8 @@
"Total_messages": "Quantidade de Mensagens",
"Total_Threads": "Total de Tópicos",
"Total_visitors": "Total de visitantes",
"totp-invalid": "Código ou senha invalidos",
"TOTP Invalid [totp-invalid]": "Código ou senha invalidos",
"Tourism": "Turismo",
"Transcript_Enabled": "Pergunte ao visitante se eles gostariam de uma transcrição após o bate-papo fechado",
"Transcript_message": "Mensagem para mostrar ao perguntar sobre Transcrição",
@ -2996,11 +3013,13 @@
"Turn_OFF": "Desligar",
"Turn_ON": "Ligar",
"Two Factor Authentication": "Autenticação de dois fatores",
"Two-factor_authentication": "Autenticação de dois fatores",
"Two-factor_authentication": "Autenticação de dois fatores por TOTP",
"Two-factor_authentication_email": "Autenticação de dois fatores por Email",
"Two-factor_authentication_disabled": "Autenticação de dois fatores desativada",
"Two-factor_authentication_enabled": "Autenticação de dois fatores ativada",
"Two-factor_authentication_is_currently_disabled": "A autenticação de dois fatores está atualmente desativada",
"Two-factor_authentication_is_currently_disabled": "A autenticação de dois fatores por TOTP está atualmente desativada",
"Two-factor_authentication_native_mobile_app_warning": "AVISO: depois de ativar isso, você não poderá fazer login nos aplicativos móveis nativos (Rocket.Chat +) usando sua senha até implementar o 2FA.",
"Two-factor_authentication_email_is_currently_disabled": "A autenticação de dois fatores por Email está atualmente desativada",
"Type": "Tipo",
"Type_your_email": "Digite seu e-mail",
"Type_your_job_title": "Digite o seu cargo",
@ -3176,6 +3195,7 @@
"Verified": "Verificado",
"Verify": "Verificar",
"Verify_your_email": "Verifique seu e-mail",
"Verify_your_email_for_the_code_we_sent": "Verifique o seu email pelo código que enviamos",
"Version": "Versão",
"Videos": "Vídeos",
"Video Conference": "Vídeo Conferência",
@ -3323,4 +3343,4 @@
"Your_question": "A sua pergunta",
"Your_server_link": "O link do seu servidor",
"Your_workspace_is_ready": "O seu espaço de trabalho está pronto a usar 🎉"
}
}

@ -203,6 +203,8 @@
"Accounts_Directory_DefaultView": "Lista de directoria por defeito",
"Accounts_SetDefaultAvatar": "Definir Avatar Padrão",
"Accounts_SetDefaultAvatar_Description": "Tenta determinar o avatar padrão com base em OAuth Account ou Gravatar",
"Accounts_Set_Email_Of_External_Accounts_as_Verified": "Definir o email das contas externas como verificado",
"Accounts_Set_Email_Of_External_Accounts_as_Verified_Description": "Contas criadas por serviços externos, tais como LDAP, OAth, etc, terão o email definido como verificado automáticamente",
"Accounts_ShowFormLogin": "Mostrar formulário de login",
"Accounts_TwoFactorAuthentication_Enabled": "Activar autenticação com dois parâmetros",
"Accounts_TwoFactorAuthentication_MaxDelta": "Delta máximo",

@ -1,6 +1,7 @@
import _ from 'underscore';
import { Accounts } from 'meteor/accounts-base';
import { settings } from '../../app/settings/server';
import { Users } from '../../app/models';
const orig_updateOrCreateUserFromExternalService = Accounts.updateOrCreateUserFromExternalService;
@ -36,7 +37,7 @@ Accounts.updateOrCreateUserFromExternalService = function(serviceName, serviceDa
if (user != null) {
const findQuery = {
address: serviceData.email,
verified: true,
verified: settings.get('Accounts_Verify_Email_For_External_Accounts'),
};
if (!_.findWhere(user.emails, findQuery)) {

@ -119,6 +119,8 @@ Accounts.onCreateUser(function(options, user = {}) {
}
if (user.services) {
const verified = settings.get('Accounts_Verify_Email_For_External_Accounts');
for (const service of Object.values(user.services)) {
if (!user.name) {
user.name = service.name || service.username;
@ -127,7 +129,7 @@ Accounts.onCreateUser(function(options, user = {}) {
if (!user.emails && service.email) {
user.emails = [{
address: service.email,
verified: true,
verified,
}];
}
}
@ -177,6 +179,14 @@ Accounts.insertUserDoc = _.wrap(Accounts.insertUserDoc, function(insertUserDoc,
user.type = 'user';
}
if (settings.get('Accounts_TwoFactorAuthentication_By_Email_Auto_Opt_In')) {
user.services = user.services || {};
user.services.email2fa = {
enabled: true,
changedAt: new Date(),
};
}
const _id = insertUserDoc.call(Accounts, options, user);
user = Meteor.users.findOne({

40
server/main.d.ts vendored

@ -0,0 +1,40 @@
/* eslint-disable @typescript-eslint/interface-name-prefix */
declare module 'meteor/random' {
namespace Random {
function _randomString(numberOfChars: number, map: string): string;
}
}
declare module 'meteor/accounts-base' {
namespace Accounts {
function _bcryptRounds(): number;
function _getLoginToken(connectionId: string): string | undefined;
}
}
declare module 'meteor/meteor' {
type globalError = Error;
namespace Meteor {
interface ErrorStatic {
new (error: string | number, reason?: string, details?: any): Error;
}
interface Error extends globalError {
error: string | number;
reason?: string;
details?: any;
}
const server: any;
interface MethodThisType {
twoFactorChecked: boolean | undefined;
}
}
}
declare module 'meteor/rocketchat:tap-i18n' {
namespace TAPi18n {
function __(s: string, options: { lng: string }): string;
}
}

@ -5,9 +5,10 @@ import { Accounts } from 'meteor/accounts-base';
import { saveCustomFields, passwordPolicy } from '../../app/lib';
import { Users } from '../../app/models';
import { settings as rcSettings } from '../../app/settings';
import { twoFactorRequired } from '../../app/2fa/server/twoFactorRequired';
Meteor.methods({
saveUserProfile(settings, customFields) {
saveUserProfile: twoFactorRequired(function(settings, customFields) {
check(settings, Object);
check(customFields, Match.Maybe(Object));
@ -91,5 +92,5 @@ Meteor.methods({
}
return true;
},
}),
});

@ -70,7 +70,7 @@ Meteor.startup(function() {
if (!Users.findOneByEmailAddress(process.env.ADMIN_EMAIL)) {
adminUser.emails = [{
address: process.env.ADMIN_EMAIL,
verified: true,
verified: process.env.ADMIN_EMAIL_VERIFIED === 'true',
}];
console.log(`Email: ${ process.env.ADMIN_EMAIL }`.green);
@ -160,7 +160,7 @@ Meteor.startup(function() {
emails: [
{
address: 'rocketchat.internal.admin.test@rocket.chat',
verified: true,
verified: false,
},
],
status: 'offline',

@ -174,4 +174,5 @@ import './v173';
import './v174';
import './v175';
import './v176';
import './v177';
import './xrun';

@ -5,7 +5,6 @@ import {
Settings,
} from '../../../app/models';
Migrations.add({
version: 172,
up() {

@ -0,0 +1,16 @@
import { Migrations } from '../../../app/migrations';
import { Settings } from '../../../app/models/server';
Migrations.add({
version: 177,
up() {
// Disable auto opt in for existent installations
Settings.upsert({
_id: 'Accounts_TwoFactorAuthentication_By_Email_Auto_Opt_In',
}, {
$set: {
value: false,
},
});
},
});
Loading…
Cancel
Save