Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: RedwoodJS Middleware to rate limit requests #1606

Draft
wants to merge 33 commits into
base: redwood
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
e7cb060
WIP: basic functionality
dthyresson May 4, 2024
42e3169
Setup vitest
dthyresson May 4, 2024
b3d68f9
Delete apps/web/next-env.d.ts
dthyresson May 4, 2024
da515f5
Fix configs and tsconfig to properly import rw middleware
dthyresson May 6, 2024
1b6e655
get to green
dthyresson May 6, 2024
6e35a37
added some ratelimit mocking
dthyresson May 6, 2024
e71d942
Fixes custom error logic
dthyresson May 6, 2024
07d96a3
Support redwoodjs logger
dthyresson May 6, 2024
495e268
Added docs
dthyresson May 7, 2024
580db94
can rely on baked in middleware pattern route matching
dthyresson May 7, 2024
3be7995
ran fmt
dthyresson May 7, 2024
88af99c
remove path to exp
dthyresson May 7, 2024
59aa8b5
Adds redwoodjs library docs
dthyresson May 8, 2024
a1276ba
update readme
dthyresson May 8, 2024
0f5448a
rework so in future one middleware can be used for both ratelimiting …
dthyresson May 10, 2024
58a25cf
fix jsdoc example code
dthyresson May 10, 2024
b7a7a96
rename custom functions for simplicity
dthyresson May 10, 2024
5e49141
rename to withUnkeyConfig
dthyresson May 10, 2024
15f4040
WIP key auth middleware
dthyresson May 10, 2024
5e6d696
WIP auth key middleware
dthyresson May 10, 2024
4b9cf39
Refactor and rename
dthyresson May 11, 2024
2a0afd5
move logger and rename apikey using create
dthyresson May 11, 2024
2b5b316
add readme for key middleware
dthyresson May 11, 2024
ad04dd7
Reorganize file directory structure
dthyresson May 11, 2024
884db75
move tests
dthyresson May 11, 2024
d70379c
rename test blocks
dthyresson May 11, 2024
7ca2759
stub some tests
dthyresson May 11, 2024
4781fe4
commented
dthyresson May 11, 2024
3e23eaf
start testing createApiKeyMiddleware
dthyresson May 12, 2024
670ce90
add apikey middleware tests
dthyresson May 13, 2024
01d2e6f
adds docs for verify keys
dthyresson May 14, 2024
0689f0c
Upgrade redwood canary
dthyresson May 14, 2024
550c271
make both docs table of contents same
dthyresson May 14, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions apps/docs/libraries/ts/redwoodjs/keys.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
---
title: "API Key Verification"
description: "Implement API Key Verification with RedwoodJS Middleware and Unkey"
---

## Usage

`createApiKeyMiddleware` is middleware function for RedwoodJS used to validate API keys sent in the request headers with [Unkey](https://www.unkey.com/docs).

By default, Unkey verifies a key provided in the request's authorization header: `"Authorization: Bearer unkey_xxx"`. How Unkey extracts the key to verify can be customized using the `getKey` function option.

Here's a basic example of how to use `createApiKeyMiddleware`:

```ts file="web/src/entry.server.tsx"
import createApiKeyMiddleware from "@unkey/redwoodjs";
import type { ApiKeyMiddlewareConfig } from "@unkey/redwoodjs";

export const registerMiddleware = () => {
const config: ApiKeyMiddlewareConfig = {
apiId: "my-app-id",
};

const middleware = createApiKeyMiddleware(config);

return [middleware];
};
```

In this example, `createApiKeyMiddleware` is used as a global middleware. It will validate the API key for all incoming requests.

## Setup

To get started with standalone API Key Verification using Unkey, see the [Public API Protection Onboarding Guide](https://www.unkey.com/docs/onboarding/onboarding-api).

## Examples

`createApiKeyMiddleware` can be configured by providing `ApiKeyMiddlewareConfig` configuration options when creating the middleware.

### Custom getKey

In this example, `createApiKeyMiddleware` will look for the API key in the 'X-API-KEY' header and validate it via a custom `getKey` function.

```ts file="web/src/entry.server.tsx"
import createApiKeyMiddleware from "@unkey/redwoodjs";
import type { ApiKeyMiddlewareConfig } from "@unkey/redwoodjs";

export const registerMiddleware = () => {
const config: ApiKeyMiddlewareConfig = {
apiId: 'my-app-id'
getKey: (req) => {
return req.headers.get("x-api-key") ?? "";
},
};

const middleware = createApiKeyMiddleware(config);

return [middleware];
};
```

### Custom onInvalidKey

In this example, `createApiKeyMiddleware` respond with a custom message and status if the key is invalid.

```ts file="web/src/entry.server.tsx"
import createApiKeyMiddleware from "@unkey/redwoodjs";
import type { ApiKeyMiddlewareConfig } from "@unkey/redwoodjs";

export const registerMiddleware = () => {
const config: ApiKeyMiddlewareConfig = {
apiId: 'my-app-id'
onInvalidKey: (_req, _result) => {
return new MiddlewareResponse("Custom forbidden", { status: 403 });
},
};

const middleware = createApiKeyMiddleware(config);

return [middleware];
};
```

### Error Handling

If the API key is missing or invalid, `createApiKeyMiddleware` will send a 401 Unauthorized response and stop the request from being processed further.

You can customize this behavior by providing your own error handling function in the options object:

```ts file="web/src/entry.server.tsx"
import createApiKeyMiddleware from "@unkey/redwoodjs";
import type { ApiKeyMiddlewareConfig } from "@unkey/redwoodjs";

export const registerMiddleware = () => {
const config: ApiKeyMiddlewareConfig = {
apiId: "my-app-id",
onError: (_req, _err) =>
new MiddlewareResponse("Custom unavailable", { status: 503 }),
};

const middleware = createApiKeyMiddleware(config);

return [middleware];
};
```
26 changes: 26 additions & 0 deletions apps/docs/libraries/ts/redwoodjs/overview.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
title: "Overview"
description: "RedwoodJS SDK for Unkey"
---

The official RedwoodJS SDK for Unkey. Use this to verify and rate limit request using Unkey.

To use, a RedwoodJS app must be setup with Server-Side Rendering (SSR) and middleware enabled.

In the future, support for rate limiting GraphQL operations and other RedwoodJS functions will be added.

## API Key Verification

RedwoodJS supports Unkey API key verification with its`createApiKeyMiddleware` middleware.

## Rate Limiting

RedwoodJS supports Unkey Rate limiting verification with its `createRatelimitMiddleware` middleware.

## Install

<Tabs>
<Tab title="yarn">
``` bash yarn rw workspace web add @unkey/redwoodjs ```
</Tab>
</Tabs>
184 changes: 184 additions & 0 deletions apps/docs/libraries/ts/redwoodjs/ratelimiting.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
---
title: "Rate Limiting"
description: "Implement rate limiting with RedwoodJS Middleware and Unkey"
---

# Usage

Implement request rate limiting in your RedwoodJS application using the `createRatelimitMiddleware` from Unkey.

<Note>
This middleware is only available when Server-Side Rendering (SSR) is enabled
in RedwoodJS.
</Note>

To learn more about this middleware, visit the [RedwoodJS Middleware documentation](https://www.unkey.com/docs/libraries/ts/redwoodjs/middleware/ratelimiting).

## Setup

To get started with standalone rate limiting using Unkey, see the [Rate Limiting Onboarding Guide](https://www.unkey.com/docs/onboarding/onboarding-ratelimiting).

**Important**: Ensure that your `UNKEY_ROOT_KEY` is set in your `.env` file for rate limiting.

## Examples

The `createRatelimitMiddleware` function allows for extensive customization to suit various use cases. Below are examples demonstrating how to:

- Implement pattern matches to enforce rate limits on specific paths.
- Utilize a custom identifier generator function.
- Customize responses when rate limits are exceeded.
- Provide custom error responses.

### Rate Limit All Requests

To apply a rate limit to all requests, register the middleware globally without route matching:

```ts file="web/src/entry.server.tsx"
import createRatelimitMiddleware from "@unkey/redwoodjs";
import type { RatelimitMiddlewareConfig } from "@unkey/redwoodjs";

export const registerMiddleware = () => {
const config: RatelimitMiddlewareConfig = {
rootKey: process.env.UNKEY_ROOT_KEY,
namespace: "my-app",
limit: 1,
duration: "30s",
async: true,
};

const middleware = createRatelimitMiddleware(config);

return [middleware];
};
```

### Basic Route Matching

To rate limit requests on specific routes, register the middleware with a pattern match:

```ts file="web/src/entry.server.tsx"
import createRatelimitMiddleware from "@unkey/redwoodjs";
import type { RatelimitMiddlewareConfig } from "@unkey/redwoodjs";

export const registerMiddleware = () => {
const config: RatelimitMiddlewareConfig = {
config: {
rootKey: process.env.UNKEY_ROOT_KEY,
namespace: "my-app",
limit: 1,
duration: "30s",
async: true,
},
};

const middleware = createRatelimitMiddleware(config);

return [[middleware, "/blog-post/:slug(\\d{1,})"]];
};
```

To handle multiple patterns, either compose a complex expression

```ts
return [[middleware, "/rss.(xml|atom|json)"]];
```

or register multiple patterns:

```ts
return [
[middleware, "/blog-post/:slug(\\d{1,})"],
[middleware, "/admin"],
];
```

### With Custom Identifier Function and Third Party Authentication

Customize the identifier function to utilize user authentication status, such as with Supabase.

Here, we use a custom identifier function `supabaseRatelimitIdentifier` that:

- checks is the request is authenticated
- constructs the identifier `sub` from the current user, since here the currentUser will be a JWT where the user id is the `sub` claim
- registers `supabaseAuthMiddleware` before `middleware` so the request can be authenticated before determining limits

```ts file="web/src/entry.server.ts"
import createSupabaseAuthMiddleware from "@redwoodjs/auth-supabase-middleware";
import createRatelimitMiddleware from "@unkey/redwoodjs";
import type { RatelimitMiddlewareConfig } from "@unkey/redwoodjs";
import type { MiddlewareRequest } from "@redwoodjs/vite/middleware";
import type { TagDescriptor } from "@redwoodjs/web";

import App from "./App";
import { Document } from "./Document";

// eslint-disable-next-line no-restricted-imports
import { getCurrentUser } from "$api/src/lib/auth";

interface Props {
css: string[];
meta?: TagDescriptor[];
}

export const supabaseRatelimitIdentifier = (req: MiddlewareRequest) => {
const authContext = req?.serverAuthContext?.get();
console.log(">>>> in supabaseRatelimitIdentifier", authContext);
const identifier = authContext?.isAuthenticated
? (authContext.currentUser?.sub as string) || "anonymous-user"
: "192.168.1.1";
return identifier;
};

export const registerMiddleware = () => {
const config: RatelimitMiddlewareConfig = {
config: {
rootKey: process.env.UNKEY_ROOT_KEY,
namespace: "my-app",
limit: 1,
duration: "30s",
async: true,
},
getIdentifier: supabaseRatelimitIdentifier,
};
const middleware = createRatelimitMiddleware(config);
const supabaseAuthMiddleware = createSupabaseAuthMiddleware({
getCurrentUser,
});

return [supabaseAuthMiddleware, [middleware, "/blog-post/:slug(\\d{1,})"]];
};
```

### Custom Rate Limit Exceeded and Error Responses

Define custom responses for exceeded limits and errors:

```ts
export const registerMiddleware = () => {
const config: RatelimitMiddlewareConfig = {
config: {
rootKey: process.env.UNKEY_ROOT_KEY,
namespace: "my-app",
limit: 1,
duration: "30s",
async: true,
},
onExceeded: (_req: MiddlewareRequest) => {
return new MiddlewareResponse("Custom Rate limit exceeded message", {
status: 429,
});
},
onError: (_req: MiddlewareRequest) => {
return new MiddlewareResponse(
"Custom Error message when rate limiting fails",
{
status: 500,
}
);
},
};
const middleware = createRatelimitMiddleware(config);

return [middleware];
};
```
Loading