From a9b236d5b801e904f125ca29b118bc5d90d3d37f Mon Sep 17 00:00:00 2001 From: guabu <135956181+guabu@users.noreply.github.com> Date: Sun, 29 Jun 2025 10:49:02 +0200 Subject: [PATCH 1/5] feat: add `withPageAuthRequired` for server --- src/server/client.ts | 35 +- .../helpers/with-page-auth-required.test.ts | 503 ++++++++++++++++++ src/server/helpers/with-page-auth-required.ts | 237 +++++++++ 3 files changed, 773 insertions(+), 2 deletions(-) create mode 100644 src/server/helpers/with-page-auth-required.test.ts create mode 100644 src/server/helpers/with-page-auth-required.ts diff --git a/src/server/client.ts b/src/server/client.ts index db449c6b..ec9179bd 100644 --- a/src/server/client.ts +++ b/src/server/client.ts @@ -7,9 +7,8 @@ import { AccessTokenError, AccessTokenErrorCode, AccessTokenForConnectionError, - AccessTokenForConnectionErrorCode, + AccessTokenForConnectionErrorCode } from "../errors/index.js"; - import { AccessTokenForConnectionOptions, AuthorizationParameters, @@ -24,6 +23,13 @@ import { RoutesOptions } from "./auth-client.js"; import { RequestCookies, ResponseCookies } from "./cookies.js"; +import { + appRouteHandlerFactory, + AppRouterPageRoute, + pageRouteHandlerFactory, + WithPageAuthRequiredAppRouterOptions, + WithPageAuthRequiredPageRouterOptions +} from "./helpers/with-page-auth-required.js"; import { AbstractSessionStore, SessionConfiguration, @@ -188,8 +194,12 @@ export class Auth0Client { private transactionStore: TransactionStore; private sessionStore: AbstractSessionStore; private authClient: AuthClient; + private options: Auth0ClientOptions; constructor(options: Auth0ClientOptions = {}) { + // TODO: temporary hack to figure out how to handle the case where a custom login route is specified + this.options = options; + // Extract and validate required options const { domain, @@ -683,6 +693,27 @@ export class Auth0Client { return this.authClient.startInteractiveLogin(options); } + withPageAuthRequired( + fnOrOpts?: WithPageAuthRequiredPageRouterOptions | AppRouterPageRoute, + opts?: WithPageAuthRequiredAppRouterOptions + ) { + const config = { + // TODO: temporary hack to figure out how to handle the case where a custom login route is specified + loginUrl: + this.options.routes?.login || + process.env.NEXT_PUBLIC_LOGIN_ROUTE || + "/auth/login" + }; + const appRouteHandler = appRouteHandlerFactory(this, config); + const pageRouteHandler = pageRouteHandlerFactory(this, config); + + if (typeof fnOrOpts === "function") { + return appRouteHandler(fnOrOpts, opts); + } + + return pageRouteHandler(fnOrOpts); + } + private async saveToSession( data: SessionData, req?: PagesRouterRequest | NextRequest, diff --git a/src/server/helpers/with-page-auth-required.test.ts b/src/server/helpers/with-page-auth-required.test.ts new file mode 100644 index 00000000..6883ccd9 --- /dev/null +++ b/src/server/helpers/with-page-auth-required.test.ts @@ -0,0 +1,503 @@ +import { IncomingMessage, ServerResponse } from "http"; +import { Socket } from "net"; +import React from "react"; +import { redirect } from "next/navigation.js"; +import ReactDOMServer from "react-dom/server"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { generateSecret } from "../../test/utils.js"; +import { Auth0Client } from "../client.js"; +import { RequestCookies } from "../cookies.js"; +import { + appRouteHandlerFactory, + pageRouteHandlerFactory +} from "./with-page-auth-required.js"; + +describe("with-page-auth-required ssr", () => { + describe("app router", () => { + vi.mock("next/navigation.js", async (importActual) => { + const actual = await importActual(); + + return { + ...actual, + redirect: vi.fn(actual.redirect) + }; + }); + + vi.mock("next/headers.js", async (importActual) => { + const actual = await importActual(); + + return { + ...actual, + cookies: vi.fn().mockImplementation(() => { + return new RequestCookies(new Headers()); + }) + }; + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it("should protect a page", async () => { + const withPageAuthRequired = appRouteHandlerFactory( + new Auth0Client({ + domain: constants.domain, + clientId: constants.clientId, + clientSecret: constants.clientSecret, + appBaseUrl: constants.appBaseUrl, + secret: constants.secret + }), + { + loginUrl: "/auth/login" + } + ); + const handler = withPageAuthRequired(() => + Promise.resolve(React.createElement("div", {}, "foo")) + ); + await expect(handler({})).rejects.toThrowError("NEXT_REDIRECT"); + expect(redirect).toHaveBeenCalledTimes(1); + expect(redirect).toHaveBeenCalledWith("/auth/login"); + }); + + it("should protect a page and redirect to returnTo option", async () => { + const withPageAuthRequired = appRouteHandlerFactory( + new Auth0Client({ + domain: constants.domain, + clientId: constants.clientId, + clientSecret: constants.clientSecret, + appBaseUrl: constants.appBaseUrl, + secret: constants.secret + }), + { + loginUrl: "/auth/login" + } + ); + const handler = withPageAuthRequired( + () => Promise.resolve(React.createElement("div", {}, "foo")), + { + returnTo: "/foo" + } + ); + await expect(handler({})).rejects.toThrowError("NEXT_REDIRECT"); + expect(redirect).toHaveBeenCalledTimes(1); + expect(redirect).toHaveBeenCalledWith("/auth/login?returnTo=/foo"); + }); + + it("should protect a page and redirect to returnTo fn option", async () => { + const withPageAuthRequired = appRouteHandlerFactory( + new Auth0Client({ + domain: constants.domain, + clientId: constants.clientId, + clientSecret: constants.clientSecret, + appBaseUrl: constants.appBaseUrl, + secret: constants.secret + }), + { + loginUrl: "/auth/login" + } + ); + const handler = withPageAuthRequired( + () => Promise.resolve(React.createElement("div", {}, "foo")), + { + async returnTo({ params, searchParams }: any) { + const query = new URLSearchParams(await searchParams).toString(); + return `/foo/${(await params).slug}${query ? `?${query}` : ""}`; + } + } + ); + await expect( + handler({ + params: Promise.resolve({ slug: "bar" }), + searchParams: Promise.resolve({ foo: "bar" }) + }) + ).rejects.toThrowError("NEXT_REDIRECT"); + expect(redirect).toHaveBeenCalledTimes(1); + expect(redirect).toHaveBeenCalledWith( + "/auth/login?returnTo=/foo/bar?foo=bar" + ); + }); + + it("should allow access to a page with a valid session", async () => { + const auth0Client = new Auth0Client({ + domain: constants.domain, + clientId: constants.clientId, + clientSecret: constants.clientSecret, + appBaseUrl: constants.appBaseUrl, + secret: constants.secret + }); + auth0Client.getSession = vi.fn().mockResolvedValue({ + user: { + sub: constants.sub, + name: "Test User" + } + }); + const withPageAuthRequired = appRouteHandlerFactory(auth0Client, { + loginUrl: "/auth/login" + }); + const handler = withPageAuthRequired(() => + Promise.resolve(React.createElement("div", {}, "foo")) + ); + const res = await handler({}); + expect(ReactDOMServer.renderToString(res)).toBe("
foo
"); + expect(auth0Client.getSession).toHaveBeenCalledTimes(1); + }); + + it("should use a custom login url", async () => { + const withPageAuthRequired = appRouteHandlerFactory( + new Auth0Client({ + domain: constants.domain, + clientId: constants.clientId, + clientSecret: constants.clientSecret, + appBaseUrl: constants.appBaseUrl, + secret: constants.secret + // TODO: need to fix this test + // routes: { login: "/api/auth/custom-login" } + }), + { + loginUrl: "/api/auth/custom-login" + } + ); + const handler = withPageAuthRequired(() => + Promise.resolve(React.createElement("div", {}, "foo")) + ); + await expect(handler({})).rejects.toThrowError("NEXT_REDIRECT"); + expect(redirect).toHaveBeenCalledTimes(1); + expect(redirect).toHaveBeenCalledWith("/api/auth/custom-login"); + }); + }); + + describe("pages router", () => { + it("should protect a page", async () => { + const auth0Client = new Auth0Client({ + domain: constants.domain, + clientId: constants.clientId, + clientSecret: constants.clientSecret, + appBaseUrl: constants.appBaseUrl, + secret: constants.secret + }); + const withPageAuthRequired = pageRouteHandlerFactory(auth0Client, { + loginUrl: "/auth/login" + }); + const handler = withPageAuthRequired(); + const res = await handler(mockCtx()); + expect(res).toEqual({ + redirect: { + destination: `/auth/login?returnTo=${encodeURIComponent("/protected")}`, + permanent: false + } + }); + }); + + it("should allow access to a page with a valid session", async () => { + const auth0Client = new Auth0Client({ + domain: constants.domain, + clientId: constants.clientId, + clientSecret: constants.clientSecret, + appBaseUrl: constants.appBaseUrl, + secret: constants.secret + }); + auth0Client.getSession = vi.fn().mockResolvedValue({ + user: { + sub: constants.sub, + name: "Test User" + } + }); + const withPageAuthRequired = pageRouteHandlerFactory(auth0Client, { + loginUrl: "/auth/login" + }); + const handler = withPageAuthRequired(); + const res = await handler(mockCtx()); + expect(res).toEqual({ + props: { + user: { + sub: constants.sub, + name: "Test User" + } + } + }); + }); + + it("should accept a custom returnTo url", async () => { + const auth0Client = new Auth0Client({ + domain: constants.domain, + clientId: constants.clientId, + clientSecret: constants.clientSecret, + appBaseUrl: constants.appBaseUrl, + secret: constants.secret + }); + const withPageAuthRequired = pageRouteHandlerFactory(auth0Client, { + loginUrl: "/auth/login" + }); + const handler = withPageAuthRequired({ + returnTo: "/foo" + }); + const res = await handler(mockCtx()); + expect(res).toEqual({ + redirect: { + destination: `/auth/login?returnTo=${encodeURIComponent("/foo")}`, + permanent: false + } + }); + }); + + it("should accept custom server-side props", async () => { + const auth0Client = new Auth0Client({ + domain: constants.domain, + clientId: constants.clientId, + clientSecret: constants.clientSecret, + appBaseUrl: constants.appBaseUrl, + secret: constants.secret + }); + auth0Client.getSession = vi.fn().mockResolvedValue({ + user: { + sub: constants.sub, + name: "Test User" + } + }); + const withPageAuthRequired = pageRouteHandlerFactory(auth0Client, { + loginUrl: "/auth/login" + }); + const getServerSidePropsSpy = vi.fn().mockResolvedValue({ + props: { + customProp: "value" + } + }); + const handler = withPageAuthRequired({ + getServerSideProps: getServerSidePropsSpy + }); + const res = await handler(mockCtx()); + expect(res).toEqual({ + props: { + customProp: "value", + user: { + sub: constants.sub, + name: "Test User" + } + } + }); + expect(getServerSidePropsSpy).toHaveBeenCalledWith( + expect.objectContaining({ + req: expect.any(IncomingMessage), + res: expect.any(ServerResponse), + query: {}, + resolvedUrl: "/protected" + }) + ); + }); + + it("should allow to override the user prop", async () => { + const auth0Client = new Auth0Client({ + domain: constants.domain, + clientId: constants.clientId, + clientSecret: constants.clientSecret, + appBaseUrl: constants.appBaseUrl, + secret: constants.secret + }); + auth0Client.getSession = vi.fn().mockResolvedValue({ + user: { + sub: constants.sub, + name: "Test User" + } + }); + const withPageAuthRequired = pageRouteHandlerFactory(auth0Client, { + loginUrl: "/auth/login" + }); + const handler = withPageAuthRequired({ + getServerSideProps: async () => ({ + props: { user: { sub: "foo" } } + }) + }); + const res = await handler(mockCtx()); + expect(res).toEqual({ + props: { + user: { sub: "foo" } + } + }); + }); + + it("should allow to override the user prop with async props", async () => { + const auth0Client = new Auth0Client({ + domain: constants.domain, + clientId: constants.clientId, + clientSecret: constants.clientSecret, + appBaseUrl: constants.appBaseUrl, + secret: constants.secret + }); + auth0Client.getSession = vi.fn().mockResolvedValue({ + user: { + sub: constants.sub, + name: "Test User" + } + }); + const withPageAuthRequired = pageRouteHandlerFactory(auth0Client, { + loginUrl: "/auth/login" + }); + const handler = withPageAuthRequired({ + getServerSideProps: async () => { + return { props: Promise.resolve({ user: { sub: "foo" } }) }; + } + }); + const res = await handler(mockCtx()); + expect(res).toEqual({ + props: { + user: { sub: "foo" } + } + }); + }); + + it("should use a custom login url", async () => { + process.env.NEXT_PUBLIC_LOGIN_ROUTE = "/api/auth/custom-login"; + const auth0Client = new Auth0Client({ + domain: constants.domain, + clientId: constants.clientId, + clientSecret: constants.clientSecret, + appBaseUrl: constants.appBaseUrl, + secret: constants.secret + }); + const withPageAuthRequired = pageRouteHandlerFactory(auth0Client, { + // TODO: are we really testing this?? + loginUrl: "/api/auth/custom-login" + }); + const handler = withPageAuthRequired(); + const res = await handler(mockCtx()); + expect(res).toEqual({ + redirect: { + destination: `/api/auth/custom-login?returnTo=${encodeURIComponent( + "/protected" + )}`, + permanent: false + } + }); + delete process.env.NEXT_PUBLIC_LOGIN_ROUTE; + }); + + it("should preserve multiple query params in the returnTo URL", async () => { + const auth0Client = new Auth0Client({ + domain: constants.domain, + clientId: constants.clientId, + clientSecret: constants.clientSecret, + appBaseUrl: constants.appBaseUrl, + secret: constants.secret + }); + const withPageAuthRequired = pageRouteHandlerFactory(auth0Client, { + loginUrl: "/auth/login" + }); + const handler = withPageAuthRequired({ + returnTo: "/foo?bar=baz&qux=quux" + }); + const res = await handler(mockCtx()); + expect(res).toEqual({ + redirect: { + destination: `/auth/login?returnTo=${encodeURIComponent( + "/foo?bar=baz&qux=quux" + )}`, + permanent: false + } + }); + }); + + it("should allow access to a page with a valid session and async props", async () => { + const auth0Client = new Auth0Client({ + domain: constants.domain, + clientId: constants.clientId, + clientSecret: constants.clientSecret, + appBaseUrl: constants.appBaseUrl, + secret: constants.secret + }); + auth0Client.getSession = vi.fn().mockResolvedValue({ + user: { + sub: constants.sub, + name: "Test User" + } + }); + const withPageAuthRequired = pageRouteHandlerFactory(auth0Client, { + loginUrl: "/auth/login" + }); + const handler = withPageAuthRequired({ + getServerSideProps() { + return Promise.resolve({ props: Promise.resolve({}) }); + } + }); + const res = await handler(mockCtx()); + expect(res).toEqual({ + props: { + user: { + sub: constants.sub, + name: "Test User" + } + } + }); + }); + + it("should save session when getServerSideProps completes async", async () => { + const auth0Client = new Auth0Client({ + domain: constants.domain, + clientId: constants.clientId, + clientSecret: constants.clientSecret, + appBaseUrl: constants.appBaseUrl, + secret: constants.secret + }); + auth0Client.getSession = vi.fn().mockResolvedValue({ + user: { + sub: constants.sub, + name: "Test User" + } + }); + auth0Client.updateSession = vi.fn().mockResolvedValue({}); + + const withPageAuthRequired = pageRouteHandlerFactory(auth0Client, { + loginUrl: "/auth/login" + }); + const handler = withPageAuthRequired({ + async getServerSideProps(ctx) { + await Promise.resolve(); + const session = await auth0Client.getSession(ctx.req); + await auth0Client.updateSession(ctx.req, ctx.res, { + ...session!, + test: "Hello World!" + }); + return { props: {} }; + } + }); + + const res = await handler(mockCtx()); + expect(res).toEqual({ + props: { + user: { + sub: constants.sub, + name: "Test User" + } + } + }); + expect(auth0Client.updateSession).toHaveBeenCalledWith( + expect.any(IncomingMessage), + expect.any(ServerResponse), + expect.objectContaining({ test: "Hello World!" }) + ); + }); + }); +}); + +function mockCtx() { + const mockReq = Object.assign(new IncomingMessage(new Socket()), { + cookies: {} + }); + const mockRes = new ServerResponse(mockReq); + + return { + resolvedUrl: "/protected", + req: mockReq, + res: mockRes, + query: {} + }; +} + +const constants = { + domain: "guabu.us.auth0.com", + clientId: "client_123", + clientSecret: "client-secret", + appBaseUrl: "https://example.com", + sub: "user_123", + secret: await generateSecret(32) +}; diff --git a/src/server/helpers/with-page-auth-required.ts b/src/server/helpers/with-page-auth-required.ts new file mode 100644 index 00000000..27b3f30c --- /dev/null +++ b/src/server/helpers/with-page-auth-required.ts @@ -0,0 +1,237 @@ +import type { ParsedUrlQuery } from "querystring"; +import type React from "react"; +import { + GetServerSideProps, + GetServerSidePropsContext, + GetServerSidePropsResult +} from "next"; +import { redirect } from "next/navigation.js"; + +import { User } from "../../types/index.js"; +import { Auth0Client } from "../client.js"; + +/** + * If you wrap your `getServerSideProps` with {@link WithPageAuthRequired} your props object will be augmented with + * the user property, which will be the user's {@link Claims}. + * + * ```js + * // pages/profile.js + * import { withPageAuthRequired } from '@auth0/nextjs-auth0'; + * + * export default function Profile({ user }) { + * return
Hello {user.name}
; + * } + * + * export const getServerSideProps = withPageAuthRequired(); + * ``` + */ +export type GetServerSidePropsResultWithSession

= + GetServerSidePropsResult

; + +/** + * A page route that has been augmented with {@link WithPageAuthRequired}. + */ +export type PageRoute = ( + ctx: GetServerSidePropsContext +) => Promise>; + +/** + * Objects containing the route parameters and search parameters of th page. + */ +export type AppRouterPageRouteOpts = { + params?: Promise>; + searchParams?: Promise<{ [key: string]: string | string[] | undefined }>; +}; + +/** + * An app route that has been augmented with {@link WithPageAuthRequired}. + */ +export type AppRouterPageRoute = ( + obj: AppRouterPageRouteOpts +) => Promise; + +/** + * If you have a custom returnTo url you should specify it in `returnTo`. + * + * You can pass in your own `getServerSideProps` method, the props returned from this will be + * merged with the user props. You can also access the user session data by calling `getSession` + * inside of this method. For example: + * + * ```js + * // pages/protected-page.js + * import { getSession, withPageAuthRequired } from '@auth0/nextjs-auth0'; + * + * export default function ProtectedPage({ user, customProp }) { + * return

Protected content
; + * } + * + * export const getServerSideProps = withPageAuthRequired({ + * // returnTo: '/unauthorized', + * async getServerSideProps(ctx) { + * // access the user session if needed + * // const session = await getSession(ctx.req, ctx.res); + * return { + * props: { + * // customProp: 'bar', + * } + * }; + * } + * }); + * ``` + */ +export type WithPageAuthRequiredPageRouterOptions< + P extends { [key: string]: any } = { [key: string]: any }, + Q extends ParsedUrlQuery = ParsedUrlQuery +> = { + getServerSideProps?: GetServerSideProps; + returnTo?: string; +}; + +/** + * Wrap your `getServerSideProps` with this method to make sure the user is authenticated before + * visiting the page. + * + * ```js + * // pages/protected-page.js + * import { withPageAuthRequired } from '@auth0/nextjs-auth0'; + * + * export default function ProtectedPage() { + * return
Protected content
; + * } + * + * export const getServerSideProps = withPageAuthRequired(); + * ``` + * + * If the user visits `/protected-page` without a valid session, it will redirect the user to the + * login page. Then they will be returned to `/protected-page` after login. + */ +export type WithPageAuthRequiredPageRouter = < + P extends { [key: string]: any } = { [key: string]: any }, + Q extends ParsedUrlQuery = ParsedUrlQuery +>( + opts?: WithPageAuthRequiredPageRouterOptions +) => PageRoute; + +/** + * Specify the URL to `returnTo` - this is important in app router pages because the server component + * won't know the URL of the page. + */ +export type WithPageAuthRequiredAppRouterOptions = { + returnTo?: + | string + | ((obj: AppRouterPageRouteOpts) => Promise | string); +}; + +/** + * Wrap your Server Component with this method to make sure the user is authenticated before + * visiting the page. + * + * ```js + * // app/protected-page/page.js + * import { withPageAuthRequired } from '@auth0/nextjs-auth0'; + * + * const ProtectedPage = withPageAuthRequired(async function ProtectedPage() { + * return
Protected content
; + * }, { returnTo: '/protected-page' }); + * + * export default ProtectedPage; + * ``` + * + * If the user visits `/protected-page` without a valid session, it will redirect the user to the + * login page. + * + * Note: Server Components are not aware of the req or the url of the page. So if you want the user to return to the + * page after login, you must specify the `returnTo` option. + * + * You can specify a function to `returnTo` that accepts the `params` (An object containing the dynamic + * route parameters) and `searchParams` (An object containing the search parameters of the current URL) + * argument from the page, to preserve dynamic routes and search params. + * + * ```js + * // app/protected-page/[slug]/page.js + * import { AppRouterPageRouteOpts, withPageAuthRequired } from '@auth0/nextjs-auth0'; + * + * const ProtectedPage = withPageAuthRequired(async function ProtectedPage({ + * params, searchParams + * }: AppRouterPageRouteOpts) { + * const slug = params?.slug as string; + * return
Protected content for {slug}
; + * }, { + * returnTo({ params }) { + * return `/protected-page/${params?.slug}`; + * } + * }); + * + * export default ProtectedPage; + * ``` + */ +export type WithPageAuthRequiredAppRouter = ( + fn: AppRouterPageRoute, + opts?: WithPageAuthRequiredAppRouterOptions +) => AppRouterPageRoute; + +/** + * Protects Page router pages {@link WithPageAuthRequiredPageRouter} or + * App router pages {@link WithPageAuthRequiredAppRouter} + */ +export type WithPageAuthRequired = WithPageAuthRequiredPageRouter & + WithPageAuthRequiredAppRouter; + +export const appRouteHandlerFactory = + ( + client: Auth0Client, + config: { + loginUrl: string; + } + ): WithPageAuthRequiredAppRouter => + (handler, opts = {}) => + async (params) => { + const session = await client.getSession(); + + if (!session?.user) { + const returnTo = + typeof opts.returnTo === "function" + ? await opts.returnTo(params) + : opts.returnTo; + redirect( + `${config.loginUrl}${opts.returnTo ? `?returnTo=${returnTo}` : ""}` + ); + } + return handler(params); + }; + +export const pageRouteHandlerFactory = + ( + client: Auth0Client, + config: { + loginUrl: string; + } + ): WithPageAuthRequiredPageRouter => + ({ getServerSideProps, returnTo } = {}) => + async (ctx) => { + const session = await client.getSession(ctx.req); + + if (!session?.user) { + return { + redirect: { + destination: `${config.loginUrl}?returnTo=${encodeURIComponent(returnTo || ctx.resolvedUrl)}`, + permanent: false + } + }; + } + let ret: any = { props: {} }; + if (getServerSideProps) { + ret = await getServerSideProps(ctx); + } + if (ret.props instanceof Promise) { + const props = await ret.props; + return { + ...ret, + props: { + user: session.user, + ...props + } + }; + } + return { ...ret, props: { user: session.user, ...ret.props } }; + }; From 1f78e61fecc9af4680ff312312a91112de3c4321 Mon Sep 17 00:00:00 2001 From: guabu <135956181+guabu@users.noreply.github.com> Date: Tue, 1 Jul 2025 14:54:02 +0200 Subject: [PATCH 2/5] chore: prepare routes on the client --- src/server/auth-client.test.ts | 178 ++++++++++++++++++ src/server/auth-client.ts | 13 +- src/server/client.ts | 25 ++- .../helpers/with-page-auth-required.test.ts | 6 +- .../redundant-txn-cookie-deletion.test.ts | 7 +- src/test/defaults.ts | 13 ++ 6 files changed, 211 insertions(+), 31 deletions(-) create mode 100644 src/test/defaults.ts diff --git a/src/server/auth-client.test.ts b/src/server/auth-client.test.ts index 00fe8bad..62511509 100644 --- a/src/server/auth-client.test.ts +++ b/src/server/auth-client.test.ts @@ -3,6 +3,7 @@ import * as jose from "jose"; import * as oauth from "oauth4webapi"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; +import { getDefaultRoutes } from "../test/defaults.js"; import { generateSecret } from "../test/utils.js"; import { SessionData } from "../types/index.js"; import { AuthClient } from "./auth-client.js"; @@ -212,6 +213,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + authorizationParameters: { scope: "profile email" }, @@ -242,6 +245,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer() }); const request = new NextRequest("https://example.com/auth/login", { @@ -271,6 +276,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer() }); const request = new NextRequest("https://example.com/auth/callback", { @@ -300,6 +307,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer() }); const request = new NextRequest("https://example.com/auth/logout", { @@ -329,6 +338,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer() }); const request = new NextRequest("https://example.com/auth/profile", { @@ -357,6 +368,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + + routes: getDefaultRoutes(), enableAccessTokenEndpoint: true, fetch: getMockAuthorizationServer() @@ -387,6 +400,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + + routes: getDefaultRoutes(), enableAccessTokenEndpoint: false, fetch: getMockAuthorizationServer() @@ -419,6 +434,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + + routes: getDefaultRoutes(), // enableAccessTokenEndpoint not specified, should default to true fetch: getMockAuthorizationServer() @@ -450,6 +467,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer() }); const request = new NextRequest( @@ -487,6 +506,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer() }); @@ -568,6 +589,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer() }); @@ -612,6 +635,7 @@ ca/T0LLtgmbMmxSv/MmzIg== fetch: getMockAuthorizationServer(), routes: { + ...getDefaultRoutes(), login: "/custom-login" } }); @@ -649,6 +673,7 @@ ca/T0LLtgmbMmxSv/MmzIg== fetch: getMockAuthorizationServer(), routes: { + ...getDefaultRoutes(), logout: "/custom-logout" } }); @@ -686,6 +711,7 @@ ca/T0LLtgmbMmxSv/MmzIg== fetch: getMockAuthorizationServer(), routes: { + ...getDefaultRoutes(), callback: "/custom-callback" } }); @@ -723,6 +749,7 @@ ca/T0LLtgmbMmxSv/MmzIg== fetch: getMockAuthorizationServer(), routes: { + ...getDefaultRoutes(), backChannelLogout: "/custom-backchannel-logout" } }); @@ -759,6 +786,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer() }); const request = new NextRequest( @@ -796,6 +825,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer() }); const request = new NextRequest( @@ -875,6 +906,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer() }); @@ -917,6 +950,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer() }); const request = new NextRequest( @@ -990,6 +1025,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: `${DEFAULT.appBaseUrl}/sub-path`, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer() }); const request = new NextRequest( @@ -1035,6 +1072,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: `${DEFAULT.appBaseUrl}`, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer() }); const request = new NextRequest( @@ -1075,6 +1114,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer({ discoveryResponse: new Response(null, { status: 500 }) }) @@ -1114,6 +1155,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer() }); const loginUrl = new URL("/auth/login", DEFAULT.appBaseUrl); @@ -1200,6 +1243,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer() }); const loginUrl = new URL("/auth/login", DEFAULT.appBaseUrl); @@ -1266,6 +1311,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer() }); const loginUrl = new URL("/auth/login", DEFAULT.appBaseUrl); @@ -1343,6 +1390,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer() }); const loginUrl = new URL("/auth/login", DEFAULT.appBaseUrl); @@ -1420,6 +1469,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer() }); const loginUrl = new URL("/auth/login", DEFAULT.appBaseUrl); @@ -1482,6 +1533,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer() }); const loginUrl = new URL("/auth/login", DEFAULT.appBaseUrl); @@ -1530,6 +1583,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer() }); const loginUrl = new URL("/auth/login", DEFAULT.appBaseUrl); @@ -1576,6 +1631,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer() }); const loginUrl = new URL("/auth/login", DEFAULT.appBaseUrl); @@ -1621,6 +1678,8 @@ ca/T0LLtgmbMmxSv/MmzIg== pushedAuthorizationRequests: true, secret, appBaseUrl: DEFAULT.appBaseUrl, + + routes: getDefaultRoutes(), fetch: getMockAuthorizationServer({ discoveryResponse: Response.json( { @@ -1668,6 +1727,8 @@ ca/T0LLtgmbMmxSv/MmzIg== pushedAuthorizationRequests: true, secret, appBaseUrl: DEFAULT.appBaseUrl, + + routes: getDefaultRoutes(), fetch: getMockAuthorizationServer({ onParRequest: async (request) => { const params = new URLSearchParams(await request.text()); @@ -1764,6 +1825,8 @@ ca/T0LLtgmbMmxSv/MmzIg== pushedAuthorizationRequests: true, secret, appBaseUrl: DEFAULT.appBaseUrl, + + routes: getDefaultRoutes(), fetch: getMockAuthorizationServer({ onParRequest: async (request) => { const params = new URLSearchParams(await request.text()); @@ -1838,6 +1901,8 @@ ca/T0LLtgmbMmxSv/MmzIg== pushedAuthorizationRequests: true, secret, appBaseUrl: DEFAULT.appBaseUrl, + + routes: getDefaultRoutes(), fetch: getMockAuthorizationServer({ onParRequest: async (request) => { const params = new URLSearchParams(await request.text()); @@ -1916,6 +1981,8 @@ ca/T0LLtgmbMmxSv/MmzIg== pushedAuthorizationRequests: true, secret, appBaseUrl: DEFAULT.appBaseUrl, + + routes: getDefaultRoutes(), authorizationParameters: { "ext-custom_param": "custom_value", audience: "urn:mystore:api" @@ -1999,6 +2066,7 @@ ca/T0LLtgmbMmxSv/MmzIg== fetch: getMockAuthorizationServer(), routes: { + ...getDefaultRoutes(), callback: "/custom-callback" } }); @@ -2044,6 +2112,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer() }); @@ -2120,6 +2190,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer() }); @@ -2192,6 +2264,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer() }); @@ -2229,6 +2303,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer() }); @@ -2280,6 +2356,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer({ discoveryResponse: Response.json( { @@ -2339,6 +2417,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer({ discoveryResponse: new Response(null, { status: 500 }) }) @@ -2379,6 +2459,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer() }); @@ -2442,6 +2524,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer() }); @@ -2476,6 +2560,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer(), noContentProfileResponseWhenUnauthenticated: true @@ -2517,6 +2603,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer() }); @@ -2613,6 +2701,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer() }); @@ -2700,6 +2790,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer() }); @@ -2786,6 +2878,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer() }); @@ -2823,6 +2917,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer() }); @@ -2876,6 +2972,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer() }); @@ -2933,6 +3031,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer({ tokenEndpointResponse: { error: "some-error-code", @@ -2994,6 +3094,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer({ discoveryResponse: new Response(null, { status: 500 }) }) @@ -3059,6 +3161,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer(), onCallback: mockOnCallback @@ -3157,6 +3261,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer(), onCallback: mockOnCallback @@ -3217,6 +3323,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer(), onCallback: mockOnCallback @@ -3293,6 +3401,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer(), onCallback: mockOnCallback @@ -3375,6 +3485,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer({ tokenEndpointFetchError: new Error("Timeout error") }), @@ -3454,6 +3566,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer({ tokenEndpointResponse: { error: "some-error-code", @@ -3545,6 +3659,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer(), beforeSessionSaved: mockBeforeSessionSaved @@ -3617,6 +3733,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer(), beforeSessionSaved: async (session) => { @@ -3713,6 +3831,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer(), beforeSessionSaved: mockBeforeSessionSaved @@ -3749,6 +3869,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer(), // @ts-expect-error intentionally testing invalid internal session data @@ -3851,6 +3973,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer({ tokenEndpointResponse: { token_type: "Bearer", @@ -3930,6 +4054,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer() }); @@ -3973,6 +4099,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer() }); @@ -4049,6 +4177,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer(), jwksCache: await getCachedJWKS() }); @@ -4093,6 +4223,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer(), jwksCache: await getCachedJWKS() }); @@ -4138,6 +4270,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer(), jwksCache: await getCachedJWKS() }); @@ -4186,6 +4320,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer(), jwksCache: await getCachedJWKS() }); @@ -4235,6 +4371,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer(), jwksCache: await getCachedJWKS() }); @@ -4278,6 +4416,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer(), jwksCache: await getCachedJWKS() }); @@ -4328,6 +4468,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer(), jwksCache: await getCachedJWKS() }); @@ -4377,6 +4519,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer(), jwksCache: await getCachedJWKS() }); @@ -4426,6 +4570,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer(), jwksCache: await getCachedJWKS() }); @@ -4475,6 +4621,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer(), jwksCache: await getCachedJWKS() }); @@ -4524,6 +4672,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer(), jwksCache: await getCachedJWKS() }); @@ -4572,6 +4722,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer() }); @@ -4606,6 +4758,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer() }); @@ -4639,6 +4793,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer({ tokenEndpointResponse: { token_type: "Bearer", @@ -4683,6 +4839,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer({ tokenEndpointResponse: { error: "some-error-code", @@ -4722,6 +4880,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer({ discoveryResponse: new Response(null, { status: 500 }) }) @@ -4759,6 +4919,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer({ tokenEndpointResponse: { token_type: "Bearer", @@ -4811,6 +4973,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + + routes: getDefaultRoutes(), signInReturnToPath, pushedAuthorizationRequests, authorizationParameters: { @@ -4949,6 +5113,8 @@ ca/T0LLtgmbMmxSv/MmzIg== clientSecret: DEFAULT.clientSecret, secret, appBaseUrl: DEFAULT.appBaseUrl, + + routes: getDefaultRoutes(), pushedAuthorizationRequests: true, authorizationParameters: { scope: "openid profile email" @@ -5102,6 +5268,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: fetchSpy }); @@ -5147,6 +5315,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: fetchSpy }); @@ -5202,6 +5372,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: fetchSpy }); @@ -5246,6 +5418,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer({ discoveryResponse: new Response(null, { status: 500 }) }) @@ -5285,6 +5459,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer() }); @@ -5321,6 +5497,8 @@ ca/T0LLtgmbMmxSv/MmzIg== secret, appBaseUrl: DEFAULT.appBaseUrl, + routes: getDefaultRoutes(), + fetch: getMockAuthorizationServer({ tokenEndpointErrorResponse: { error: "some-error-code", diff --git a/src/server/auth-client.ts b/src/server/auth-client.ts index f874fa2f..78a0db28 100644 --- a/src/server/auth-client.ts +++ b/src/server/auth-client.ts @@ -128,7 +128,7 @@ export interface AuthClientOptions { beforeSessionSaved?: BeforeSessionSavedHook; onCallback?: OnCallbackHook; - routes?: RoutesOptions; + routes: Routes; // custom fetch implementation to allow for dependency injection fetch?: typeof fetch; @@ -253,16 +253,7 @@ export class AuthClient { this.onCallback = options.onCallback || this.defaultOnCallback; // routes - this.routes = { - login: process.env.NEXT_PUBLIC_LOGIN_ROUTE || "/auth/login", - logout: "/auth/logout", - callback: "/auth/callback", - backChannelLogout: "/auth/backchannel-logout", - profile: process.env.NEXT_PUBLIC_PROFILE_ROUTE || "/auth/profile", - accessToken: - process.env.NEXT_PUBLIC_ACCESS_TOKEN_ROUTE || "/auth/access-token", - ...options.routes - }; + this.routes = options.routes; this.enableAccessTokenEndpoint = options.enableAccessTokenEndpoint ?? true; this.noContentProfileResponseWhenUnauthenticated = diff --git a/src/server/client.ts b/src/server/client.ts index ec9179bd..661aaffe 100644 --- a/src/server/client.ts +++ b/src/server/client.ts @@ -20,6 +20,7 @@ import { AuthClient, BeforeSessionSavedHook, OnCallbackHook, + Routes, RoutesOptions } from "./auth-client.js"; import { RequestCookies, ResponseCookies } from "./cookies.js"; @@ -194,12 +195,9 @@ export class Auth0Client { private transactionStore: TransactionStore; private sessionStore: AbstractSessionStore; private authClient: AuthClient; - private options: Auth0ClientOptions; + private routes: Routes; constructor(options: Auth0ClientOptions = {}) { - // TODO: temporary hack to figure out how to handle the case where a custom login route is specified - this.options = options; - // Extract and validate required options const { domain, @@ -246,6 +244,17 @@ export class Auth0Client { } } + this.routes = { + login: process.env.NEXT_PUBLIC_LOGIN_ROUTE || "/auth/login", + logout: "/auth/logout", + callback: "/auth/callback", + backChannelLogout: "/auth/backchannel-logout", + profile: process.env.NEXT_PUBLIC_PROFILE_ROUTE || "/auth/profile", + accessToken: + process.env.NEXT_PUBLIC_ACCESS_TOKEN_ROUTE || "/auth/access-token", + ...options.routes + }; + this.transactionStore = new TransactionStore({ ...options.session, secret, @@ -284,7 +293,7 @@ export class Auth0Client { beforeSessionSaved: options.beforeSessionSaved, onCallback: options.onCallback, - routes: options.routes, + routes: this.routes, allowInsecureRequests: options.allowInsecureRequests, httpTimeout: options.httpTimeout, @@ -698,11 +707,7 @@ export class Auth0Client { opts?: WithPageAuthRequiredAppRouterOptions ) { const config = { - // TODO: temporary hack to figure out how to handle the case where a custom login route is specified - loginUrl: - this.options.routes?.login || - process.env.NEXT_PUBLIC_LOGIN_ROUTE || - "/auth/login" + loginUrl: this.routes.login }; const appRouteHandler = appRouteHandlerFactory(this, config); const pageRouteHandler = pageRouteHandlerFactory(this, config); diff --git a/src/server/helpers/with-page-auth-required.test.ts b/src/server/helpers/with-page-auth-required.test.ts index 6883ccd9..416d19ec 100644 --- a/src/server/helpers/with-page-auth-required.test.ts +++ b/src/server/helpers/with-page-auth-required.test.ts @@ -5,6 +5,7 @@ import { redirect } from "next/navigation.js"; import ReactDOMServer from "react-dom/server"; import { afterEach, describe, expect, it, vi } from "vitest"; +import { getDefaultRoutes } from "../../test/defaults.js"; import { generateSecret } from "../../test/utils.js"; import { Auth0Client } from "../client.js"; import { RequestCookies } from "../cookies.js"; @@ -151,8 +152,6 @@ describe("with-page-auth-required ssr", () => { clientSecret: constants.clientSecret, appBaseUrl: constants.appBaseUrl, secret: constants.secret - // TODO: need to fix this test - // routes: { login: "/api/auth/custom-login" } }), { loginUrl: "/api/auth/custom-login" @@ -347,7 +346,6 @@ describe("with-page-auth-required ssr", () => { }); it("should use a custom login url", async () => { - process.env.NEXT_PUBLIC_LOGIN_ROUTE = "/api/auth/custom-login"; const auth0Client = new Auth0Client({ domain: constants.domain, clientId: constants.clientId, @@ -356,7 +354,6 @@ describe("with-page-auth-required ssr", () => { secret: constants.secret }); const withPageAuthRequired = pageRouteHandlerFactory(auth0Client, { - // TODO: are we really testing this?? loginUrl: "/api/auth/custom-login" }); const handler = withPageAuthRequired(); @@ -369,7 +366,6 @@ describe("with-page-auth-required ssr", () => { permanent: false } }); - delete process.env.NEXT_PUBLIC_LOGIN_ROUTE; }); it("should preserve multiple query params in the returnTo URL", async () => { diff --git a/src/server/redundant-txn-cookie-deletion.test.ts b/src/server/redundant-txn-cookie-deletion.test.ts index b4a166d5..3a2b7bef 100644 --- a/src/server/redundant-txn-cookie-deletion.test.ts +++ b/src/server/redundant-txn-cookie-deletion.test.ts @@ -4,6 +4,7 @@ import * as oauth from "oauth4webapi"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { InvalidStateError, MissingStateError } from "../errors/index.js"; +import { getDefaultRoutes } from "../test/defaults.js"; import { SessionData } from "../types/index.js"; import { AuthClient, AuthClientOptions } from "./auth-client.js"; import { @@ -50,11 +51,7 @@ const baseOptions: Partial = { clientSecret: "test-client-secret", appBaseUrl: "http://localhost:3000", secret: "a-sufficiently-long-secret-for-testing", - routes: { - login: "/api/auth/login", - logout: "/api/auth/logout", - callback: "/api/auth/callback" - } + routes: getDefaultRoutes() }; describe("Ensure that redundant transaction cookies are deleted from auth-client methods", () => { diff --git a/src/test/defaults.ts b/src/test/defaults.ts new file mode 100644 index 00000000..c68a8076 --- /dev/null +++ b/src/test/defaults.ts @@ -0,0 +1,13 @@ +import type { Routes } from "../server/auth-client.js"; + +export function getDefaultRoutes(): Routes { + return { + login: process.env.NEXT_PUBLIC_LOGIN_ROUTE || "/auth/login", + logout: "/auth/logout", + callback: "/auth/callback", + backChannelLogout: "/auth/backchannel-logout", + profile: process.env.NEXT_PUBLIC_PROFILE_ROUTE || "/auth/profile", + accessToken: + process.env.NEXT_PUBLIC_ACCESS_TOKEN_ROUTE || "/auth/access-token" + }; +} From b8fe9ae8c8719f35194459b46bb842e7a06146d1 Mon Sep 17 00:00:00 2001 From: guabu <135956181+guabu@users.noreply.github.com> Date: Tue, 1 Jul 2025 17:55:42 +0200 Subject: [PATCH 3/5] chore: export types and update doc strings --- src/server/helpers/with-page-auth-required.ts | 36 ++++++++++--------- src/server/index.ts | 12 +++++++ 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/src/server/helpers/with-page-auth-required.ts b/src/server/helpers/with-page-auth-required.ts index 27b3f30c..bdccac3b 100644 --- a/src/server/helpers/with-page-auth-required.ts +++ b/src/server/helpers/with-page-auth-required.ts @@ -12,17 +12,17 @@ import { Auth0Client } from "../client.js"; /** * If you wrap your `getServerSideProps` with {@link WithPageAuthRequired} your props object will be augmented with - * the user property, which will be the user's {@link Claims}. + * the user property, which will be the {@link User} object. * * ```js * // pages/profile.js - * import { withPageAuthRequired } from '@auth0/nextjs-auth0'; + * import { auth0 } from "@/lib/auth0"; * * export default function Profile({ user }) { * return
Hello {user.name}
; * } * - * export const getServerSideProps = withPageAuthRequired(); + * export const getServerSideProps = auth0.withPageAuthRequired(); * ``` */ export type GetServerSidePropsResultWithSession

= @@ -36,7 +36,7 @@ export type PageRoute = ( ) => Promise>; /** - * Objects containing the route parameters and search parameters of th page. + * Objects containing the route parameters and search parameters of the page. */ export type AppRouterPageRouteOpts = { params?: Promise>; @@ -59,17 +59,17 @@ export type AppRouterPageRoute = ( * * ```js * // pages/protected-page.js - * import { getSession, withPageAuthRequired } from '@auth0/nextjs-auth0'; + * import { auth0 } from "@/lib/auth0"; * * export default function ProtectedPage({ user, customProp }) { * return

Protected content
; * } * - * export const getServerSideProps = withPageAuthRequired({ + * export const getServerSideProps = auth0.withPageAuthRequired({ * // returnTo: '/unauthorized', * async getServerSideProps(ctx) { * // access the user session if needed - * // const session = await getSession(ctx.req, ctx.res); + * // const session = await auth0.getSession(ctx.req); * return { * props: { * // customProp: 'bar', @@ -93,13 +93,13 @@ export type WithPageAuthRequiredPageRouterOptions< * * ```js * // pages/protected-page.js - * import { withPageAuthRequired } from '@auth0/nextjs-auth0'; + * import { auth0 } from "@/lib/auth0"; * * export default function ProtectedPage() { * return
Protected content
; * } * - * export const getServerSideProps = withPageAuthRequired(); + * export const getServerSideProps = auth0.withPageAuthRequired(); * ``` * * If the user visits `/protected-page` without a valid session, it will redirect the user to the @@ -128,9 +128,9 @@ export type WithPageAuthRequiredAppRouterOptions = { * * ```js * // app/protected-page/page.js - * import { withPageAuthRequired } from '@auth0/nextjs-auth0'; + * import { auth0 } from "@/lib/auth0"; * - * const ProtectedPage = withPageAuthRequired(async function ProtectedPage() { + * const ProtectedPage = auth0.withPageAuthRequired(async function ProtectedPage() { * return
Protected content
; * }, { returnTo: '/protected-page' }); * @@ -143,22 +143,24 @@ export type WithPageAuthRequiredAppRouterOptions = { * Note: Server Components are not aware of the req or the url of the page. So if you want the user to return to the * page after login, you must specify the `returnTo` option. * - * You can specify a function to `returnTo` that accepts the `params` (An object containing the dynamic - * route parameters) and `searchParams` (An object containing the search parameters of the current URL) + * You can specify a function to `returnTo` that accepts the `params` (A Promise that resolves to + * an object containing the dynamic route parameters) and `searchParams` (A Promise that resolves to an + * object containing the search parameters of the current URL) * argument from the page, to preserve dynamic routes and search params. * * ```js * // app/protected-page/[slug]/page.js - * import { AppRouterPageRouteOpts, withPageAuthRequired } from '@auth0/nextjs-auth0'; + * import { AppRouterPageRouteOpts } from '@auth0/nextjs-auth0/server'; + * import { auth0 } from "@/lib/auth0"; * - * const ProtectedPage = withPageAuthRequired(async function ProtectedPage({ + * const ProtectedPage = auth0.withPageAuthRequired(async function ProtectedPage({ * params, searchParams * }: AppRouterPageRouteOpts) { - * const slug = params?.slug as string; + * const slug = (await params)?.slug as string; * return
Protected content for {slug}
; * }, { * returnTo({ params }) { - * return `/protected-page/${params?.slug}`; + * return `/protected-page/${(await params)?.slug}`; * } * }); * diff --git a/src/server/index.ts b/src/server/index.ts index 234311a8..ad9f66cd 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -7,3 +7,15 @@ export { TransactionStore } from "./transaction-store.js"; export { AbstractSessionStore } from "./session/abstract-session-store.js"; export { filterDefaultIdTokenClaims, DEFAULT_ID_TOKEN_CLAIMS } from "./user.js"; + +export { + GetServerSidePropsResultWithSession, + WithPageAuthRequired, + WithPageAuthRequiredPageRouterOptions, + WithPageAuthRequiredAppRouterOptions, + PageRoute, + AppRouterPageRouteOpts, + AppRouterPageRoute, + WithPageAuthRequiredPageRouter, + WithPageAuthRequiredAppRouter +} from "./helpers/with-page-auth-required.js"; From 210dd5f1632a4ca118302ddfad0778d2338abbff Mon Sep 17 00:00:00 2001 From: guabu <135956181+guabu@users.noreply.github.com> Date: Tue, 1 Jul 2025 19:07:17 +0200 Subject: [PATCH 4/5] chore: add examples --- EXAMPLES.md | 40 +++++++++++++++++++ .../helpers/with-page-auth-required.test.ts | 1 - src/server/helpers/with-page-auth-required.ts | 2 +- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/EXAMPLES.md b/EXAMPLES.md index e79e5656..27ae07c2 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -9,6 +9,9 @@ - [On the server (App Router)](#on-the-server-app-router) - [On the server (Pages Router)](#on-the-server-pages-router) - [Middleware](#middleware) +- [Protecting a Server-Side Rendered (SSR) Page](#protecting-a-server-side-rendered-ssr-page) + - [Page Router](#page-router) + - [App Router](#app-router) - [Protecting a Client-Side Rendered (CSR) Page](#protecting-a-client-side-rendered-csr-page) - [Accessing the idToken](#accessing-the-idtoken) - [Updating the session](#updating-the-session) @@ -201,6 +204,43 @@ export async function middleware(request: NextRequest) { > [!IMPORTANT] > The `request` object must be passed as a parameter to the `getSession(request)` method when called from a middleware to ensure that any updates to the session can be read within the same request. +## Protecting a Server-Side Rendered (SSR) Page + +#### Page Router + +Requests to `/pages/profile` without a valid session cookie will be redirected to the login page. + +```jsx +// pages/profile.js +import { auth0 } from "@/lib/auth0"; + +export default function Profile({ user }) { + return
Hello {user.name}
; +} + +// You can optionally pass your own `getServerSideProps` function into +// `withPageAuthRequired` and the props will be merged with the `user` prop +export const getServerSideProps = auth0.withPageAuthRequired(); +``` + +#### App Router + +Requests to `/profile` without a valid session cookie will be redirected to the login page. + +```jsx +// app/profile/page.js +import { auth0 } from "@/lib/auth0"; + +export default auth0.withPageAuthRequired( + async function Profile() { + const { user } = await auth0.getSession(); + return
Hello {user.name}
; + }, + { returnTo: "/profile" } +); +// You need to provide a `returnTo` since Server Components aren't aware of the page's URL +``` + ## Protecting a Client-Side Rendered (CSR) Page To protect a Client-Side Rendered (CSR) page, you can use the `withPageAuthRequired` higher-order function. Requests to `/profile` without a valid session cookie will be redirected to the login page. diff --git a/src/server/helpers/with-page-auth-required.test.ts b/src/server/helpers/with-page-auth-required.test.ts index 416d19ec..e9c7b342 100644 --- a/src/server/helpers/with-page-auth-required.test.ts +++ b/src/server/helpers/with-page-auth-required.test.ts @@ -5,7 +5,6 @@ import { redirect } from "next/navigation.js"; import ReactDOMServer from "react-dom/server"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { getDefaultRoutes } from "../../test/defaults.js"; import { generateSecret } from "../../test/utils.js"; import { Auth0Client } from "../client.js"; import { RequestCookies } from "../cookies.js"; diff --git a/src/server/helpers/with-page-auth-required.ts b/src/server/helpers/with-page-auth-required.ts index bdccac3b..41af2dfe 100644 --- a/src/server/helpers/with-page-auth-required.ts +++ b/src/server/helpers/with-page-auth-required.ts @@ -5,7 +5,6 @@ import { GetServerSidePropsContext, GetServerSidePropsResult } from "next"; -import { redirect } from "next/navigation.js"; import { User } from "../../types/index.js"; import { Auth0Client } from "../client.js"; @@ -195,6 +194,7 @@ export const appRouteHandlerFactory = typeof opts.returnTo === "function" ? await opts.returnTo(params) : opts.returnTo; + const { redirect } = await import("next/navigation.js"); redirect( `${config.loginUrl}${opts.returnTo ? `?returnTo=${returnTo}` : ""}` ); From a2145dccfb10ad1d69170a9a25d44d84ddc45f2c Mon Sep 17 00:00:00 2001 From: guabu <135956181+guabu@users.noreply.github.com> Date: Mon, 7 Jul 2025 19:17:27 +0300 Subject: [PATCH 5/5] chore: update tests to include required routes --- src/server/logout-strategy.flow.test.ts | 31 +++++++++++++++++-------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/server/logout-strategy.flow.test.ts b/src/server/logout-strategy.flow.test.ts index 1d5bb9b1..a1267f43 100644 --- a/src/server/logout-strategy.flow.test.ts +++ b/src/server/logout-strategy.flow.test.ts @@ -11,6 +11,7 @@ import { it } from "vitest"; +import { getDefaultRoutes } from "../test/defaults.js"; import { generateSecret } from "../test/utils.js"; import type { SessionData } from "../types/index.js"; import { AuthClient } from "./auth-client.js"; @@ -108,7 +109,8 @@ describe("Logout Strategy Flow Tests", () => { secret, transactionStore, sessionStore, - logoutStrategy: "auto" + logoutStrategy: "auto", + routes: getDefaultRoutes() }); const session: SessionData = { @@ -166,7 +168,8 @@ describe("Logout Strategy Flow Tests", () => { secret, transactionStore, sessionStore, - logoutStrategy: "auto" + logoutStrategy: "auto", + routes: getDefaultRoutes() }); const request = new NextRequest( @@ -198,7 +201,8 @@ describe("Logout Strategy Flow Tests", () => { secret, transactionStore, sessionStore, - logoutStrategy: "auto" + logoutStrategy: "auto", + routes: getDefaultRoutes() }); const returnToUrl = "http://localhost:3000/custom-page"; @@ -232,7 +236,8 @@ describe("Logout Strategy Flow Tests", () => { secret, transactionStore, sessionStore, - logoutStrategy: "oidc" + logoutStrategy: "oidc", + routes: getDefaultRoutes() }); const session: SessionData = { @@ -283,7 +288,8 @@ describe("Logout Strategy Flow Tests", () => { secret, transactionStore, sessionStore, - logoutStrategy: "oidc" + logoutStrategy: "oidc", + routes: getDefaultRoutes() }); const request = new NextRequest( @@ -312,7 +318,8 @@ describe("Logout Strategy Flow Tests", () => { secret, transactionStore, sessionStore, - logoutStrategy: "v2" + logoutStrategy: "v2", + routes: getDefaultRoutes() }); const session: SessionData = { @@ -362,7 +369,8 @@ describe("Logout Strategy Flow Tests", () => { secret, transactionStore, sessionStore, - logoutStrategy: "v2" + logoutStrategy: "v2", + routes: getDefaultRoutes() }); const wildcardUrl = "http://localhost:3000/*/about"; @@ -393,7 +401,8 @@ describe("Logout Strategy Flow Tests", () => { secret, transactionStore, sessionStore, - logoutStrategy: "v2" + logoutStrategy: "v2", + routes: getDefaultRoutes() }); const request = new NextRequest( @@ -427,7 +436,8 @@ describe("Logout Strategy Flow Tests", () => { secret, transactionStore, sessionStore, - logoutStrategy: strategy + logoutStrategy: strategy, + routes: getDefaultRoutes() }); const session: SessionData = { @@ -480,7 +490,8 @@ describe("Logout Strategy Flow Tests", () => { secret, transactionStore, sessionStore, - logoutStrategy: "auto" + logoutStrategy: "auto", + routes: getDefaultRoutes() }); const request = new NextRequest(