fix: rate limit auth (#3820)
* fix: rate limit auth * fix: replace lru-cache w memory-cache * remove comments * fix: yarn.lock * fix: remove changes yarn lock * fix: add missing EOL empty liune * fix: move rate limiter so it kicks the last, limit to 10 tries per minute * fix: move limiter w rest of code * test: trying fix onboardong * fix: undo changes in globalSetup.ts * test: fix disable login for onboarding * fix: use username instead of email for token check * fix: tests * fix: don't run on test * fix: add missing comma * fix: remove uniqueTokenPerInterval * fix: add errorcode to packages lib auth * Update packages/lib/rateLimit.ts fix: improve readability Co-authored-by: Omar López <zomars@me.com> * Update packages/lib/rateLimit.ts fix: no unnecessary any Co-authored-by: Omar López <zomars@me.com> * Update packages/lib/rateLimit.ts fix: improve readability Co-authored-by: Omar López <zomars@me.com> * fix: rename interval -> intervalInMs * fix: check user.email not username which could be empty * fix: rateLimit update all naming Co-authored-by: Agusti Fernandez Pardo <git@agusti.me> Co-authored-by: Omar López <zomars@me.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com>
This commit is contained in:
parent
f5c1c76f0a
commit
b8b1b9a6d0
|
@ -32,6 +32,7 @@ export enum ErrorCode {
|
|||
InternalServerError = "internal-server-error",
|
||||
NewPasswordMatchesOld = "new-password-matches-old",
|
||||
ThirdPartyIdentityProviderEnabled = "third-party-identity-provider-enabled",
|
||||
RateLimitExceeded = "rate-limit-exceeded",
|
||||
InvalidPassword = "invalid-password",
|
||||
}
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/Imperso
|
|||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||
import { defaultCookies } from "@calcom/lib/default-cookies";
|
||||
import rateLimit from "@calcom/lib/rateLimit";
|
||||
import { serverConfig } from "@calcom/lib/serverConfig";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
|
@ -100,6 +101,11 @@ const providers: Provider[] = [
|
|||
}
|
||||
}
|
||||
|
||||
const limiter = rateLimit({
|
||||
intervalInMs: 60 * 1000, // 1 minute
|
||||
});
|
||||
await limiter.check(10, user.email); // 10 requests per minute
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
|
|
|
@ -53,7 +53,8 @@ export default function Login({
|
|||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const errorMessages: { [key: string]: string } = {
|
||||
// [ErrorCode.SecondFactorRequired]: t("2fa_enabled_instructions"),
|
||||
[ErrorCode.RateLimitExceeded]: t("rate_limit_exceeded"),
|
||||
[ErrorCode.SecondFactorRequired]: t("2fa_enabled_instructions"),
|
||||
[ErrorCode.IncorrectPassword]: `${t("incorrect_password")} ${t("please_try_again")}`,
|
||||
[ErrorCode.UserNotFound]: t("no_account_exists"),
|
||||
[ErrorCode.IncorrectTwoFactorCode]: `${t("incorrect_2fa_code")} ${t("please_try_again")}`,
|
||||
|
|
|
@ -1044,6 +1044,7 @@
|
|||
"using_additional_inputs_as_variables": "How to use additional inputs as variables?",
|
||||
"download_desktop_app": "Download desktop app",
|
||||
"set_ping_link": "Set Ping link",
|
||||
"rate_limit_exceeded": "Rate limit exceeded",
|
||||
"when_something_happens": "When something happens",
|
||||
"action_is_performed": "An action is performed",
|
||||
"test_action": "Test action",
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { IdentityProvider } from "@prisma/client";
|
||||
import { compare, hash } from "bcryptjs";
|
||||
import type { NextApiRequest } from "next";
|
||||
import type { Session } from "next-auth";
|
||||
|
@ -60,3 +61,24 @@ export const ensureSession = async (ctxOrReq: CtxOrReq) => {
|
|||
if (!session?.user.id) throw new HttpError({ statusCode: 401, message: "Unauthorized" });
|
||||
return session;
|
||||
};
|
||||
|
||||
export enum ErrorCode {
|
||||
UserNotFound = "user-not-found",
|
||||
IncorrectPassword = "incorrect-password",
|
||||
UserMissingPassword = "missing-password",
|
||||
TwoFactorDisabled = "two-factor-disabled",
|
||||
TwoFactorAlreadyEnabled = "two-factor-already-enabled",
|
||||
TwoFactorSetupRequired = "two-factor-setup-required",
|
||||
SecondFactorRequired = "second-factor-required",
|
||||
IncorrectTwoFactorCode = "incorrect-two-factor-code",
|
||||
InternalServerError = "internal-server-error",
|
||||
NewPasswordMatchesOld = "new-password-matches-old",
|
||||
ThirdPartyIdentityProviderEnabled = "third-party-identity-provider-enabled",
|
||||
RateLimitExceeded = "rate-limit-exceeded",
|
||||
}
|
||||
|
||||
export const identityProviderNameMap: { [key in IdentityProvider]: string } = {
|
||||
[IdentityProvider.CAL]: "Cal",
|
||||
[IdentityProvider.GOOGLE]: "Google",
|
||||
[IdentityProvider.SAML]: "SAML",
|
||||
};
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import cache from "memory-cache";
|
||||
|
||||
import { ErrorCode } from "./auth";
|
||||
|
||||
const rateLimit = (options: { intervalInMs: number }) => {
|
||||
return {
|
||||
check: (requestLimit: number, uniqueIdentifier: string) => {
|
||||
const count = cache.get(uniqueIdentifier) || [0];
|
||||
if (count[0] === 0) {
|
||||
cache.put(uniqueIdentifier, count, options.intervalInMs);
|
||||
}
|
||||
count[0] += 1;
|
||||
|
||||
const currentUsage = count[0];
|
||||
const isRateLimited = currentUsage >= requestLimit;
|
||||
|
||||
if (isRateLimited) {
|
||||
throw new Error(ErrorCode.RateLimitExceeded);
|
||||
}
|
||||
|
||||
return { isRateLimited, requestLimit, remaining: isRateLimited ? 0 : requestLimit - currentUsage };
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default rateLimit;
|
Loading…
Reference in New Issue
Block a user