fix: add 2fa and email verification grace period (#10771)
This commit is contained in:
parent
28b94a865f
commit
0f2b6bbe1a
|
@ -1,10 +1,10 @@
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { authenticator } from "otplib";
|
|
||||||
|
|
||||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
|
import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
|
||||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||||
|
import { totpAuthenticatorCheck } from "@calcom/lib/totp";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { IdentityProvider } from "@calcom/prisma/client";
|
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
|
// 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) {
|
if (!isValidToken) {
|
||||||
return res.status(400).json({ error: ErrorCode.IncorrectTwoFactorCode });
|
return res.status(400).json({ error: ErrorCode.IncorrectTwoFactorCode });
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { authenticator } from "otplib";
|
|
||||||
|
|
||||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||||
|
import { totpAuthenticatorCheck } from "@calcom/lib/totp";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
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 });
|
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) {
|
if (!isValidToken) {
|
||||||
return res.status(400).json({ error: ErrorCode.IncorrectTwoFactorCode });
|
return res.status(400).json({ error: ErrorCode.IncorrectTwoFactorCode });
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { expect } from "@playwright/test";
|
||||||
import { authenticator } from "otplib";
|
import { authenticator } from "otplib";
|
||||||
|
|
||||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||||
|
import { totpAuthenticatorCheck } from "@calcom/lib/totp";
|
||||||
|
|
||||||
import { test } from "./lib/fixtures";
|
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 }) {
|
async function fillOtp({ page, secret, noRetry }: { page: Page; secret: string; noRetry?: boolean }) {
|
||||||
let token = authenticator.generate(secret);
|
let token = authenticator.generate(secret);
|
||||||
if (!noRetry && !authenticator.check(token, secret)) {
|
if (!noRetry && !totpAuthenticatorCheck(token, secret)) {
|
||||||
console.log("Token expired, Renerating.");
|
console.log("Token expired, Renerating.");
|
||||||
// Maybe token was just about to expire, try again just once more
|
// Maybe token was just about to expire, try again just once more
|
||||||
token = authenticator.generate(secret);
|
token = authenticator.generate(secret);
|
||||||
|
|
|
@ -148,7 +148,10 @@ const providers: Provider[] = [
|
||||||
throw new Error(ErrorCode.InternalServerError);
|
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) {
|
if (!isValidToken) {
|
||||||
throw new Error(ErrorCode.IncorrectTwoFactorCode);
|
throw new Error(ErrorCode.IncorrectTwoFactorCode);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
};
|
|
@ -1,10 +1,9 @@
|
||||||
import { authenticator } from "otplib";
|
|
||||||
|
|
||||||
import { deleteStripeCustomer } from "@calcom/app-store/stripepayment/lib/customer";
|
import { deleteStripeCustomer } from "@calcom/app-store/stripepayment/lib/customer";
|
||||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||||
import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
|
import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
|
||||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||||
import { deleteWebUser as syncServicesDeleteWebUser } from "@calcom/lib/sync/SyncServiceManager";
|
import { deleteWebUser as syncServicesDeleteWebUser } from "@calcom/lib/sync/SyncServiceManager";
|
||||||
|
import { totpAuthenticatorCheck } from "@calcom/lib/totp";
|
||||||
import { prisma } from "@calcom/prisma";
|
import { prisma } from "@calcom/prisma";
|
||||||
import { IdentityProvider } from "@calcom/prisma/enums";
|
import { IdentityProvider } from "@calcom/prisma/enums";
|
||||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
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
|
// 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) {
|
if (!isValidToken) {
|
||||||
throw new Error(ErrorCode.IncorrectTwoFactorCode);
|
throw new Error(ErrorCode.IncorrectTwoFactorCode);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { createHash } from "crypto";
|
import { createHash } from "crypto";
|
||||||
import { totp } from "otplib";
|
|
||||||
|
|
||||||
|
import { totpRawCheck } from "@calcom/lib/totp";
|
||||||
import type { ZVerifyCodeInputSchema } from "@calcom/prisma/zod-utils";
|
import type { ZVerifyCodeInputSchema } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
@ -18,8 +18,7 @@ export const verifyCodeUnAuthenticatedHandler = async ({ input }: VerifyTokenOpt
|
||||||
.update(email + process.env.CALENDSO_ENCRYPTION_KEY)
|
.update(email + process.env.CALENDSO_ENCRYPTION_KEY)
|
||||||
.digest("hex");
|
.digest("hex");
|
||||||
|
|
||||||
totp.options = { step: 900 };
|
const isValidToken = totpRawCheck(code, secret, { step: 900 });
|
||||||
const isValidToken = totp.check(code, secret);
|
|
||||||
|
|
||||||
if (!isValidToken) throw new TRPCError({ code: "BAD_REQUEST", message: "invalid_code" });
|
if (!isValidToken) throw new TRPCError({ code: "BAD_REQUEST", message: "invalid_code" });
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { createHash } from "crypto";
|
import { createHash } from "crypto";
|
||||||
import { totp } from "otplib";
|
|
||||||
|
|
||||||
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||||
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
||||||
|
import { totpRawCheck } from "@calcom/lib/totp";
|
||||||
import type { ZVerifyCodeInputSchema } from "@calcom/prisma/zod-utils";
|
import type { ZVerifyCodeInputSchema } from "@calcom/prisma/zod-utils";
|
||||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
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)
|
.update(email + process.env.CALENDSO_ENCRYPTION_KEY)
|
||||||
.digest("hex");
|
.digest("hex");
|
||||||
|
|
||||||
totp.options = { step: 900 };
|
const isValidToken = totpRawCheck(code, secret, { step: 900 });
|
||||||
const isValidToken = totp.check(code, secret);
|
|
||||||
|
|
||||||
if (!isValidToken) throw new TRPCError({ code: "BAD_REQUEST", message: "invalid_code" });
|
if (!isValidToken) throw new TRPCError({ code: "BAD_REQUEST", message: "invalid_code" });
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user