diff --git a/backend/src/controllers/v2/authController.ts b/backend/src/controllers/v2/authController.ts index 1e9a73da0..fa41eda73 100644 --- a/backend/src/controllers/v2/authController.ts +++ b/backend/src/controllers/v2/authController.ts @@ -243,7 +243,6 @@ export const sendMfaToken = async (req: Request, res: Response) => { * @param res */ export const verifyMfaToken = async (req: Request, res: Response) => { - try { const { email, mfaToken } = req.body; await TokenService.validateToken({ @@ -296,15 +295,6 @@ export const verifyMfaToken = async (req: Request, res: Response) => { resObj.protectedKeyTag = user.protectedKeyTag; } - // case: user does not have MFA enabled - // return (access) token in response return res.status(200).send(resObj); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to authenticate. Try again?' - }); - } } diff --git a/backend/src/helpers/token.ts b/backend/src/helpers/token.ts index 026bb4ae7..ca8838151 100644 --- a/backend/src/helpers/token.ts +++ b/backend/src/helpers/token.ts @@ -12,7 +12,7 @@ import { import { SALT_ROUNDS } from '../config'; -import { ForbiddenRequestError } from '../utils/errors'; +import { UnauthorizedRequestError } from '../utils/errors'; /** * Create and store a token in the database for purpose [type] @@ -34,7 +34,7 @@ const createTokenHelper = async ({ phoneNumber?: string; organizationId?: Types.ObjectId }) => { - let token, expiresAt; + let token, expiresAt, triesLeft; try { // generate random token based on specified token use-case // type [type] @@ -47,6 +47,7 @@ const createTokenHelper = async ({ case TOKEN_EMAIL_MFA: // generate random 6-digit code token = String(crypto.randomInt(Math.pow(10, 5), Math.pow(10, 6) - 1)); + triesLeft = 5; expiresAt = new Date((new Date()).getTime() + 300000); break; case TOKEN_EMAIL_ORG_INVITATION: @@ -78,6 +79,7 @@ const createTokenHelper = async ({ phoneNumber?: string; organization?: Types.ObjectId; tokenHash: string; + triesLeft?: number; expiresAt: Date; } @@ -100,6 +102,10 @@ const createTokenHelper = async ({ query.organization = organizationId update.organization = organizationId } + + if (triesLeft) { + update.triesLeft = triesLeft; + } await TokenData.findOneAndUpdate( query, @@ -157,19 +163,51 @@ const validateTokenHelper = async ({ if (!tokenData) throw new Error('Failed to find token to validate'); if (tokenData.expiresAt < new Date()) { + // case: token expired await TokenData.findByIdAndDelete(tokenData._id); - throw ForbiddenRequestError({ - message: 'Failed token data validation due to token is no longer valid' + throw UnauthorizedRequestError({ + message: 'MFA session expired. Please log in again', + context: { + code: 'mfa_expired' + } }); } const isValid = await bcrypt.compare(token, tokenData.tokenHash); if (!isValid) { - throw ForbiddenRequestError({ - message: 'Failed token data validation due to incorrect token' + // case: token is not valid + if (tokenData?.triesLeft !== undefined) { + // case: token has a try-limit + if (tokenData.triesLeft === 1) { + // case: token is out of tries + await TokenData.findByIdAndDelete(tokenData._id); + } else { + // case: token has more than 1 try left + await TokenData.findByIdAndUpdate(tokenData._id, { + triesLeft: tokenData.triesLeft - 1 + }, { + new: true + }); + } + + throw UnauthorizedRequestError({ + message: 'MFA code is invalid', + context: { + code: 'mfa_invalid', + triesLeft: tokenData.triesLeft - 1 + } + }); + } + + throw UnauthorizedRequestError({ + message: 'MFA code is invalid', + context: { + code: 'mfa_invalid' + } }); } + // case: token is valid await TokenData.findByIdAndDelete(tokenData._id); } diff --git a/backend/src/middleware/requestErrorHandler.ts b/backend/src/middleware/requestErrorHandler.ts index f70b989d2..653c0df2e 100644 --- a/backend/src/middleware/requestErrorHandler.ts +++ b/backend/src/middleware/requestErrorHandler.ts @@ -17,9 +17,7 @@ export const requestErrorHandler: ErrorRequestHandler = (error: RequestError | E } //TODO: Find better way to type check for error. In current setting you need to cast type to get the functions and variables from RequestError - if (error instanceof TokenExpiredError) { - error = UnauthorizedRequestError({ stack: error.stack, message: 'Token expired' }); - } else if (!(error instanceof RequestError)) { + if (!(error instanceof RequestError)) { error = InternalServerError({ context: { exception: error.message }, stack: error.stack }) getLogger('backend-main').log((error).levelName.toLowerCase(), (error).message) } diff --git a/backend/src/models/tokenData.ts b/backend/src/models/tokenData.ts index ad23d56aa..2ee39c694 100644 --- a/backend/src/models/tokenData.ts +++ b/backend/src/models/tokenData.ts @@ -6,6 +6,7 @@ export interface ITokenData { phoneNumber?: string; organization?: Types.ObjectId; tokenHash: string; + triesLeft?: number; expiresAt: Date; createdAt: Date; updatedAt: Date; @@ -37,6 +38,9 @@ const tokenDataSchema = new Schema({ select: false, required: true }, + triesLeft: { + type: Number + }, expiresAt: { type: Date, expires: 0, diff --git a/frontend/src/components/login/LoginStep.tsx b/frontend/src/components/login/LoginStep.tsx index 42d3e6d22..b19bafdd9 100644 --- a/frontend/src/components/login/LoginStep.tsx +++ b/frontend/src/components/login/LoginStep.tsx @@ -63,7 +63,6 @@ export default function LoginStep ({ } } catch (err) { - console.error(err); setLoginError(true); } diff --git a/frontend/src/components/login/MFAStep.tsx b/frontend/src/components/login/MFAStep.tsx index 8793064b1..c7e1e70e9 100644 --- a/frontend/src/components/login/MFAStep.tsx +++ b/frontend/src/components/login/MFAStep.tsx @@ -30,6 +30,18 @@ const props = { } } as const; +interface VerifyMfaTokenError { + response: { + data: { + context: { + code: string; + triesLeft: number; + } + }, + status: number; + } +} + /** * 2nd step of login - users enter their MFA code * @param {Object} obj @@ -73,7 +85,15 @@ export default function MFAStep({ } } catch (err) { - console.error(err); + const error = err as VerifyMfaTokenError; + + if (error?.response?.status === 500) { + window.location.reload(); + } else if (error?.response?.data?.context?.triesLeft === 0) { + window.location.reload(); + router.push('/login'); + } + setIsLoading(false); setCodeError(true); }