fix: use userId as ratelimit identifier (#18803)

* fix: use userId as ratelimit identifier

Using raw api keys as identifiers is problematic because they are logged
and are accessible via Unkey's UI.

My first idea was to use the api key's primary key, which would solve
this just fine. But after looking at the implementation it seemed easier
to use the user's id, which is what most people are interested in
limiting, not the actual keys. Also the userId was already passed in the
requests context, which made it easier. Sean confirmed that ratelimiting
the user, not their keys made more sense, so here we are.

* Update rateLimitApiKey.ts

* Update rateLimitApiKey.ts

* Fix tests

* Fix tests

---------

Co-authored-by: Benny Joo <sldisek783@gmail.com>
Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
This commit is contained in:
Andreas Thomas
2025-01-22 21:37:30 -05:00
committed by GitHub
co-authored by Benny Joo Joe Au-Yeung
parent 3765c76c96
commit 1ffea493ec
2 changed files with 22 additions and 10 deletions
@@ -13,6 +13,8 @@ import { rateLimitApiKey } from "~/lib/helpers/rateLimitApiKey";
type CustomNextApiRequest = NextApiRequest & Request;
type CustomNextApiResponse = NextApiResponse & Response;
const testUserId = 123;
vi.mock("@calcom/lib/checkRateLimitAndThrowError", () => ({
checkRateLimitAndThrowError: vi.fn(),
}));
@@ -26,6 +28,7 @@ describe("rateLimitApiKey middleware", () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
query: {},
userId: testUserId,
});
await rateLimitApiKey(req, res, vi.fn() as any);
@@ -38,6 +41,7 @@ describe("rateLimitApiKey middleware", () => {
const { req, res } = createMocks({
method: "GET",
query: { apiKey: "test-key" },
userId: testUserId,
});
(checkRateLimitAndThrowError as any).mockResolvedValueOnce({
@@ -50,7 +54,7 @@ describe("rateLimitApiKey middleware", () => {
await rateLimitApiKey(req, res, vi.fn() as any);
expect(checkRateLimitAndThrowError).toHaveBeenCalledWith({
identifier: "test-key",
identifier: testUserId.toString(),
rateLimitingType: "api",
onRateLimiterResponse: expect.any(Function),
});
@@ -60,6 +64,7 @@ describe("rateLimitApiKey middleware", () => {
const { req, res } = createMocks({
method: "GET",
query: { apiKey: "test-key" },
userId: testUserId,
});
const rateLimiterResponse: RatelimitResponse = {
@@ -89,6 +94,7 @@ describe("rateLimitApiKey middleware", () => {
const { req, res } = createMocks({
method: "GET",
query: { apiKey: "test-key" },
userId: testUserId,
});
(checkRateLimitAndThrowError as any).mockRejectedValue(new Error("Rate limit exceeded"));
@@ -104,6 +110,7 @@ describe("rateLimitApiKey middleware", () => {
const { req, res } = createMocks({
method: "GET",
query: { apiKey: "test-key" },
userId: testUserId,
});
const rateLimiterResponse: RatelimitResponse = {
@@ -127,8 +134,8 @@ describe("rateLimitApiKey middleware", () => {
await rateLimitApiKey(req, res, vi.fn() as any);
expect(handleAutoLock).toHaveBeenCalledWith({
identifier: "test-key",
identifierType: "apiKey",
identifier: testUserId.toString(),
identifierType: "userId",
rateLimitResponse: rateLimiterResponse,
});
@@ -140,6 +147,7 @@ describe("rateLimitApiKey middleware", () => {
const { req, res } = createMocks({
method: "GET",
query: { apiKey: "test-key" },
userId: testUserId,
});
const rateLimiterResponse: RatelimitResponse = {
@@ -163,8 +171,8 @@ describe("rateLimitApiKey middleware", () => {
await rateLimitApiKey(req, res, vi.fn() as any);
expect(handleAutoLock).toHaveBeenCalledWith({
identifier: "test-key",
identifierType: "apiKey",
identifier: testUserId.toString(),
identifierType: "userId",
rateLimitResponse: rateLimiterResponse,
});
@@ -176,6 +184,7 @@ describe("rateLimitApiKey middleware", () => {
const { req, res } = createMocks({
method: "GET",
query: { apiKey: "test-key" },
userId: testUserId,
});
const rateLimiterResponse: RatelimitResponse = {
@@ -200,8 +209,8 @@ describe("rateLimitApiKey middleware", () => {
await rateLimitApiKey(req, res, next);
expect(handleAutoLock).toHaveBeenCalledWith({
identifier: "test-key",
identifierType: "apiKey",
identifier: testUserId.toString(),
identifierType: "userId",
rateLimitResponse: rateLimiterResponse,
});
@@ -214,6 +223,7 @@ describe("rateLimitApiKey middleware", () => {
const { req, res } = createMocks({
method: "GET",
query: { apiKey: "test-key" },
userId: testUserId,
});
// Mock checkRateLimitAndThrowError to throw HttpError
+5 -3
View File
@@ -5,12 +5,14 @@ import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowE
import { HttpError } from "@calcom/lib/http-error";
export const rateLimitApiKey: NextMiddleware = async (req, res, next) => {
if (!req.userId) return res.status(401).json({ message: "No userId provided" });
if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" });
// TODO: Add a way to add trusted api keys
try {
const identifier = req.userId.toString();
await checkRateLimitAndThrowError({
identifier: req.query.apiKey as string,
identifier,
rateLimitingType: "api",
onRateLimiterResponse: async (response) => {
res.setHeader("X-RateLimit-Limit", response.limit);
@@ -19,8 +21,8 @@ export const rateLimitApiKey: NextMiddleware = async (req, res, next) => {
try {
const didLock = await handleAutoLock({
identifier: req.query.apiKey as string, // Casting as this is verified in another middleware
identifierType: "apiKey",
identifier,
identifierType: "userId",
rateLimitResponse: response,
});