@waelhabbaldev/next-jwt-auth
Version:
A secure, lightweight JWT authentication solution for Next.js, providing access and refresh token handling, middleware support, and easy React integration.
472 lines (364 loc) • 15.7 kB
Markdown
# /next-jwt-auth
[](https://www.npmjs.com/package/@waelhabbaldev/next-jwt-auth)
[](https://opensource.org/licenses/MIT)
<div align="center">
<img src="icons/icon.svg" alt="Next.js JWT Auth" width="256" height="256">
</div>
<h3 align="center">Declarative, Secure, Enterprise-Grade Authentication for Next.js</h3>
A lightweight, secure, and performance-optimized authentication library for the Next.js App Router. It implements a robust, multi-layered security model using JWTs and provides a simple, declarative API to protect your pages, server actions, and API routes.
---
## Features
* **State-of-the-Art Security:** Automatic Refresh Token Rotation and JTI-based Reuse Detection to protect against token theft and session hijacking.
* **Flexible JWT Algorithms:** Supports both symmetric (`HS256`) and asymmetric (`RS256`) algorithms out of the box.
* **Zero-Downtime Key Rotation:** Built-in support for JWT Key Rotation (`kid`) to allow for seamless, zero-downtime secret updates.
* **Multi-Factor Authentication (MFA):** Easily add a second factor of authentication (e.g., TOTP) to your sign-in flow.
* **Built-in CSRF Protection:** Double Submit Cookie pattern automatically protects all Server Actions from Cross-Site Request Forgery.
* **Rate Limiting:** Pluggable rate-limiting support to protect your `signIn` endpoint against brute-force attacks.
* **Declarative Protection Guards:** Secure your application with a single line of code using `protectPage()`, `protectAction()`, and `protectApi()`.
* **Optimized for Performance:** Middleware-based session validation and request-level caching (`React.cache`) automatically prevent redundant database calls.
* **Flexible Authorization:** Implement Role-Based (RBAC) or Attribute-Based (ABAC) access control with a simple `authorize` callback in any protection guard.
* **Extensible:** Supports social/OAuth providers, custom logging, and customizable error messages.
* **Session Versioning:** Instantly invalidate all of a user's sessions from the server-side by incrementing a `version` number in the database.
* **Next.js Ready:** Built for the App Router with first-class support for Server Components, Server Actions, API Routes, and Middleware.
---
## Installation
```bash
bun add /next-jwt-auth jose
# or
npm install /next-jwt-auth jose
# or
yarn add /next-jwt-auth jose
```
> **Note:** `jose` is a peer dependency and must be installed alongside the package.
---
## Database Schema Requirements
Your database needs two tables to support all security features.
#### 1. `users` Table
Must include `version` and `isForbidden` columns. For MFA, add `hasMFA`.
```sql
CREATE TABLE `users` (
-- ... other user columns (id, email, passwordHash, etc.)
`version` INT UNSIGNED NOT NULL DEFAULT 1,
`isForbidden` BOOLEAN NOT NULL DEFAULT FALSE,
`hasMFA` BOOLEAN NOT NULL DEFAULT FALSE
);
```
#### 2. `revokedTokens` Table
Required for Refresh Token Reuse Detection, the core defense against token theft.
```sql
CREATE TABLE `revokedTokens` (
`jti` VARCHAR(36) NOT NULL COMMENT 'The JWT ID, typically a UUID.',
`expiresAt` TIMESTAMP NOT NULL COMMENT 'When the token can be safely deleted.',
PRIMARY KEY (`jti`),
INDEX `IX_revokedTokens_expiresAt` (`expiresAt`)
);
```
> **Note:** You should run a scheduled job to periodically delete expired JTIs: `DELETE FROM revokedTokens WHERE expiresAt < NOW();`.
---
## Quick Start
### 1. Define your User Identity and DAL
Implement the `UserIdentityDAL` interface to connect the library to your database. This is the bridge between the auth logic and your data.
```ts
// src/lib/auth-dal.ts
import type { UserIdentity, UserIdentityDAL } from "@waelhabbaldev/next-jwt-auth";
import db from "./db"; // Your database client
import { compare } from "bcrypt"; // Example password hashing library
// Define a type for your user identity that extends the base UserIdentity
export interface AppUserIdentity extends UserIdentity {
id: number;
email: string;
}
export const authDal: UserIdentityDAL<AppUserIdentity> = {
// Find a user by their credentials (e.g., email and password)
async fetchIdentityByCredentials(email, password) {
const user = await db.user.findUnique({ where: { email } });
if (!user) return null;
const isPasswordValid = await compare(password, user.passwordHash);
if (!isPasswordValid) return null;
// Return the full user identity object on success
return { ...user, roles: user.roles.split(',') };
},
// Find a user for an existing session
async fetchIdentityForSession(identifier) {
const user = await db.user.findUnique({ where: { id: Number(identifier) } });
if (!user) return null;
return { ...user, roles: user.roles.split(',') };
},
// Invalidate all sessions by incrementing the user's version
async invalidateAllSessionsForIdentity(identifier) {
await db.user.update({
where: { id: Number(identifier) },
data: { version: { increment: 1 } },
});
},
// Check if a JTI has been used (replay attack defense)
async isTokenJtiUsed(jti) {
const token = await db.revokedToken.findUnique({ where: { jti } });
return !!token;
},
// Mark a JTI as used
async markTokenJtiAsUsed(jti, expirationInSeconds) {
const expiresAt = new Date(Date.now() + expirationInSeconds * 1000);
await db.revokedToken.create({ data: { jti, expiresAt } });
},
// Optional: Implement your TOTP or other MFA verification logic
async verifyMFA(identifier, code) {
// Example: const isValid = totp.verify({ token: code, ... });
return true;
},
};
```
### 2. Configure and Export the Auth Instance
Create a central file (`src/lib/auth.ts`) to configure your `auth` object. **Store secrets in environment variables.**
```ts
// src/lib/auth.ts
import { createAuth } from "@waelhabbaldev/next-jwt-auth";
import { authDal, AppUserIdentity } from "./auth-dal";
import { checkRateLimit } from "./rate-limiter"; // Your rate limit logic
export const {
createMiddleware,
getSession,
getCsrfToken,
signIn,
signOut,
protectPage,
protectAction,
protectApi,
} = createAuth<AppUserIdentity>({
// Required
dal: authDal,
baseUrl: process.env.BASE_URL!, // e.g., "http://localhost:3000"
secrets: {
accessTokenSecret: process.env.ACCESS_TOKEN_SECRET!,
refreshTokenSecret: process.env.REFRESH_TOKEN_SECRET!,
},
cookies: {
access: { name: "__at", maxAge: 15 * 60 }, // 15 minutes
refresh: { name: "__rt", maxAge: 30 * 24 * 60 * 60 }, // 30 days
},
redirects: {
unauthenticated: "/signin",
unauthorized: "/dashboard?error=unauthorized",
forbidden: "/signin?error=forbidden",
},
// Recommended Security Features
refreshTokenRotationIntervalSeconds: 7 * 24 * 60 * 60, // 7 days
csrfEnabled: true,
// Optional Features
rateLimit: checkRateLimit,
debug: process.env.NODE_ENV === "development",
});
```
### 3. Set Up Middleware for Session Management
The middleware is **essential** for handling automatic session refreshing and caching identity for performance.
```ts
// middleware.ts
import { createMiddleware } from "./lib/auth";
export default createMiddleware();
export const config = {
/**
* Forces 'nodejs' runtime. Required for full Node.js APIs (e.g., database clients)
* as the default 'edge' runtime restricts I/O and networking features.
*/
runtime:'nodejs',
// Match all paths except for static assets and API routes.
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};
```
---
## Protecting Your Application
### Protecting Pages and Layouts
Use `protectPage()` to secure any Server Component. It guarantees a valid session or redirects.
```tsx
// app/dashboard/layout.tsx
import { protectPage } from "@/lib/auth";
export default async function DashboardLayout({ children }) {
const session = await protectPage();
// `session.identity` is guaranteed to be valid and available here.
return <main>{children}</main>;
}
```
### Protecting Server Actions
Use `protectAction()` to secure Server Actions. This **automatically validates the CSRF token**.
```ts
// app/actions.ts
"use server";
import { protectAction } from "@/lib/auth";
import { CsrfError } from "@waelhabbaldev/next-jwt-auth";
export async function sensitiveAction(formData: FormData) {
try {
// Pass the formData to enable the automatic CSRF check.
const session = await protectAction(undefined, formData);
// User is authenticated and authorized.
// ... logic for the action ...
} catch (error) {
if (error instanceof CsrfError) {
return { error: "Invalid CSRF token." };
}
// Handle other auth errors (NotAuthenticatedError, ForbiddenError)
}
}
```
#### CSRF Protection with Forms
For forms using Server Actions, you must provide the CSRF token. This is done by generating the token on the server page and passing it to a Client Component.
**1. Generate the token in your Server Page/Component:**
```tsx
// app/settings/page.tsx (Server Component)
import { getCsrfToken } from '@/lib/auth';
import { CsrfProvider } from '/next-jwt-auth/client';
import { SettingsForm } from './settings-form'; // Your form is a Client Component
export default async function SettingsPage() {
const csrfToken = await getCsrfToken();
return (
<CsrfProvider token={csrfToken}>
<SettingsForm />
</CsrfProvider>
);
}
```
**2. Use the `CsrfInput` in your Client Component form:**
Place the `<CsrfInput />` component inside your form. It automatically reads the token from the provider.
```tsx
// components/settings-form.tsx (Client Component)
"use client";
import { CsrfInput } from "@waelhabbaldev/next-jwt-auth/client";
import { sensitiveAction } from "@/app/actions";
export function SettingsForm() {
return (
<form action={sensitiveAction}>
<CsrfInput />
{/* ... rest of your form inputs */}
<button type="submit">Save</button>
</form>
);
}
```
### Protecting API Routes
Use `protectApi()`. On failure, it returns a `response` object that you must immediately return.
```ts
// app/api/projects/[id]/route.ts
import { protectApi } from "@/lib/auth";
import { NextResponse } from "next/server";
export async function GET(req, { params }) {
const result = await protectApi();
// If `response` is present, auth failed. Return it.
if (result.response) return result.response;
// `result.session` is guaranteed to be valid here.
const { session } = result;
return NextResponse.json({ data: `secret data for ${session.identity.email}` });
}
```
---
## Client-Side Usage (React Hooks & Components)
This library provides React components and hooks for managing authentication state in Client Components. These must be imported from the `/client` entry point.
```ts
import { AuthProvider, useAuth } from '/next-jwt-auth/client';
```
### Setting up `AuthProvider`
Wrap your application layout with `AuthProvider` to make the session available globally.
**1. Create Server Actions for the client to use:**
```ts
// app/auth/actions.ts
'use server';
import { getSession, signIn, signOut } from '@/lib/auth';
import { AuthError } from '/next-jwt-auth';
// Action to get the current session
export async function getSessionAction() {
return getSession();
}
// Action to sign in
export async function signInAction(identifier: string, secret: string) {
try {
return await signIn(identifier, secret);
} catch (error) {
if (error instanceof AuthError) {
// Re-throw specific auth errors to be handled by the client
throw error;
}
throw new Error('An unknown error occurred.');
}
}
// Action to sign out
export async function signOutAction() {
return signOut();
}
```
**2. Create a client-side provider wrapper:**
```tsx
// app/providers.tsx
'use client';
import { AuthProvider } from '/next-jwt-auth/client';
import { getSessionAction, signInAction, signOutAction } from '@/app/auth/actions';
export function Providers({ children }) {
return (
<AuthProvider
sessionFetcher={getSessionAction}
signInAction={signInAction}
signOutAction={signOutAction}
>
{children}
</AuthProvider>
);
}
```
**3. Use the provider in your root layout:**
```tsx
// app/layout.tsx
import { Providers } from './providers';
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
```
### Using the `useAuth` Hook
Now, any Client Component can access the authentication state and actions.
```tsx
// components/user-button.tsx
'use client';
import { useAuth } from '/next-jwt-auth/client';
export function UserButton() {
const { identity, isAuthenticated, isLoading, signOut } = useAuth<AppUserIdentity>();
if (isLoading) return <div>Loading...</div>;
if (!isAuthenticated) {
return <a href="/signin">Sign In</a>;
}
return (
<div>
<span>Welcome, {identity.email}!</span>
<button onClick={() => signOut()}>Sign Out</button>
</div>
);
}```
---
## API Reference
### Server-Side API (`/next-jwt-auth`)
| Method | Description |
| --------------------- | --------------------------------------------------------------------------- |
| `createAuth()` | Creates and configures the main authentication instance. |
| `protectPage()` | Secures Pages/Layouts. Guarantees a session or redirects on failure. |
| `protectAction()` | Secures Server Actions. Throws `AuthError` on failure. Auto-validates CSRF. |
| `protectApi()` | Secures API Routes. Returns a `{ session }` or `{ response }` object. |
| `getSession()` | **(Fast)** Gets the session without protection. Reads from cache. Can be `null`. |
| `getCsrfToken()` | Generates a CSRF token for use with `CsrfProvider`. |
| `createMiddleware()` | **(Essential)** Creates the middleware for session management and rotation. |
| `signIn()` | Authenticates a user and sets session cookies. |
| `signOut()` | Deletes session cookies and invalidates the user's sessions. |
| `verifyAccessToken()` | A utility to verify an access token's signature without hitting the DB. |
### Client-Side API (`/next-jwt-auth/client`)
| Component/Hook | Description |
| -------------- | ------------------------------------------------------------------------------- |
| `AuthProvider` | React Context provider for making session state available to Client Components. |
| `useAuth()` | React hook to access session state (`identity`, `isAuthenticated`, `signIn`, `signOut`, etc.). |
| `CsrfProvider` | Provides a CSRF token (generated on the server) to descendant components. |
| `CsrfInput` | A hidden input that automatically includes the CSRF token in a form. |
---
## Keywords
`nextjs`, `authentication`, `authorization`, `jwt`, `auth`, `security`, `rbac`, `abac`, `session-management`, `access-token`, `refresh-token`, `middleware`, `mfa`, `oauth`, `rate-limiting`, `csrf`, `key-rotation`, `nextjs-auth`, `app-router`, `jose`
---
## License
MIT License