Chore: Rewrite 2fa to typescript (#25285)

pull/25425/head^2
Kevin Aleman 4 years ago committed by GitHub
parent c731037a31
commit d4f4e550e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      apps/meteor/app/2fa/server/code/TOTPCheck.ts
  2. 0
      apps/meteor/app/2fa/server/index.ts
  3. 11
      apps/meteor/app/2fa/server/lib/totp.ts
  4. 46
      apps/meteor/app/2fa/server/loginHandler.ts
  5. 6
      apps/meteor/app/2fa/server/methods/checkCodesRemaining.ts
  6. 15
      apps/meteor/app/2fa/server/methods/disable.ts
  7. 13
      apps/meteor/app/2fa/server/methods/enable.ts
  8. 12
      apps/meteor/app/2fa/server/methods/regenerateCodes.ts
  9. 10
      apps/meteor/app/2fa/server/methods/validateTempToken.ts
  10. 6
      apps/meteor/app/authentication/server/ILoginAttempt.ts
  11. 16
      apps/meteor/definition/externals/meteor/oauth.d.ts
  12. 2
      apps/meteor/lib/callbacks.ts
  13. 1
      apps/meteor/package.json
  14. 10
      yarn.lock

@ -20,6 +20,10 @@ export class TOTPCheck implements ICodeCheck {
return false;
}
if (!user.services?.totp?.secret) {
return false;
}
return TOTP.verify({
secret: user.services?.totp?.secret,
token: code,

@ -2,22 +2,23 @@ import { SHA256 } from 'meteor/sha';
import { Random } from 'meteor/random';
import speakeasy from 'speakeasy';
// @ts-expect-error
import { Users } from '../../../models';
import { settings } from '../../../settings/server';
export const TOTP = {
generateSecret() {
generateSecret(): speakeasy.GeneratedSecret {
return speakeasy.generateSecret();
},
generateOtpauthURL(secret, username) {
generateOtpauthURL(secret: speakeasy.GeneratedSecret, username: string): string {
return speakeasy.otpauthURL({
secret: secret.ascii,
label: `Rocket.Chat:${username}`,
});
},
verify({ secret, token, backupTokens, userId }) {
verify({ secret, token, backupTokens, userId }: { secret: string; token: string; backupTokens?: string[]; userId?: string }): boolean {
// validates a backup code
if (token.length === 8 && backupTokens) {
const hashedCode = SHA256(token);
@ -34,7 +35,7 @@ export const TOTP = {
return false;
}
const maxDelta = settings.get('Accounts_TwoFactorAuthentication_MaxDelta');
const maxDelta = settings.get<number>('Accounts_TwoFactorAuthentication_MaxDelta');
if (maxDelta) {
const verifiedDelta = speakeasy.totp.verifyDelta({
secret,
@ -53,7 +54,7 @@ export const TOTP = {
});
},
generateCodes() {
generateCodes(): { codes: string[]; hashedCodes: string[] } {
// generate 12 backup codes
const codes = [];
const hashedCodes = [];

@ -6,11 +6,20 @@ import { check } from 'meteor/check';
import { callbacks } from '../../../lib/callbacks';
import { checkCodeForUser } from './code/index';
const isMeteorError = (error: any): error is Meteor.Error => {
return error?.meteorError !== undefined;
};
const isCredentialWithError = (credential: any): credential is { error: Error } => {
return credential?.error !== undefined;
};
Accounts.registerLoginHandler('totp', function (options) {
if (!options.totp || !options.totp.code) {
return;
}
// @ts-expect-error - not sure how to type this yet
return Accounts._runLoginHandlers(this, options.totp.login);
});
@ -27,11 +36,15 @@ callbacks.add(
return login;
}
if (!login.user) {
return login;
}
const { totp } = loginArgs;
checkCodeForUser({
user: login.user,
code: totp && totp.code,
code: totp?.code,
options: { disablePasswordFallback: true },
});
@ -41,24 +54,27 @@ callbacks.add(
'2fa',
);
const recreateError = (errorDoc) => {
let error;
const copyTo = <T extends Error>(from: T, to: T): T => {
Object.getOwnPropertyNames(to).forEach((key) => {
const idx: keyof T = key as keyof T;
to[idx] = from[idx];
});
if (errorDoc.meteorError) {
error = new Meteor.Error();
delete errorDoc.meteorError;
} else {
error = new Error();
return to;
};
const recreateError = (errorDoc: Error | Meteor.Error): Error | Meteor.Error => {
if (isMeteorError(errorDoc)) {
const error = new Meteor.Error('');
return copyTo(errorDoc, error);
}
Object.getOwnPropertyNames(errorDoc).forEach((key) => {
error[key] = errorDoc[key];
});
return error;
const error = new Error();
return copyTo(errorDoc, error);
};
OAuth._retrievePendingCredential = function (key, ...args) {
const credentialSecret = args.length > 0 && args[0] !== undefined ? args[0] : null;
OAuth._retrievePendingCredential = function (key, ...args): string | Error | void {
const credentialSecret = args.length > 0 && args[0] !== undefined ? args[0] : undefined;
check(key, String);
const pendingCredential = OAuth._pendingCredentials.findOne({
@ -70,7 +86,7 @@ OAuth._retrievePendingCredential = function (key, ...args) {
return;
}
if (pendingCredential.credential.error) {
if (isCredentialWithError(pendingCredential.credential)) {
OAuth._pendingCredentials.remove({
_id: pendingCredential._id,
});

@ -8,6 +8,12 @@ Meteor.methods({
const user = Meteor.user();
if (!user) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
method: '2fa:checkCodesRemaining',
});
}
if (!user.services || !user.services.totp || !user.services.totp.enabled) {
throw new Meteor.Error('invalid-totp');
}

@ -1,20 +1,27 @@
import { Meteor } from 'meteor/meteor';
import { Users } from '../../../models';
import { Users } from '../../../models/server';
import { TOTP } from '../lib/totp';
Meteor.methods({
'2fa:disable'(code) {
if (!Meteor.userId()) {
const userId = Meteor.userId();
if (!userId) {
throw new Meteor.Error('not-authorized');
}
const user = Meteor.user();
if (!user) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
method: '2fa:disable',
});
}
const verified = TOTP.verify({
secret: user.services.totp.secret,
token: code,
userId: Meteor.userId(),
userId,
backupTokens: user.services.totp.hashedBackup,
});
@ -22,6 +29,6 @@ Meteor.methods({
return false;
}
return Users.disable2FAByUserId(Meteor.userId());
return Users.disable2FAByUserId(userId);
},
});

@ -1,19 +1,26 @@
import { Meteor } from 'meteor/meteor';
import { Users } from '../../../models';
import { Users } from '../../../models/server';
import { TOTP } from '../lib/totp';
Meteor.methods({
'2fa:enable'() {
if (!Meteor.userId()) {
const userId = Meteor.userId();
if (!userId) {
throw new Meteor.Error('not-authorized');
}
const user = Meteor.user();
if (!user || !user.username) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
method: '2fa:enable',
});
}
const secret = TOTP.generateSecret();
Users.disable2FAAndSetTempSecretByUserId(Meteor.userId(), secret.base32);
Users.disable2FAAndSetTempSecretByUserId(userId, secret.base32);
return {
secret: secret.base32,

@ -1,15 +1,21 @@
import { Meteor } from 'meteor/meteor';
import { Users } from '../../../models';
import { Users } from '../../../models/server';
import { TOTP } from '../lib/totp';
Meteor.methods({
'2fa:regenerateCodes'(userToken) {
if (!Meteor.userId()) {
const userId = Meteor.userId();
if (!userId) {
throw new Meteor.Error('not-authorized');
}
const user = Meteor.user();
if (!user) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
method: '2fa:regenerateCodes',
});
}
if (!user.services || !user.services.totp || !user.services.totp.enabled) {
throw new Meteor.Error('invalid-totp');
@ -18,7 +24,7 @@ Meteor.methods({
const verified = TOTP.verify({
secret: user.services.totp.secret,
token: userToken,
userId: Meteor.userId(),
userId,
backupTokens: user.services.totp.hashedBackup,
});

@ -1,15 +1,21 @@
import { Meteor } from 'meteor/meteor';
import { Users } from '../../../models';
import { Users } from '../../../models/server';
import { TOTP } from '../lib/totp';
Meteor.methods({
'2fa:validateTempToken'(userToken) {
if (!Meteor.userId()) {
const userId = Meteor.userId();
if (!userId) {
throw new Meteor.Error('not-authorized');
}
const user = Meteor.user();
if (!user) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
method: '2fa:validateTempToken',
});
}
if (!user.services || !user.services.totp || !user.services.totp.tempSecret) {
throw new Meteor.Error('invalid-totp');

@ -7,6 +7,12 @@ interface IMethodArgument {
algorithm: string;
};
resume?: string;
cas?: boolean;
totp?: {
code: string;
};
}
export interface ILoginAttempt {

@ -1,7 +1,23 @@
declare module 'meteor/oauth' {
import { Mongo } from 'meteor/mongo';
import { IRocketChatRecord } from '@rocket.chat/core-typings';
interface IOauthCredentials extends IRocketChatRecord {
key: string;
credentialSecret: string;
credential:
| {
error: Error;
}
| string;
}
namespace OAuth {
function _redirectUri(serviceName: string, config: any, params: any, absoluteUrlOptions: any): string;
function _retrieveCredentialSecret(credentialToken: string): string | null;
function _retrievePendingCredential(key: string, ...args: string[]): void;
function openSecret(secret: string): string;
const _storageTokenPrefix: string;
const _pendingCredentials: Mongo.Collection<IOauthCredentials>;
}
}

@ -14,6 +14,7 @@ import type {
import type { Logger } from '../app/logger/server';
import type { IBusinessHourBehavior } from '../app/livechat/server/business-hour/AbstractBusinessHour';
import { getRandomId } from './random';
import { ILoginAttempt } from '../app/authentication/server/ILoginAttempt';
enum CallbackPriority {
HIGH = -1000,
@ -54,6 +55,7 @@ type EventLikeCallbackSignatures = {
'beforeJoinDefaultChannels': (user: IUser) => void;
'beforeCreateChannel': (owner: IUser, room: IRoom) => void;
'afterCreateRoom': (owner: IUser, room: IRoom) => void;
'onValidateLogin': (login: ILoginAttempt) => void;
};
/**

@ -215,6 +215,7 @@
"@types/lodash": "^4.14.177",
"@types/lodash.debounce": "^4.0.6",
"@types/proxy-from-env": "^1.0.1",
"@types/speakeasy": "^2.0.7",
"adm-zip": "0.5.9",
"agenda": "https://github.com/RocketChat/agenda#c2cfcc532b8409561104dca980e6adbbcbdf5442",
"ajv": "^8.7.1",

@ -4037,6 +4037,7 @@ __metadata:
"@types/rewire": ^2.5.28
"@types/semver": ^7.3.6
"@types/sharp": ^0.29.4
"@types/speakeasy": ^2.0.7
"@types/string-strip-html": ^5.0.0
"@types/supertest": ^2.0.11
"@types/toastr": ^2.1.39
@ -6984,6 +6985,15 @@ __metadata:
languageName: node
linkType: hard
"@types/speakeasy@npm:^2.0.7":
version: 2.0.7
resolution: "@types/speakeasy@npm:2.0.7"
dependencies:
"@types/node": "*"
checksum: 30152d950ea23654060ef596ea459935a9ea80ba4d9803b13fc9b02c7a27a7b5c96742f2cb00db51b19ba0e13ef9a16c1fd977042f61c9019b10c4191e2f1b97
languageName: node
linkType: hard
"@types/stack-utils@npm:^2.0.0":
version: 2.0.1
resolution: "@types/stack-utils@npm:2.0.1"

Loading…
Cancel
Save