npm package: @get401/next
Description: get401 authentication for Next.js — middleware, route handler wrappers, and server-side session helpers.
Latest version: 0.1.1

# @get401/next

[get401](https://get401.com) authentication for Next.js. Supports the **App Router** (route handlers, middleware, Server Components) and the **Pages Router** (API routes, `getServerSideProps`).

> **Backend only.** Runs in Node.js route handlers and Edge middleware — never in the browser.

## Installation

```bash
npm install @get401/next
# or
pnpm add @get401/next
```

## Setup

Create a shared auth instance — typically in `lib/auth.ts`:

```ts
// lib/auth.ts
import { Get401Auth } from '@get401/next'

export const auth = new Get401Auth({
  appId: process.env.GET401_APP_ID!,
  origin: process.env.GET401_ORIGIN!,
  // host: 'https://app.get401.com', // optional
})
```

Add your environment variables:

```bash
# .env.local
GET401_APP_ID=your-app-id
GET401_ORIGIN=https://yourapp.com
```

---

## App Router — Route Handlers

### `auth.withAuth` — require authentication

```ts
// app/api/me/route.ts
import { auth } from '@/lib/auth'

export const GET = auth.withAuth(async (req, claims) => {
  return Response.json({ userId: claims.sub, roles: claims.roles })
})
```

### `auth.withRoles` — require roles

```ts
// app/api/admin/route.ts
import { auth } from '@/lib/auth'

// At least one role (default)
export const GET = auth.withRoles(['ADMIN'], async (req, claims) => {
  return Response.json({ message: `Hello admin ${claims.sub}` })
})

// All roles required
export const DELETE = auth.withRoles(
  ['ADMIN', 'SUPERUSER'],
  async (req, claims) => Response.json({ ok: true }),
  { requireAll: true },
)
```

### `auth.withScope` — require a scope

```ts
// app/api/reports/export/route.ts
export const POST = auth.withScope('reports:export', async (req, claims) => {
  return Response.json({ ok: true })
})
```

### `auth.getSession` — manual session check

```ts
// app/api/feed/route.ts
import { NextRequest } from 'next/server'
import { auth } from '@/lib/auth'

export async function GET(req: NextRequest) {
  const claims = await auth.getSession(req)

  if (claims) {
    return Response.json(await personalizedFeed(claims.sub))
  }
  return Response.json(await publicFeed())
}
```

---

## App Router — Server Components

Use `auth.verifyToken()` together with `cookies()` from `next/headers`:

```tsx
// app/dashboard/page.tsx
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import { auth } from '@/lib/auth'

export default async function Dashboard() {
  const cookieStore = await cookies()
  const token = cookieStore.get('aact')?.value
  const claims = token ? await auth.verifyToken(token) : null

  if (!claims) redirect('/login')

  return <h1>Hello, {claims.sub}</h1>
}
```

---

## Next.js Middleware

Protect entire path groups at the edge — no per-route boilerplate:

```ts
// middleware.ts
import { auth } from './lib/auth'

export default auth.middleware({
  protectedPaths: ['/dashboard', '/settings', '/api/me'],
  loginUrl: '/login',       // page paths redirect here (with ?returnTo=...)
  onUnauthorized: 'redirect', // 'redirect' (default) | 'json'
})

export const config = {
  matcher: ['/((?!_next|favicon.ico|login|public).*)'],
}
```

**Behavior:**
- `/api/...` protected paths → `401 JSON` response
- Page protected paths → redirect to `loginUrl?returnTo=<path>`

---

## Pages Router

### API routes

```ts
// pages/api/me.ts
import type { NextApiRequest, NextApiResponse } from 'next'
import { auth } from '@/lib/auth'

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const token = req.cookies['aact']
  const claims = token ? await auth.verifyToken(token) : null

  if (!claims) return res.status(401).json({ error: 'Authentication required.' })
  res.json({ userId: claims.sub })
}
```

### `getServerSideProps`

```ts
import { GetServerSideProps } from 'next'
import { auth } from '@/lib/auth'

export const getServerSideProps: GetServerSideProps = async ({ req }) => {
  const token = req.cookies['aact']
  const claims = token ? await auth.verifyToken(token) : null

  if (!claims) return { redirect: { destination: '/login', permanent: false } }
  return { props: { userId: claims.sub } }
}
```

---

## API reference

| Method | Description |
|--------|-------------|
| `auth.getSession(request)` | Returns `TokenClaims \| null` from a `Request` or `NextRequest` |
| `auth.verifyToken(token)` | Returns `TokenClaims \| null` from a raw JWT string |
| `auth.requireSession(request)` | Returns `TokenClaims` or throws a `401 Response` |
| `auth.withAuth(handler)` | Route handler wrapper — requires valid session |
| `auth.withRoles(roles, handler, opts?)` | Route handler wrapper — requires roles |
| `auth.withScope(scope, handler)` | Route handler wrapper — requires scope |
| `auth.middleware(config?)` | Returns a Next.js middleware function |

## HTTP responses on failure

| Situation | Status |
|-----------|--------|
| Missing `aact` cookie | `401` |
| Expired token | `401` |
| Invalid / tampered token | `401` |
| Wrong algorithm | `401` |
| Missing role | `403` |
| Missing scope | `403` |
| get401 backend unreachable | `503` |