Skip to content
Draft
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
27 changes: 27 additions & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2382,3 +2382,30 @@ export async function middleware(request) {
### Run code after callback

Please refer to [onCallback](https://github.com/auth0/nextjs-auth0/blob/main/EXAMPLES.md#oncallback) for details on how to run code after callback.

## Next.js 16 Compatibility
To support `Next.js 16`, rename your `middleware.ts` file to `proxy.ts`, and rename the exported function from `middleware` to `proxy`.
All existing examples and helpers (`getSession`, `updateSession`, `getAccessToken`, etc.) will continue to work without any other changes.

```diff

- // middleware.ts
- export async function middleware(request: NextRequest) {
- return auth0.middleware(request);
- }

+ // proxy.ts
+ export async function proxy(request: Request) {
+ return auth0.middleware(request);
+ }

```
> [!NOTE]
> Next.js 16 still supports the traditional `middleware.ts` file for Edge runtime use-cases,
but it is now considered deprecated. Future versions of `Next.js` may remove Edge-only middleware,
so it’s recommended to migrate to `proxy.ts` for long-term compatibility.

For more details, see the official Next.js documentation:

➡️ [Upgrading to Next 16 Middleware](https://nextjs.org/docs/app/api-reference/file-conventions/proxy#upgrading-to-nextjs-16)
➡️ [Proxy.ts Conventions](https://nextjs.org/docs/app/api-reference/file-conventions/proxy)
55 changes: 55 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,33 @@ export const auth0 = new Auth0Client();
> The Auth0Client automatically uses safe defaults to manage authentication cookies. For advanced use cases, you can customize transaction cookie behavior by providing your own configuration. See [Transaction Cookie Configuration](https://github.com/auth0/nextjs-auth0/blob/main/EXAMPLES.md#transaction-cookie-configuration) for details.

### 4. Add the authentication middleware
Authentication requests in Next.js are intercepted at the network boundary using a middleware or proxy file.
Follow the setup below depending on your Next.js version.

#### 🟦 On Next.js 15

Create a `middleware.ts` file in the root of your project:

```ts
import type { NextRequest } from "next/server";
import { auth0 } from "./lib/auth0"; // Adjust path if your auth0 client is elsewhere

export async function middleware(request: NextRequest) {
return await auth0.middleware(request);
}

export const config = {
matcher: [
/*
* Match all request paths except for:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico, sitemap.xml, robots.txt (metadata files)
*/
"/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)"
]
};
```

Create a `middleware.ts` file in the root of your project's directory:

Expand Down Expand Up @@ -98,6 +125,34 @@ export const config = {
> [!NOTE]
> If you're using a `src/` directory, the `middleware.ts` file must be created inside the `src/` directory.


#### 🟨 On Next.js 16
Next.js 16 introduces a new convention called proxy.ts, replacing middleware.ts.
This change better represents the network interception boundary and unifies request handling
for both the Edge and Node runtimes.

Create a proxy.ts file in the root of your project (Or rename your existing middleware.ts to proxy.ts):
```ts
import { auth0 } from "./lib/auth0";

export async function proxy(request: Request) { // Note that proxy uses the standard Request type
return await auth0.middleware(request);
}

export const config = {
matcher: [
"/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)"
]
};
```
> [!IMPORTANT]
> Starting with **Next.js 16**, the recommended file for handling authentication boundaries is **`proxy.ts`**. You can still continue using **`middleware.ts`** for backward compatibility, it will work under the **Edge runtime** in Next.js 16. However, it is **deprecated** for the Node runtime and will be removed in a future release.
>
> The new proxy layer also executes slightly earlier in the routing pipeline, so make sure your matcher patterns do not conflict with other proxy or middleware routes.
>
> Additionally, the Edge runtime now applies stricter header and cookie validation,
> so avoid setting non-string cookie values or invalid header formats.

> [!IMPORTANT]
> This broad middleware matcher is essential for rolling sessions and security features. For scenarios when rolling sessions are disabled, see [Session Configuration](https://github.com/auth0/nextjs-auth0/blob/main/EXAMPLES.md#session-configuration) for alternative approaches.

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"vitest": "^2.1.4"
},
"peerDependencies": {
"next": "^14.2.25 || ^15.2.3",
"next": "^14.2.25 || ^15.2.3 || ^16.0.0-0",
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-0",
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-0"
},
Expand Down
51 changes: 34 additions & 17 deletions src/server/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
WithPageAuthRequiredAppRouterOptions,
WithPageAuthRequiredPageRouterOptions
} from "./helpers/with-page-auth-required.js";
import { toNextRequest, toNextResponse } from "./next-compat.js";
import {
AbstractSessionStore,
SessionConfiguration,
Expand Down Expand Up @@ -502,8 +503,8 @@ export class Auth0Client {
/**
* middleware mounts the SDK routes to run as a middleware function.
*/
middleware(req: NextRequest): Promise<NextResponse> {
return this.authClient.handler.bind(this.authClient)(req);
middleware(req: Request | NextRequest): Promise<NextResponse> {
return this.authClient.handler.bind(this.authClient)(toNextRequest(req));
}

/**
Expand All @@ -526,12 +527,13 @@ export class Auth0Client {
* getSession returns the session data for the current request.
*/
async getSession(
req?: PagesRouterRequest | NextRequest
req?: Request | PagesRouterRequest | NextRequest
): Promise<SessionData | null> {
if (req) {
// middleware usage
if (req instanceof NextRequest) {
return this.sessionStore.get(req.cookies);
if (req instanceof Request) {
const nextReq = toNextRequest(req);
return this.sessionStore.get(nextReq.cookies);
}

// pages router usage
Expand Down Expand Up @@ -745,7 +747,7 @@ export class Auth0Client {
*/
async getAccessTokenForConnection(
options: AccessTokenForConnectionOptions,
req: PagesRouterRequest | NextRequest | undefined,
req: PagesRouterRequest | NextRequest | Request | undefined,
res: PagesRouterResponse | NextResponse | undefined
): Promise<{ token: string; expiresAt: number }>;

Expand All @@ -768,11 +770,12 @@ export class Auth0Client {
*/
async getAccessTokenForConnection(
options: AccessTokenForConnectionOptions,
req?: PagesRouterRequest | NextRequest,
req?: PagesRouterRequest | NextRequest | Request,
res?: PagesRouterResponse | NextResponse
): Promise<{ token: string; expiresAt: number; scope?: string }> {
const session: SessionData | null = req
? await this.getSession(req)
const nextReq = req instanceof Request ? toNextRequest(req) : req;
const session: SessionData | null = nextReq
? await this.getSession(nextReq)
: await this.getSession();

if (!session) {
Expand Down Expand Up @@ -828,7 +831,7 @@ export class Auth0Client {
...session,
connectionTokenSets: tokenSets
},
req,
nextReq,
res
);
}
Expand All @@ -846,8 +849,8 @@ export class Auth0Client {
* This method can be used in middleware and `getServerSideProps`, API routes, and middleware in the **Pages Router**.
*/
async updateSession(
req: PagesRouterRequest | NextRequest,
res: PagesRouterResponse | NextResponse,
req: PagesRouterRequest | NextRequest | Request,
res: PagesRouterResponse | NextResponse | Response,
session: SessionData
): Promise<void>;

Expand All @@ -862,10 +865,23 @@ export class Auth0Client {
* updateSession updates the session of the currently authenticated user. If the user does not have a session, an error is thrown.
*/
async updateSession(
reqOrSession: PagesRouterRequest | NextRequest | SessionData,
res?: PagesRouterResponse | NextResponse,
reqOrSession: PagesRouterRequest | NextRequest | Request | SessionData,
res?: PagesRouterResponse | NextResponse | Response,
sessionData?: SessionData
) {
// Normalize plain Request (Next 16 Node runtime) to NextRequest
if (
reqOrSession instanceof Request &&
!(reqOrSession instanceof NextRequest)
) {
reqOrSession = toNextRequest(reqOrSession);
}

// Normalize plain Response (Next 16 Node runtime) to NextResponse
if (res && res instanceof Response && !(res instanceof NextResponse)) {
res = toNextResponse(res);
}

if (!res) {
// app router: Server Actions, Route Handlers
const existingSession = await this.getSession();
Expand Down Expand Up @@ -1243,7 +1259,7 @@ export class Auth0Client {
* @see {@link FetcherMinimalConfig} for available configuration options
*/
public async createFetcher<TOutput extends Response = Response>(
req: PagesRouterRequest | NextRequest | undefined,
req: PagesRouterRequest | NextRequest | Request | undefined,
options: {
/** Enable DPoP for this fetcher instance (overrides global setting) */
useDPoP?: boolean;
Expand All @@ -1255,8 +1271,9 @@ export class Auth0Client {
fetch?: CustomFetchImpl<TOutput>;
}
) {
const session: SessionData | null = req
? await this.getSession(req)
const nextReq = req instanceof Request ? toNextRequest(req) : req;
const session: SessionData | null = nextReq
? await this.getSession(nextReq)
: await this.getSession();

if (!session) {
Expand Down
2 changes: 1 addition & 1 deletion src/server/cookies.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { NextResponse } from "next/server.js";
import type { NextResponse } from "next/server.js";
import {
RequestCookie,
RequestCookies,
Expand Down
9 changes: 6 additions & 3 deletions src/server/helpers/with-api-auth-required.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NextApiHandler } from "next";
import { NextRequest, NextResponse } from "next/server.js";

import { Auth0Client } from "../client.js";
import { toNextRequest } from "../next-compat.js";

/**
* This contains `param`s, which is a Promise that resolves to an object
Expand All @@ -22,7 +23,7 @@ export type AppRouteHandlerFn = (
/**
* Incoming request object.
*/
req: NextRequest,
req: NextRequest | Request,
/**
* Context properties on the request (including the parameters if this was a
* dynamic route).
Expand Down Expand Up @@ -80,7 +81,9 @@ export type WithApiAuthRequired = WithApiAuthRequiredAppRoute &
export const appRouteHandlerFactory =
(client: Auth0Client): WithApiAuthRequiredAppRoute =>
(apiRoute) =>
async (req, params): Promise<NextResponse> => {
async (req: NextRequest | Request, params): Promise<NextResponse> => {
const nextReq = req instanceof Request ? toNextRequest(req) : req;

const session = await client.getSession();

if (!session || !session.user) {
Expand All @@ -94,7 +97,7 @@ export const appRouteHandlerFactory =
);
}

const apiRes: NextResponse | Response = await apiRoute(req, params);
const apiRes: NextResponse | Response = await apiRoute(nextReq, params);
const nextApiRes: NextResponse =
apiRes instanceof NextResponse
? apiRes
Expand Down
114 changes: 114 additions & 0 deletions src/server/next-compat.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { NextRequest, NextResponse } from "next/server.js";
import { describe, expect, it } from "vitest";

import { toNextRequest, toNextResponse } from "./next-compat.js";

describe("next-compact", () => {
describe("toNextRequest", () => {
it("should return the same instance if input is already a NextRequest", () => {
const req = new NextRequest("https://example.com/api/test", {
method: "GET"
});
const result = toNextRequest(req);
expect(result).toBe(req);
});

it("should convert a plain Request into a NextRequest preserving url, method, headers and body", async () => {
const headers = new Headers({ "x-test": "true" });
const body = JSON.stringify({ foo: "bar" });
const plainReq = new Request("https://example.com/api/data", {
method: "POST",
headers,
body
});

const nextReq = toNextRequest(plainReq);
expect(nextReq).toBeInstanceOf(NextRequest);
expect(nextReq.url).toBe("https://example.com/api/data");
expect(nextReq.method).toBe("POST");
expect(nextReq.headers.get("x-test")).toBe("true");

const parsed = await nextReq.json();
expect(parsed).toEqual({ foo: "bar" });
});

it("should set duplex to 'half' if not provided", () => {
const req = new Request("https://example.com", { method: "GET" });
const nextReq = toNextRequest(req);
expect((nextReq as any).duplex).toBe("half");
});

it("should default to 'half' duplex when invalid or missing", () => {
// Mock an object without a valid duplex property
const fakeReq: any = {
url: "https://example.com",
method: "GET",
headers: new Headers(),
body: null
};

// The conversion should not throw and should set duplex: 'half'
const nextReq = toNextRequest(fakeReq);
expect(nextReq).toBeInstanceOf(NextRequest);
expect((nextReq as any).duplex).toBe("half");
});
});

describe("toNextResponse", () => {
it("should return the same instance if input is already a NextResponse", () => {
const res = NextResponse.json({ ok: true }, { status: 200 });
const result = toNextResponse(res);
expect(result).toBe(res);
});

it("should convert a plain Response into a NextResponse preserving body, status, and headers", async () => {
const plainRes = new Response(JSON.stringify({ ok: true }), {
status: 202,
statusText: "Accepted",
headers: { "x-test": "42" }
});

const nextRes = toNextResponse(plainRes);
expect(nextRes).toBeInstanceOf(NextResponse);
expect(nextRes.status).toBe(202);
expect(nextRes.statusText).toBe("Accepted");
expect(nextRes.headers.get("x-test")).toBe("42");

const data = await nextRes.json();
expect(data).toEqual({ ok: true });
});

it("should copy url if present (mocked plain object, assignable)", () => {
// Use a *plain object*, not a real Response instance.
const fakeRes: any = {
body: "ok",
status: 200,
statusText: "OK",
headers: new Headers(),
url: "https://example.com/test"
};

const nextRes = toNextResponse(fakeRes);

// NextResponse inherits a read-only url getter, so we can’t assert strict equality here.
// Instead, we confirm our helper didn’t throw and that it *tried* to propagate the url.
expect(nextRes).toBeInstanceOf(NextResponse);
expect(() => (nextRes as any).url).not.toThrow();
});

it("should silently ignore errors when accessing url", () => {
const fakeRes = {
body: "ok",
status: 200,
statusText: "OK",
headers: new Headers()
} as any;
Object.defineProperty(fakeRes, "url", {
get() {
throw new Error("inaccessible");
}
});
expect(() => toNextResponse(fakeRes)).not.toThrow();
});
});
});
Loading