fix: add 2fa and email verification grace period (#10771)

pull/10816/head^2
nicktrn 2023-08-17 15:13:04 +01:00 committed by GitHub
parent 28b94a865f
commit 0f2b6bbe1a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 64 additions and 15 deletions

View File

@ -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 });

View File

@ -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 });
}

View File

@ -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);

View File

@ -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);
}

48
packages/lib/totp.ts Normal file
View File

@ -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<AuthenticatorOptions> = {}
) => {
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<TOTPOptions> = {}) => {
const { window = [1, 0], ...rest } = opts;
const authenticator = new TOTP({
createDigest,
window,
...rest,
});
return authenticator.check(token, secret);
};

View File

@ -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);
}

View File

@ -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" });

View File

@ -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" });