From 0f2b6bbe1a56debfc347ff8ec6bff0b33dc33373 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Thu, 17 Aug 2023 15:13:04 +0100 Subject: [PATCH] fix: add 2fa and email verification grace period (#10771) --- .../pages/api/auth/two-factor/totp/disable.ts | 4 +- .../pages/api/auth/two-factor/totp/enable.ts | 4 +- apps/web/playwright/login.2fa.e2e.ts | 3 +- .../features/auth/lib/next-auth-options.ts | 5 +- packages/lib/totp.ts | 48 +++++++++++++++++++ .../loggedInViewer/deleteMe.handler.ts | 5 +- .../auth/verifyCodeUnAuthenticated.handler.ts | 5 +- .../organizations/verifyCode.handler.ts | 5 +- 8 files changed, 64 insertions(+), 15 deletions(-) create mode 100644 packages/lib/totp.ts diff --git a/apps/web/pages/api/auth/two-factor/totp/disable.ts b/apps/web/pages/api/auth/two-factor/totp/disable.ts index 339d7f35bf..abc0835c4a 100644 --- a/apps/web/pages/api/auth/two-factor/totp/disable.ts +++ b/apps/web/pages/api/auth/two-factor/totp/disable.ts @@ -1,10 +1,10 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import { authenticator } from "otplib"; import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode"; import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword"; import { symmetricDecrypt } from "@calcom/lib/crypto"; +import { totpAuthenticatorCheck } from "@calcom/lib/totp"; import prisma from "@calcom/prisma"; import { IdentityProvider } from "@calcom/prisma/client"; @@ -69,7 +69,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) } // If user has 2fa enabled, check if body.code is correct - const isValidToken = authenticator.check(req.body.code, secret); + const isValidToken = totpAuthenticatorCheck(req.body.code, secret); if (!isValidToken) { return res.status(400).json({ error: ErrorCode.IncorrectTwoFactorCode }); diff --git a/apps/web/pages/api/auth/two-factor/totp/enable.ts b/apps/web/pages/api/auth/two-factor/totp/enable.ts index 98322cc34c..4dd32f0ca3 100644 --- a/apps/web/pages/api/auth/two-factor/totp/enable.ts +++ b/apps/web/pages/api/auth/two-factor/totp/enable.ts @@ -1,9 +1,9 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import { authenticator } from "otplib"; import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode"; import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import { symmetricDecrypt } from "@calcom/lib/crypto"; +import { totpAuthenticatorCheck } from "@calcom/lib/totp"; import prisma from "@calcom/prisma"; export default async function handler(req: NextApiRequest, res: NextApiResponse) { @@ -48,7 +48,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) return res.status(500).json({ error: ErrorCode.InternalServerError }); } - const isValidToken = authenticator.check(req.body.code, secret); + const isValidToken = totpAuthenticatorCheck(req.body.code, secret); if (!isValidToken) { return res.status(400).json({ error: ErrorCode.IncorrectTwoFactorCode }); } diff --git a/apps/web/playwright/login.2fa.e2e.ts b/apps/web/playwright/login.2fa.e2e.ts index c77ae9f060..5aff129949 100644 --- a/apps/web/playwright/login.2fa.e2e.ts +++ b/apps/web/playwright/login.2fa.e2e.ts @@ -3,6 +3,7 @@ import { expect } from "@playwright/test"; import { authenticator } from "otplib"; import { symmetricDecrypt } from "@calcom/lib/crypto"; +import { totpAuthenticatorCheck } from "@calcom/lib/totp"; import { test } from "./lib/fixtures"; @@ -141,7 +142,7 @@ test.describe("2FA Tests", async () => { async function fillOtp({ page, secret, noRetry }: { page: Page; secret: string; noRetry?: boolean }) { let token = authenticator.generate(secret); - if (!noRetry && !authenticator.check(token, secret)) { + if (!noRetry && !totpAuthenticatorCheck(token, secret)) { console.log("Token expired, Renerating."); // Maybe token was just about to expire, try again just once more token = authenticator.generate(secret); diff --git a/packages/features/auth/lib/next-auth-options.ts b/packages/features/auth/lib/next-auth-options.ts index e9f537988a..ef91f9db47 100644 --- a/packages/features/auth/lib/next-auth-options.ts +++ b/packages/features/auth/lib/next-auth-options.ts @@ -148,7 +148,10 @@ const providers: Provider[] = [ throw new Error(ErrorCode.InternalServerError); } - const isValidToken = (await import("otplib")).authenticator.check(credentials.totpCode, secret); + const isValidToken = (await import("@calcom/lib/totp")).totpAuthenticatorCheck( + credentials.totpCode, + secret + ); if (!isValidToken) { throw new Error(ErrorCode.IncorrectTwoFactorCode); } diff --git a/packages/lib/totp.ts b/packages/lib/totp.ts new file mode 100644 index 0000000000..e1390aed28 --- /dev/null +++ b/packages/lib/totp.ts @@ -0,0 +1,48 @@ +import { Authenticator, TOTP } from "@otplib/core"; +import type { AuthenticatorOptions } from "@otplib/core/authenticator"; +import type { TOTPOptions } from "@otplib/core/totp"; +import { createDigest, createRandomBytes } from "@otplib/plugin-crypto"; +import { keyDecoder, keyEncoder } from "@otplib/plugin-thirty-two"; + +/** + * Checks the validity of a TOTP token using a base32-encoded secret. + * + * @param token - The token. + * @param secret - The base32-encoded shared secret. + * @param opts - The AuthenticatorOptions object. + * @param opts.window - The amount of past and future tokens considered valid. Either a single value or array of `[past, future]`. Default: `[1, 0]` + */ +export const totpAuthenticatorCheck = ( + token: string, + secret: string, + opts: Partial = {} +) => { + const { window = [1, 0], ...rest } = opts; + const authenticator = new Authenticator({ + createDigest, + createRandomBytes, + keyDecoder, + keyEncoder, + window, + ...rest, + }); + return authenticator.check(token, secret); +}; + +/** + * Checks the validity of a TOTP token using a raw secret. + * + * @param token - The token. + * @param secret - The raw hex-encoded shared secret. + * @param opts - The TOTPOptions object. + * @param opts.window - The amount of past and future tokens considered valid. Either a single value or array of `[past, future]`. Default: `[1, 0]` + */ +export const totpRawCheck = (token: string, secret: string, opts: Partial = {}) => { + const { window = [1, 0], ...rest } = opts; + const authenticator = new TOTP({ + createDigest, + window, + ...rest, + }); + return authenticator.check(token, secret); +}; diff --git a/packages/trpc/server/routers/loggedInViewer/deleteMe.handler.ts b/packages/trpc/server/routers/loggedInViewer/deleteMe.handler.ts index 58b9eba27b..dac7b1d54c 100644 --- a/packages/trpc/server/routers/loggedInViewer/deleteMe.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/deleteMe.handler.ts @@ -1,10 +1,9 @@ -import { authenticator } from "otplib"; - import { deleteStripeCustomer } from "@calcom/app-store/stripepayment/lib/customer"; import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode"; import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword"; import { symmetricDecrypt } from "@calcom/lib/crypto"; import { deleteWebUser as syncServicesDeleteWebUser } from "@calcom/lib/sync/SyncServiceManager"; +import { totpAuthenticatorCheck } from "@calcom/lib/totp"; import { prisma } from "@calcom/prisma"; import { IdentityProvider } from "@calcom/prisma/enums"; import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; @@ -66,7 +65,7 @@ export const deleteMeHandler = async ({ ctx, input }: DeleteMeOptions) => { } // If user has 2fa enabled, check if input.totpCode is correct - const isValidToken = authenticator.check(input.totpCode, secret); + const isValidToken = totpAuthenticatorCheck(input.totpCode, secret); if (!isValidToken) { throw new Error(ErrorCode.IncorrectTwoFactorCode); } diff --git a/packages/trpc/server/routers/viewer/auth/verifyCodeUnAuthenticated.handler.ts b/packages/trpc/server/routers/viewer/auth/verifyCodeUnAuthenticated.handler.ts index 467c936683..842e440892 100644 --- a/packages/trpc/server/routers/viewer/auth/verifyCodeUnAuthenticated.handler.ts +++ b/packages/trpc/server/routers/viewer/auth/verifyCodeUnAuthenticated.handler.ts @@ -1,6 +1,6 @@ import { createHash } from "crypto"; -import { totp } from "otplib"; +import { totpRawCheck } from "@calcom/lib/totp"; import type { ZVerifyCodeInputSchema } from "@calcom/prisma/zod-utils"; import { TRPCError } from "@trpc/server"; @@ -18,8 +18,7 @@ export const verifyCodeUnAuthenticatedHandler = async ({ input }: VerifyTokenOpt .update(email + process.env.CALENDSO_ENCRYPTION_KEY) .digest("hex"); - totp.options = { step: 900 }; - const isValidToken = totp.check(code, secret); + const isValidToken = totpRawCheck(code, secret, { step: 900 }); if (!isValidToken) throw new TRPCError({ code: "BAD_REQUEST", message: "invalid_code" }); diff --git a/packages/trpc/server/routers/viewer/organizations/verifyCode.handler.ts b/packages/trpc/server/routers/viewer/organizations/verifyCode.handler.ts index 3cc61bc385..885bb3b6ac 100644 --- a/packages/trpc/server/routers/viewer/organizations/verifyCode.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/verifyCode.handler.ts @@ -1,8 +1,8 @@ import { createHash } from "crypto"; -import { totp } from "otplib"; import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; import { IS_PRODUCTION } from "@calcom/lib/constants"; +import { totpRawCheck } from "@calcom/lib/totp"; import type { ZVerifyCodeInputSchema } from "@calcom/prisma/zod-utils"; import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; @@ -31,8 +31,7 @@ export const verifyCodeHandler = async ({ ctx, input }: VerifyCodeOptions) => { .update(email + process.env.CALENDSO_ENCRYPTION_KEY) .digest("hex"); - totp.options = { step: 900 }; - const isValidToken = totp.check(code, secret); + const isValidToken = totpRawCheck(code, secret, { step: 900 }); if (!isValidToken) throw new TRPCError({ code: "BAD_REQUEST", message: "invalid_code" });