Skip to content

Commit

Permalink
docs(examples): Added Auth.js 5 example app (#432)
Browse files Browse the repository at this point in the history
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
davidmytton authored Apr 2, 2024
1 parent 6ab6cdb commit b7a1901
Show file tree
Hide file tree
Showing 41 changed files with 4,628 additions and 0 deletions.
22 changes: 22 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,28 @@ updates:
- dependency-name: "@types/node"
versions: [">18.18"]

- package-ecosystem: npm
directory: /examples/nextjs-14-authjs-5
schedule:
# Our dependencies should be checked daily
interval: daily
assignees:
- blaine-arcjet
reviewers:
- blaine-arcjet
commit-message:
prefix: deps(example)
prefix-development: deps(example)
groups:
dependencies:
patterns:
- "*"
ignore:
# Ignore updates to the @types/node package due to conflict between
# Headers in DOM.
- dependency-name: "@types/node"
versions: [">18.18"]

- package-ecosystem: npm
directory: /examples/nextjs-14-clerk-rl
schedule:
Expand Down
44 changes: 44 additions & 0 deletions .github/workflows/reusable-examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions examples/nextjs-14-authjs-5/.env.local.example
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=
20 changes: 20 additions & 0 deletions examples/nextjs-14-authjs-5/.gitignore
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
58 changes: 58 additions & 0 deletions examples/nextjs-14-authjs-5/README.md
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.
38 changes: 38 additions & 0 deletions examples/nextjs-14-authjs-5/app/api-example/page.tsx
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>
)
}
61 changes: 61 additions & 0 deletions examples/nextjs-14-authjs-5/app/api/protected/route.ts
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 examples/nextjs-14-authjs-5/app/auth/[...nextauth]/route.ts
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 };
22 changes: 22 additions & 0 deletions examples/nextjs-14-authjs-5/app/client-example/page.tsx
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 added examples/nextjs-14-authjs-5/app/favicon.ico
Binary file not shown.
47 changes: 47 additions & 0 deletions examples/nextjs-14-authjs-5/app/globals.css
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;
}
}
Loading

0 comments on commit b7a1901

Please sign in to comment.