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

docs(examples): Added Auth.js 5 example app #432

Merged
merged 18 commits into from
Apr 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
Loading