Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
28 changes: 28 additions & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
- [Customizing Auth Handlers](#customizing-auth-handlers)
- [Run custom code before Auth Handlers](#run-custom-code-before-auth-handlers)
- [Run code after callback](#run-code-after-callback)
- [Next.js 16 Compatibility](#nextjs-16-compatibility)

## Passing authorization parameters

Expand Down Expand Up @@ -2734,3 +2735,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)
38 changes: 35 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,12 @@ 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.

Create a `middleware.ts` file in the root of your project's directory:
#### 🟦 On Next.js 15

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

```ts
import type { NextRequest } from "next/server";
Expand All @@ -85,7 +89,7 @@ export async function middleware(request: NextRequest) {
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* Match all request paths except for:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico, sitemap.xml, robots.txt (metadata files)
Expand All @@ -98,6 +102,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 Expand Up @@ -410,4 +442,4 @@ Please do not report security vulnerabilities on the public GitHub issue tracker
</p>
<p align="center">
This project is licensed under the MIT license. See the <a href="https://github.com/auth0/nextjs-auth0/blob/main/LICENSE"> LICENSE</a> file for more info.
</p>
</p>
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",
"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
122 changes: 122 additions & 0 deletions src/server/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,128 @@ ykwV8CV22wKDubrDje1vchfTL/ygX6p27RKpJm8eAH7k3EwVeg3NDfNVzQ==
);
});
});

describe("Request normalization | Next 15 + 16 compatibility", () => {
let client: Auth0Client;
let mockSession: SessionData;

beforeEach(() => {
process.env[ENV_VARS.DOMAIN] = "test.auth0.com";
process.env[ENV_VARS.CLIENT_ID] = "test_client_id";
process.env[ENV_VARS.CLIENT_SECRET] = "test_client_secret";
process.env[ENV_VARS.APP_BASE_URL] = "https://myapp.test";
process.env[ENV_VARS.SECRET] = "test_secret";

client = new Auth0Client();
mockSession = {
user: { sub: "user123" },
tokenSet: { accessToken: "token", expiresAt: Date.now() / 1000 + 3600 },
internal: { sid: "sid", createdAt: Date.now() / 1000 },
createdAt: Date.now() / 1000
};
});

it("should return session successfully in getSession with plain Request", async () => {
const spy = vi
.spyOn(client["sessionStore"], "get")
.mockResolvedValue(mockSession);

const req = new Request("https://myapp.test/api/test", { method: "GET" });
const result = await client.getSession(req as any);

expect(spy).toHaveBeenCalledTimes(1);
expect(result).toEqual(mockSession);
});

it("should get access token for connection with plain Request", async () => {
vi.spyOn(client, "getSession").mockResolvedValue(mockSession);
const expiresAt = Math.floor(Date.now() / 1000) + 3600;
vi.spyOn(client["authClient"], "getConnectionTokenSet").mockResolvedValue(
[
null,
{
accessToken: "abc",
expiresAt: expiresAt,
scope: "openid",
connection: "github"
}
]
);
vi.spyOn(client as any, "saveToSession").mockResolvedValue(undefined);

const req = new Request("https://myapp.test/api/test", { method: "GET" });
const res = new Response();

const result = await client.getAccessTokenForConnection(
{ connection: "github" },
req as any,
res as any
);

expect(result.token).toBe("abc");
expect(result.expiresAt).toBe(expiresAt);
});

it("should update session successfully with plain Request", async () => {
vi.spyOn(client, "getSession").mockResolvedValue(mockSession);
vi.spyOn(client["sessionStore"], "set").mockResolvedValue(undefined);

const req = new Request("https://myapp.test/api/update", {
method: "POST"
});
const res = new Response();
const updatedSession = { ...mockSession, user: { sub: "new_user" } };

await client.updateSession(req as any, res as any, updatedSession);

expect(client["sessionStore"].set).toHaveBeenCalledTimes(1);
});

it("should create fetcher successfully with plain Request", async () => {
vi.spyOn(client, "getSession").mockResolvedValue(mockSession);

const mockFetcher = {
config: {},
hooks: {},
isAbsoluteUrl: vi.fn().mockReturnValue(true),
buildUrl: vi.fn().mockReturnValue("https://api.example.com"),
fetchWithAuth: vi.fn().mockResolvedValue(new Response("{}")),
fetch: vi.fn(),
getAccessToken: vi.fn(),
getDPoPProof: vi.fn(),
attachDPoPHeaders: vi.fn(),
validateResponse: vi.fn()
};

vi.spyOn(client["authClient"], "fetcherFactory").mockResolvedValue(
mockFetcher as any
);

const req = new Request("https://myapp.test/api", { method: "GET" });
const fetcher = await client.createFetcher(req as any, {});

expect(fetcher).toBeDefined();
expect(fetcher.fetchWithAuth).toBeInstanceOf(Function);
// Instead of accessing the protected method, test public behavior or remove this line
// For example, you can check that fetchWithAuth was called with an absolute URL
await fetcher.fetchWithAuth("https://api.example.com");
expect(fetcher.fetchWithAuth).toHaveBeenCalledWith(
"https://api.example.com"
);
});

it("should call middleware successfully with plain Request", async () => {
const handlerSpy = vi
.spyOn(client["authClient"], "handler")
.mockResolvedValue(NextResponse.next());

const req = new Request("https://myapp.test/auth", { method: "GET" });
const result = await client.middleware(req as any);

expect(handlerSpy).toHaveBeenCalledTimes(1);
expect(result).toBeInstanceOf(NextResponse);
});
});
});

export type GetAccessTokenOptions = {
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
Loading