Skip to content

Commit

Permalink
Merge pull request #102 from serenity-kit/OPA-01-005
Browse files Browse the repository at this point in the history
fix OPA-01-005: add rate limiting
  • Loading branch information
nikgraf authored Jan 29, 2024
2 parents 3abb889 + 3b18bc4 commit 6f1ad74
Show file tree
Hide file tree
Showing 22 changed files with 265 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { LockerWithServerVerificationMac } from "@/app/utils/locker";
import { isValidLocker } from "@/app/utils/locker/server/isValidLocker";
import sodium from "libsodium-wrappers";
import { NextRequest, NextResponse } from "next/server";
import database from "../db";
import { checkRateLimit } from "../rateLimiter";
import withUserSession from "../withUserSession";
import sodium from "libsodium-wrappers";

function isValidLockerPayload(
data: unknown,
Expand All @@ -20,12 +21,19 @@ function isValidLockerPayload(
);
}

export async function POST(req: NextRequest) {
export async function POST(request: NextRequest) {
if (checkRateLimit({ request })) {
return NextResponse.json(
{ error: "You have exceeded 40 requests/min" },
{ status: 429 },
);
}

const db = await database;
await sodium.ready;

return withUserSession(db, async (session) => {
const payload: unknown = await req.json();
const payload: unknown = await request.json();

if (!isValidLockerPayload(payload)) {
return NextResponse.json(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,17 @@ import { randomInt } from "crypto";
import { cookies } from "next/dist/client/components/headers";
import { NextRequest, NextResponse } from "next/server";
import database from "../../db";
import { checkRateLimit } from "../../rateLimiter";
import { LoginFinishParams } from "../../schema";

export async function POST(request: NextRequest) {
if (checkRateLimit({ request })) {
return NextResponse.json(
{ error: "You have exceeded 40 requests/min" },
{ status: 429 },
);
}

let userIdentifier, finishLoginRequest;
try {
const rawValues = await request.json();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,17 @@ import * as opaque from "@serenity-kit/opaque";
import { NextRequest, NextResponse } from "next/server";
import database from "../../db";
import { SERVER_SETUP } from "../../env";
import { checkRateLimit } from "../../rateLimiter";
import { LoginStartParams } from "../../schema";

export async function POST(request: NextRequest) {
if (checkRateLimit({ request })) {
return NextResponse.json(
{ error: "You have exceeded 40 requests/min" },
{ status: 429 },
);
}

let userIdentifier, startLoginRequest;
try {
const rawValues = await request.json();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { cookies } from "next/headers";
import { NextRequest, NextResponse } from "next/server";
import database from "../db";

export async function POST(req: NextRequest) {
export async function POST(request: NextRequest) {
const db = await database;
const sessionCookie = cookies().get("session");
if (sessionCookie) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { NextRequest } from "next/server";

const limit = 40;
const limitWindow = 1000 * 60; // 1 minute

const rateLimiter = new Map<
string,
{ requestCount: number; firstRequest: Date }
>();

type Params = {
request: NextRequest;
};

/** Simple rate-limiter based on the IP */
export const checkRateLimit = ({ request }: Params) => {
const ip = request.ip || "anonymous";
const existingEntry = rateLimiter.get(ip);
if (!existingEntry) {
rateLimiter.set(ip, {
requestCount: 1,
firstRequest: new Date(),
});
return false;
}

const { requestCount, firstRequest } = existingEntry;
const now = new Date();

// if the first request was more than 1 minute ago, reset the counter
if (now.getTime() - firstRequest.getTime() > limitWindow) {
rateLimiter.set(ip, {
requestCount: 1,
firstRequest: now,
});
return false;
}

// if the request count is more than the limit, block the request
if (requestCount > limit) {
return true;
}

// otherwise, increment the request count and allow the request
rateLimiter.set(ip, {
requestCount: requestCount + 1,
firstRequest,
});
return false;
};
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import { checkRateLimit } from "@/app/api/rateLimiter";
import { LoginFinishParams } from "@/app/api/schema";
import * as opaque from "@serenity-kit/opaque";
import { NextRequest, NextResponse } from "next/server";
import database from "../../../db";

export async function POST(request: NextRequest) {
if (checkRateLimit({ request })) {
return NextResponse.json(
{ error: "You have exceeded 40 requests/min" },
{ status: 429 },
);
}

let userIdentifier, finishLoginRequest;
try {
const rawValues = await request.json();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import { checkRateLimit } from "@/app/api/rateLimiter";
import { LoginStartParams } from "@/app/api/schema";
import * as opaque from "@serenity-kit/opaque";
import { NextRequest, NextResponse } from "next/server";
import database from "../../../db";
import { SERVER_SETUP } from "../../../env";

export async function POST(request: NextRequest) {
if (checkRateLimit({ request })) {
return NextResponse.json(
{ error: "You have exceeded 40 requests/min" },
{ status: 429 },
);
}

let userIdentifier, startLoginRequest;
try {
const rawValues = await request.json();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import { checkRateLimit } from "@/app/api/rateLimiter";
import { RecoveryRegisterFinish } from "@/app/api/schema";
import withUserSession from "@/app/api/withUserSession";
import { NextResponse } from "next/server";
import { NextRequest, NextResponse } from "next/server";
import database from "../../../db";

export async function POST(request: Request) {
export async function POST(request: NextRequest) {
if (checkRateLimit({ request })) {
return NextResponse.json(
{ error: "You have exceeded 40 requests/min" },
{ status: 429 },
);
}

const db = await database;

return withUserSession(db, async (session) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { checkRateLimit } from "@/app/api/rateLimiter";
import { RecoveryRegisterStart } from "@/app/api/schema";
import withUserSession from "@/app/api/withUserSession";
import * as opaque from "@serenity-kit/opaque";
import { NextRequest, NextResponse } from "next/server";
import database from "../../../db";
import { SERVER_SETUP } from "../../../env";

export async function POST(req: NextRequest) {
export async function POST(request: NextRequest) {
if (checkRateLimit({ request })) {
return NextResponse.json(
{ error: "You have exceeded 40 requests/min" },
{ status: 429 },
);
}

const db = await database;

return withUserSession(db, async (session) => {
Expand All @@ -19,7 +27,7 @@ export async function POST(req: NextRequest) {

let registrationRequest;
try {
const rawValues = await req.json();
const rawValues = await request.json();
const values = RecoveryRegisterStart.parse(rawValues);
registrationRequest = values.registrationRequest;
} catch (err) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { NextResponse } from "next/server";
import { NextRequest, NextResponse } from "next/server";
import database from "../db";
import { checkRateLimit } from "../rateLimiter";
import withUserSession from "../withUserSession";

export async function DELETE() {
export async function DELETE(request: NextRequest) {
if (checkRateLimit({ request })) {
return NextResponse.json(
{ error: "You have exceeded 40 requests/min" },
{ status: 429 },
);
}

const db = await database;

return withUserSession(db, async (session) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { NextResponse } from "next/server";
import { NextRequest, NextResponse } from "next/server";
import database from "../../db";
import { checkRateLimit } from "../../rateLimiter";
import { RegisterFinishParams } from "../../schema";

export async function POST(request: Request) {
export async function POST(request: NextRequest) {
if (checkRateLimit({ request })) {
return NextResponse.json(
{ error: "You have exceeded 40 requests/min" },
{ status: 429 },
);
}

let userIdentifier, registrationRecord;
try {
const rawValues = await request.json();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,20 @@ import * as opaque from "@serenity-kit/opaque";
import { NextRequest, NextResponse } from "next/server";
import database from "../../db";
import { SERVER_SETUP } from "../../env";
import { checkRateLimit } from "../../rateLimiter";
import { RegisterStartParams } from "../../schema";

export async function POST(req: NextRequest) {
export async function POST(request: NextRequest) {
if (checkRateLimit({ request })) {
return NextResponse.json(
{ error: "You have exceeded 40 requests/min" },
{ status: 429 },
);
}

let userIdentifier, registrationRequest;
try {
const rawValues = await req.json();
const rawValues = await request.json();
const values = RegisterStartParams.parse(rawValues);
userIdentifier = values.userIdentifier;
registrationRequest = values.registrationRequest;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import * as opaque from "@serenity-kit/opaque";
import { NextRequest, NextResponse } from "next/server";
import database from "../../db";
import { checkRateLimit } from "../../rateLimiter";
import { LoginFinishParams } from "../../schema";

export async function POST(request: NextRequest) {
if (checkRateLimit({ request })) {
return NextResponse.json(
{ error: "You have exceeded 40 requests/min" },
{ status: 429 },
);
}

let userIdentifier, finishLoginRequest;
try {
const rawValues = await request.json();
Expand Down
8 changes: 8 additions & 0 deletions examples/fullstack-simple-nextjs/app/api/login/start/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,17 @@ import * as opaque from "@serenity-kit/opaque";
import { NextRequest, NextResponse } from "next/server";
import database from "../../db";
import { SERVER_SETUP } from "../../env";
import { checkRateLimit } from "../../rateLimiter";
import { LoginStartParams } from "../../schema";

export async function POST(request: NextRequest) {
if (checkRateLimit({ request })) {
return NextResponse.json(
{ error: "You have exceeded 40 requests/min" },
{ status: 429 },
);
}

let userIdentifier, startLoginRequest;
try {
const rawValues = await request.json();
Expand Down
50 changes: 50 additions & 0 deletions examples/fullstack-simple-nextjs/app/api/rateLimiter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { NextRequest } from "next/server";

const limit = 40;
const limitWindow = 1000 * 60; // 1 minute

const rateLimiter = new Map<
string,
{ requestCount: number; firstRequest: Date }
>();

type Params = {
request: NextRequest;
};

/** Simple rate-limiter based on the IP */
export const checkRateLimit = ({ request }: Params) => {
const ip = request.ip || "anonymous";
const existingEntry = rateLimiter.get(ip);
if (!existingEntry) {
rateLimiter.set(ip, {
requestCount: 1,
firstRequest: new Date(),
});
return false;
}

const { requestCount, firstRequest } = existingEntry;
const now = new Date();

// if the first request was more than 1 minute ago, reset the counter
if (now.getTime() - firstRequest.getTime() > limitWindow) {
rateLimiter.set(ip, {
requestCount: 1,
firstRequest: now,
});
return false;
}

// if the request count is more than the limit, block the request
if (requestCount > limit) {
return true;
}

// otherwise, increment the request count and allow the request
rateLimiter.set(ip, {
requestCount: requestCount + 1,
firstRequest,
});
return false;
};
12 changes: 10 additions & 2 deletions examples/fullstack-simple-nextjs/app/api/register/finish/route.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { NextResponse } from "next/server";
import { NextRequest, NextResponse } from "next/server";
import database from "../../db";
import { checkRateLimit } from "../../rateLimiter";
import { RegisterFinishParams } from "../../schema";

export async function POST(request: Request) {
export async function POST(request: NextRequest) {
if (checkRateLimit({ request })) {
return NextResponse.json(
{ error: "You have exceeded 40 requests/min" },
{ status: 429 },
);
}

let userIdentifier, registrationRecord;
try {
const rawValues = await request.json();
Expand Down
Loading

0 comments on commit 6f1ad74

Please sign in to comment.