From 0901f87fb775c9731f3ba409c16575d8b08b263a Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Mon, 16 Jan 2023 16:49:34 -0300 Subject: [PATCH 1/3] Add support to 2FA errors for Meteor.callAsync --- .../app/2fa/client/overrideMeteorCall.ts | 30 ++++++++++- .../meteor/client/lib/2fa/process2faReturn.ts | 50 +++++++++++++++++++ 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/apps/meteor/app/2fa/client/overrideMeteorCall.ts b/apps/meteor/app/2fa/client/overrideMeteorCall.ts index ac19a24219fc..382499e2aedc 100644 --- a/apps/meteor/app/2fa/client/overrideMeteorCall.ts +++ b/apps/meteor/app/2fa/client/overrideMeteorCall.ts @@ -1,10 +1,10 @@ import { Meteor } from 'meteor/meteor'; import { t } from '../../utils/client'; -import { process2faReturn } from '../../../client/lib/2fa/process2faReturn'; +import { process2faReturn, process2faAsyncReturn } from '../../../client/lib/2fa/process2faReturn'; import { isTotpInvalidError } from '../../../client/lib/2fa/utils'; -const { call } = Meteor; +const { call, callAsync } = Meteor; type Callback = { (error: unknown): void; @@ -34,8 +34,34 @@ const callWithoutTotp = (methodName: string, args: unknown[], callback: Callback }); }); +const callAsyncWithTotp = + (methodName: string, args: unknown[]) => + async (twoFactorCode: string, twoFactorMethod: string): Promise => { + try { + const result = await callAsync(methodName, ...args, { twoFactorCode, twoFactorMethod }); + + return result; + } catch (error: unknown) { + if (isTotpInvalidError(error)) { + throw new Error(twoFactorMethod === 'password' ? t('Invalid_password') : t('Invalid_two_factor_code')); + } + + throw error; + } + }; + Meteor.call = function (methodName: string, ...args: unknown[]): unknown { const callback = args.length > 0 && typeof args[args.length - 1] === 'function' ? (args.pop() as Callback) : (): void => undefined; return callWithoutTotp(methodName, args, callback)(); }; + +Meteor.callAsync = async function _callAsyncWithTotp(methodName: string, ...args: unknown[]): Promise { + const promise = callAsync(methodName, ...args); + + return process2faAsyncReturn({ + promise, + onCode: callAsyncWithTotp(methodName, args), + emailOrUsername: undefined, + }); +}; diff --git a/apps/meteor/client/lib/2fa/process2faReturn.ts b/apps/meteor/client/lib/2fa/process2faReturn.ts index 6ae60dcaf002..b76c0220fc25 100644 --- a/apps/meteor/client/lib/2fa/process2faReturn.ts +++ b/apps/meteor/client/lib/2fa/process2faReturn.ts @@ -76,3 +76,53 @@ export function process2faReturn({ }, }); } + +export async function process2faAsyncReturn({ + promise, + onCode, + emailOrUsername, +}: { + promise: Promise; + onCode: (code: string, method: string) => void; + emailOrUsername: string | null | undefined; +}): Promise { + return new Promise((resolve, reject) => { + promise + // if the promise is resolved, we don't need to do anything + .then((result) => { + resolve(result); + }) + // if the promise is rejected, we need to check if it's a 2fa error + .catch((error) => { + // if it's not a 2fa error, we reject the promise + if (!isTotpRequiredError(error) || !hasRequiredTwoFactorMethod(error)) { + reject(error); + return; + } + + const props = { + method: error.details.method, + emailOrUsername: emailOrUsername || error.details.emailOrUsername || Meteor.user()?.username, + }; + + assertModalProps(props); + + imperativeModal.open({ + component: TwoFactorModal, + props: { + ...props, + onConfirm: (code: string, method: string): void => { + imperativeModal.close(); + + // once we have the code, we resolve the promise with the result of the `onCode` callback + resolve(onCode(method === 'password' ? SHA256(code) : code, method)); + }, + onClose: (): void => { + imperativeModal.close(); + reject(new Meteor.Error('totp-canceled')); + }, + }, + }); + }); + }); +} From 68a3f6b4577cc545e2691f2836028c87fad040c0 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Tue, 17 Jan 2023 06:09:51 -0800 Subject: [PATCH 2/3] Apply suggestions from code review Co-authored-by: Guilherme Gazzo --- apps/meteor/client/lib/2fa/process2faReturn.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/meteor/client/lib/2fa/process2faReturn.ts b/apps/meteor/client/lib/2fa/process2faReturn.ts index b76c0220fc25..db6995834755 100644 --- a/apps/meteor/client/lib/2fa/process2faReturn.ts +++ b/apps/meteor/client/lib/2fa/process2faReturn.ts @@ -89,9 +89,7 @@ export async function process2faAsyncReturn({ return new Promise((resolve, reject) => { promise // if the promise is resolved, we don't need to do anything - .then((result) => { - resolve(result); - }) + .then(resolve) // if the promise is rejected, we need to check if it's a 2fa error .catch((error) => { // if it's not a 2fa error, we reject the promise From 1c9b34d7df4f735d75f0dac970a812480984cb7e Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Tue, 17 Jan 2023 12:02:04 -0300 Subject: [PATCH 3/3] fix review --- .../meteor/client/lib/2fa/process2faReturn.ts | 64 +++++++++---------- 1 file changed, 30 insertions(+), 34 deletions(-) diff --git a/apps/meteor/client/lib/2fa/process2faReturn.ts b/apps/meteor/client/lib/2fa/process2faReturn.ts index db6995834755..98b213b70a72 100644 --- a/apps/meteor/client/lib/2fa/process2faReturn.ts +++ b/apps/meteor/client/lib/2fa/process2faReturn.ts @@ -86,41 +86,37 @@ export async function process2faAsyncReturn({ onCode: (code: string, method: string) => void; emailOrUsername: string | null | undefined; }): Promise { - return new Promise((resolve, reject) => { - promise - // if the promise is resolved, we don't need to do anything - .then(resolve) - // if the promise is rejected, we need to check if it's a 2fa error - .catch((error) => { - // if it's not a 2fa error, we reject the promise - if (!isTotpRequiredError(error) || !hasRequiredTwoFactorMethod(error)) { - reject(error); - return; - } - - const props = { - method: error.details.method, - emailOrUsername: emailOrUsername || error.details.emailOrUsername || Meteor.user()?.username, - }; - - assertModalProps(props); - - imperativeModal.open({ - component: TwoFactorModal, - props: { - ...props, - onConfirm: (code: string, method: string): void => { - imperativeModal.close(); - - // once we have the code, we resolve the promise with the result of the `onCode` callback - resolve(onCode(method === 'password' ? SHA256(code) : code, method)); - }, - onClose: (): void => { - imperativeModal.close(); - reject(new Meteor.Error('totp-canceled')); - }, + // if the promise is rejected, we need to check if it's a 2fa error + return promise.catch((error) => { + // if it's not a 2fa error, we reject the promise + if (!isTotpRequiredError(error) || !hasRequiredTwoFactorMethod(error)) { + throw error; + } + + const props = { + method: error.details.method, + emailOrUsername: emailOrUsername || error.details.emailOrUsername || Meteor.user()?.username, + }; + + assertModalProps(props); + + return new Promise((resolve, reject) => { + imperativeModal.open({ + component: TwoFactorModal, + props: { + ...props, + onConfirm: (code: string, method: string): void => { + imperativeModal.close(); + + // once we have the code, we resolve the promise with the result of the `onCode` callback + resolve(onCode(method === 'password' ? SHA256(code) : code, method)); }, - }); + onClose: (): void => { + imperativeModal.close(); + reject(new Meteor.Error('totp-canceled')); + }, + }, }); + }); }); }