-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
docs(examples): Added Auth.js 5 example app (#432)
This example shows how to use an Arcjet rate limit with a user ID from [Auth.js authentication with Next.js](https://authjs.dev/). It's a copy of [the Next.js demo](https://github.com/nextauthjs/next-auth/tree/5ea8b7b0f4d285e48f141dd91e518c905c9fb34e/apps/examples/nextjs), but with Arcjet added.
- Loading branch information
1 parent
6ab6cdb
commit b7a1901
Showing
41 changed files
with
4,628 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -123,6 +123,50 @@ jobs: | |
working-directory: examples/nextjs-14-app-dir-validate-email | ||
run: npm run build | ||
|
||
nextjs-14-authjs-5: | ||
name: Next.js 14 + Auth.js 5 | ||
runs-on: ubuntu-latest | ||
permissions: | ||
contents: read | ||
steps: | ||
# Environment security | ||
- name: Harden Runner | ||
uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 | ||
with: | ||
disable-sudo: true | ||
egress-policy: block | ||
allowed-endpoints: > | ||
fonts.googleapis.com:443 | ||
fonts.gstatic.com:443 | ||
github.com:443 | ||
registry.npmjs.org:443 | ||
# Checkout | ||
# Most toolchains require checkout first | ||
- name: Checkout | ||
uses: actions/checkout@v4 | ||
|
||
# Language toolchains | ||
- name: Install Node | ||
uses: actions/[email protected] | ||
with: | ||
node-version: 20 | ||
|
||
# Workflow | ||
|
||
- name: Install dependencies | ||
run: npm ci | ||
|
||
- name: Install example dependencies | ||
working-directory: examples/nextjs-14-authjs-5 | ||
run: npm ci | ||
|
||
- name: Build | ||
working-directory: examples/nextjs-14-authjs-5 | ||
env: | ||
AUTH_SECRET: TEST_SECRET | ||
run: npm run build | ||
|
||
nextjs-14-clerk-rl: | ||
name: Next.js 14 + Clerk + Rate Limit | ||
runs-on: ubuntu-latest | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
# Get your Arcjet key from https://app.arcjet.com | ||
ARCJET_KEY= | ||
# Set a secret for NextAuth.js | ||
# Linux: `openssl rand -hex 32` or go to https://generate-secret.vercel.app/32 | ||
AUTH_SECRET= | ||
# Set your GitHub credentials by creating a new OAuth App at | ||
# https://github.com/settings/developers See also: | ||
# https://authjs.dev/reference/core/providers/github | ||
GITHUB_ID= | ||
GITHUB_SECRET= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
.DS_Store | ||
|
||
node_modules/ | ||
logs | ||
*.log | ||
npm-debug.log* | ||
yarn-debug.log* | ||
yarn-error.log* | ||
lerna-debug.log* | ||
.yarn-integrity | ||
.npm | ||
|
||
.eslintcache | ||
|
||
*.tsbuildinfo | ||
next-env.d.ts | ||
|
||
.next | ||
.vercel | ||
.env*.local |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
<a href="https://arcjet.com" target="_arcjet-home"> | ||
<picture> | ||
<source media="(prefers-color-scheme: dark)" srcset="https://arcjet.com/arcjet-logo-minimal-dark-mark-all.svg"> | ||
<img src="https://arcjet.com/arcjet-logo-minimal-light-mark-all.svg" alt="Arcjet Logo" height="128" width="auto"> | ||
</picture> | ||
</a> | ||
|
||
# Arcjet Rate Limit / Auth.js 5 Authentication Example | ||
|
||
This example shows how to use an Arcjet rate limit with a user ID from [Auth.js | ||
authentication with Next.js](https://authjs.dev/). It's a copy of [the Next.js | ||
demo](https://github.com/nextauthjs/next-auth/tree/5ea8b7b0f4d285e48f141dd91e518c905c9fb34e/apps/examples/nextjs), | ||
but with Arcjet added. | ||
|
||
**Note:** Auth.js 5 is still in development and was renamed from NextAuth. The | ||
stable version is NextAuth 4. See [the Arcjet | ||
docs](https://docs.arcjet.com/integrations/nextauth) and separate example app if | ||
you're using that version. | ||
|
||
## Protection | ||
|
||
* The main Auth.js route handler at `app/auth/[...nextauth]/route.ts` has `POST` | ||
requests protected with a rate limit and bot protection. This helps protect | ||
the login and signup actions against brute force attacks and other abuse. | ||
* The `/app/api/protected/route.ts` route handler applies a rate limit based on | ||
the authenticated user's ID. | ||
* Middleware in `middleware.ts` runs on requests to `/middleware-example` and | ||
checks the user's session, applying a rate limit based on the user's ID if | ||
they are authenticated. | ||
|
||
## How to use | ||
|
||
1. From the root of the project, install the SDK dependencies. | ||
|
||
```bash | ||
npm ci | ||
``` | ||
|
||
2. Enter this directory and install the example's dependencies. | ||
|
||
```bash | ||
cd examples/nextjs-14-authjs-5 | ||
npm ci | ||
``` | ||
|
||
3. Rename `.env.local.example` to `.env.local` and fill in the required | ||
environment variables. You will need to [create a GitHub OAuth | ||
app](https://github.com/settings/applications) for testing. The callback URL | ||
setting for your OAuth app is usually `http://localhost:3000`. | ||
|
||
4. Start the dev server. | ||
|
||
```bash | ||
npm run dev | ||
``` | ||
|
||
5. Visit `http://localhost:3000`. | ||
6. Try the different routes linked on the page. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
"use client" | ||
import CustomLink from "@/components/custom-link" | ||
import { useEffect, useState } from "react" | ||
|
||
export default function Page() { | ||
const [data, setData] = useState() | ||
useEffect(() => { | ||
; (async () => { | ||
const res = await fetch("/api/protected") | ||
const json = await res.json() | ||
setData(json) | ||
})() | ||
}, []) | ||
return ( | ||
<div className="flex flex-col gap-6"> | ||
<h1 className="text-3xl font-bold">Route Handler Usage</h1> | ||
<p> | ||
This page fetches data from an API{" "} | ||
<CustomLink href="https://nextjs.org/docs/app/building-your-application/routing/route-handlers"> | ||
Route Handler | ||
</CustomLink> | ||
. The API is protected using the universal{" "} | ||
<CustomLink href="https://nextjs.authjs.dev#auth"> | ||
<code>auth()</code> | ||
</CustomLink>{" "} | ||
method. | ||
</p> | ||
<div className="flex flex-col rounded-md bg-neutral-100"> | ||
<div className="p-4 font-bold rounded-t-md bg-neutral-200"> | ||
Data from API Route | ||
</div> | ||
<pre className="py-6 px-4 whitespace-pre-wrap break-all"> | ||
{JSON.stringify(data, null, 2)} | ||
</pre> | ||
</div> | ||
</div> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import arcjet, { tokenBucket } from "@arcjet/next"; | ||
import { auth } from "auth"; | ||
|
||
// The arcjet instance is created outside of the handler | ||
const aj = arcjet({ | ||
key: process.env.ARCJET_KEY!, // Get your site key from https://app.arcjet.com | ||
rules: [ | ||
// Create a token bucket rate limit. Other algorithms are supported. | ||
tokenBucket({ | ||
mode: "LIVE", // will block requests. Use "DRY_RUN" to log only | ||
characteristics: ["userId"], // Rate limit based on the Clerk userId | ||
refillRate: 5, // refill 5 tokens per interval | ||
interval: 10, // refill every 10 seconds | ||
capacity: 10, // bucket maximum capacity of 10 tokens | ||
}), | ||
], | ||
}); | ||
|
||
export const GET = auth(async (req) => { | ||
if (req.auth) { | ||
console.log("User:", req.auth.user); | ||
|
||
// If there is a user ID then use it, otherwise use the email | ||
let userId: string; | ||
if (req.auth.user?.id) { | ||
userId = req.auth.user.id; | ||
} else if (req.auth.user?.email) { | ||
// A very simple hash to avoid sending PII to Arcjet. You may wish to add a | ||
// unique salt prefix to protect against reverse lookups. | ||
const email = req.auth.user!.email; | ||
const emailHash = require("crypto") | ||
.createHash("sha256") | ||
.update(email) | ||
.digest("hex"); | ||
|
||
userId = emailHash; | ||
} else { | ||
return Response.json({ message: "Unauthorized" }, { status: 401 }); | ||
} | ||
|
||
// Deduct 5 tokens from the token bucket | ||
const decision = await aj.protect(req, { userId, requested: 5 }); | ||
console.log("Arcjet Decision:", decision); | ||
|
||
if (decision.isDenied()) { | ||
return Response.json( | ||
{ | ||
error: "Too Many Requests", | ||
reason: decision.reason, | ||
}, | ||
{ | ||
status: 429, | ||
} | ||
); | ||
} | ||
|
||
return Response.json({ data: "Protected data" }); | ||
} | ||
|
||
return Response.json({ message: "Not authenticated" }, { status: 401 }); | ||
}) as any; // TODO: Fix `auth()` return type |
43 changes: 43 additions & 0 deletions
43
examples/nextjs-14-authjs-5/app/auth/[...nextauth]/route.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import arcjet, { detectBot, slidingWindow } from "@arcjet/next"; | ||
import { handlers } from "auth"; | ||
import { NextRequest, NextResponse } from "next/server"; | ||
|
||
const aj = arcjet({ | ||
key: process.env.ARCJET_KEY, | ||
rules: [ | ||
slidingWindow({ | ||
mode: "LIVE", // will block requests. Use "DRY_RUN" to log only | ||
interval: 60, // tracks requests across a 60 second sliding window | ||
max: 10, // allow a maximum of 10 requests | ||
}), | ||
detectBot({ | ||
mode: "LIVE", // will block requests. Use "DRY_RUN" to log only | ||
block: ["AUTOMATED"], // blocks all automated clients | ||
}), | ||
], | ||
}); | ||
|
||
// Protect the sensitive actions e.g. login, signup, etc with Arcjet | ||
const ajProtectedPOST = async (req: NextRequest) => { | ||
const decision = await aj.protect(req); | ||
console.log("Arcjet decision", decision); | ||
|
||
if (decision.isDenied()) { | ||
if (decision.reason.isRateLimit()) { | ||
return NextResponse.json({ error: "Too Many Requests" }, { status: 429 }); | ||
} else { | ||
return NextResponse.json({ error: "Forbidden" }, { status: 403 }); | ||
} | ||
} | ||
|
||
|
||
return handlers.POST(req); | ||
}; | ||
|
||
// You could also protect the GET handler, but these tend to be less sensitive | ||
// so it's not always necessary | ||
const GET = async (req: NextRequest) => { | ||
return handlers.GET(req); | ||
} | ||
|
||
export { GET, ajProtectedPOST as POST }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { auth } from "auth" | ||
import ClientExample from "@/components/client-example" | ||
import { SessionProvider } from "next-auth/react" | ||
|
||
export default async function ClientPage() { | ||
const session = await auth() | ||
if (session?.user) { | ||
// TODO: Look into https://react.dev/reference/react/experimental_taintObjectReference | ||
// filter out sensitive data before passing to client. | ||
session.user = { | ||
name: session.user.name, | ||
email: session.user.email, | ||
image: session.user.image, | ||
} | ||
} | ||
|
||
return ( | ||
<SessionProvider basePath={"/auth"} session={session}> | ||
<ClientExample /> | ||
</SessionProvider> | ||
) | ||
} |
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
@tailwind base; | ||
@tailwind components; | ||
@tailwind utilities; | ||
|
||
@layer base { | ||
:root { | ||
--background: 0 0% 100%; | ||
--foreground: 222.2 84% 4.9%; | ||
|
||
--card: 0 0% 100%; | ||
--card-foreground: 222.2 84% 4.9%; | ||
|
||
--popover: 0 0% 100%; | ||
--popover-foreground: 222.2 84% 4.9%; | ||
|
||
--primary: 222.2 47.4% 11.2%; | ||
--primary-foreground: 210 40% 98%; | ||
|
||
--secondary: 210 40% 96.1%; | ||
--secondary-foreground: 222.2 47.4% 11.2%; | ||
|
||
--muted: 210 40% 96.1%; | ||
--muted-foreground: 215.4 16.3% 46.9%; | ||
|
||
--accent: 210 40% 96.1%; | ||
--accent-foreground: 222.2 47.4% 11.2%; | ||
|
||
--destructive: 0 84.2% 60.2%; | ||
--destructive-foreground: 210 40% 98%; | ||
|
||
--border: 214.3 31.8% 91.4%; | ||
--input: 214.3 31.8% 91.4%; | ||
--ring: 222.2 84% 4.9%; | ||
|
||
--radius: 0.5rem; | ||
} | ||
} | ||
|
||
@layer base { | ||
* { | ||
@apply border-border; | ||
} | ||
|
||
body { | ||
@apply bg-background text-foreground; | ||
} | ||
} |
Oops, something went wrong.