diff --git a/EXAMPLES.md b/EXAMPLES.md
index 1f5d662b9..3446c676c 100644
--- a/EXAMPLES.md
+++ b/EXAMPLES.md
@@ -2622,15 +2622,23 @@ export const auth0 = new Auth0Client({
login: "/login",
logout: "/logout",
callback: "/callback",
- backChannelLogout: "/backchannel-logout"
+ backChannelLogout: "/backchannel-logout",
+ profile: "/api/me",
+ accessToken: "/api/auth/token"
}
});
```
-> [!NOTE]
+> [!NOTE]
> If you customize the login url you will need to set the environment variable `NEXT_PUBLIC_LOGIN_ROUTE` to this custom value for `withPageAuthRequired` to work correctly.
-To configure the profile and access token routes, you must use the `NEXT_PUBLIC_PROFILE_ROUTE` and `NEXT_PUBLIC_ACCESS_TOKEN_ROUTE`, respectively. For example:
+#### Configuring routes for client-side usage
+
+When customizing the `profile` and `accessToken` routes, you need to ensure that client-side functions (`useUser`, `getAccessToken`) and the `Auth0Provider` use the correct routes. There are two approaches:
+
+**Option 1: Using environment variables (recommended for most cases)**
+
+Set the environment variables in your `.env.local` file:
```
# .env.local
@@ -2640,7 +2648,40 @@ NEXT_PUBLIC_PROFILE_ROUTE=/api/me
NEXT_PUBLIC_ACCESS_TOKEN_ROUTE=/api/auth/token
```
-> [!IMPORTANT]
+**Option 2: Passing routes programmatically (recommended for multi-tenant applications)**
+
+For multi-tenant applications where routes may vary by tenant at runtime, you can pass the route directly to the client-side functions:
+
+```tsx
+import { useUser, getAccessToken, Auth0Provider } from "@auth0/nextjs-auth0/client";
+
+// In your component
+function MyComponent() {
+ const { user } = useUser({ route: "/tenant-a/auth/profile" });
+
+ const handleGetToken = async () => {
+ const token = await getAccessToken({
+ route: "/tenant-a/auth/access-token"
+ });
+ };
+
+ return
{user?.name}
;
+}
+
+// In your layout
+export default function RootLayout({ children }) {
+ return (
+
+ {children}
+
+ );
+}
+```
+
+> [!IMPORTANT]
+> When using `useUser` with a custom route, ensure the `Auth0Provider` is configured with the same `profileRoute` to properly initialize the SWR cache.
+
+> [!IMPORTANT]
> Updating the route paths will also require updating the **Allowed Callback URLs** and **Allowed Logout URLs** configured in the [Auth0 Dashboard](https://manage.auth0.com) for your client.
## Testing helpers
diff --git a/src/client/helpers/get-access-token.test.ts b/src/client/helpers/get-access-token.test.ts
index 9ec96e8c1..5adb1d30d 100644
--- a/src/client/helpers/get-access-token.test.ts
+++ b/src/client/helpers/get-access-token.test.ts
@@ -52,6 +52,9 @@ export const restHandlers = [
}
return HttpResponse.json({ token });
+ }),
+ http.get("/custom/token", () => {
+ return HttpResponse.json({ token: "" });
})
];
@@ -119,4 +122,34 @@ describe("getAccessToken", () => {
})
).rejects.toThrowError("The request is missing a required parameter.");
});
+
+ it("should use custom route when provided", async () => {
+ const result = await getAccessToken({
+ route: "/custom/token"
+ });
+
+ expect(result).toBe("");
+ });
+
+ it("should use custom route with audience and scope", async () => {
+ server.use(
+ http.get("/custom/token", ({ request }) => {
+ const url = new URL(request.url);
+ const audience = url.searchParams.get("audience");
+ const scope = url.searchParams.get("scope");
+
+ return HttpResponse.json({
+ token: ``
+ });
+ })
+ );
+
+ const result = await getAccessToken({
+ route: "/custom/token",
+ audience: "custom_audience",
+ scope: "read:custom"
+ });
+
+ expect(result).toBe("");
+ });
});
diff --git a/src/client/helpers/get-access-token.ts b/src/client/helpers/get-access-token.ts
index 30e1c5f29..dede90d82 100644
--- a/src/client/helpers/get-access-token.ts
+++ b/src/client/helpers/get-access-token.ts
@@ -21,6 +21,11 @@ import { normalizeWithBasePath } from "../../utils/pathUtils.js";
* const ordersToken = await getAccessToken({
* audience: 'https://orders-api.example.com'
* });
+ *
+ * // Custom route - useful for multi-tenant applications
+ * const token = await getAccessToken({
+ * route: '/tenant-a/auth/access-token'
+ * });
* ```
*/
export type AccessTokenOptions = {
@@ -49,6 +54,15 @@ export type AccessTokenOptions = {
* @example 'https://orders-api.mycompany.com'
*/
audience?: string;
+
+ /**
+ * Custom route for the access token endpoint.
+ * Useful for multi-tenant applications where different tenants require different route configurations.
+ * If not specified, falls back to the NEXT_PUBLIC_ACCESS_TOKEN_ROUTE environment variable or "/auth/access-token".
+ *
+ * @example '/tenant-a/auth/access-token'
+ */
+ route?: string;
};
type AccessTokenResponse = {
@@ -81,7 +95,9 @@ export async function getAccessToken(
}
let url = normalizeWithBasePath(
- process.env.NEXT_PUBLIC_ACCESS_TOKEN_ROUTE || "/auth/access-token"
+ options.route ||
+ process.env.NEXT_PUBLIC_ACCESS_TOKEN_ROUTE ||
+ "/auth/access-token"
);
// Only append the query string if we have any url parameters to add
diff --git a/src/client/helpers/with-page-auth-required.test.tsx b/src/client/helpers/with-page-auth-required.test.tsx
index 74529a859..e5cc26f23 100644
--- a/src/client/helpers/with-page-auth-required.test.tsx
+++ b/src/client/helpers/with-page-auth-required.test.tsx
@@ -250,9 +250,8 @@ describe("with-page-auth-required csr", () => {
expect(window.location.assign).toHaveBeenCalled();
});
const url = new URL(
- (
- window.location.assign as MockedFunction
- ).mock.calls[0][0],
+ (window.location.assign as MockedFunction)
+ .mock.calls[0][0],
"https://example.com"
);
expect(url.searchParams.get("returnTo")).toEqual("/foo?bar=baz&qux=quux");
diff --git a/src/client/hooks/use-user.integration.test.tsx b/src/client/hooks/use-user.integration.test.tsx
index c6087f55e..c4e895e2c 100644
--- a/src/client/hooks/use-user.integration.test.tsx
+++ b/src/client/hooks/use-user.integration.test.tsx
@@ -170,4 +170,79 @@ describe("useUser Integration with SWR Cache", () => {
expect(fetchSpy).toHaveBeenCalledOnce();
expect(fetchSpy).toHaveBeenCalledWith("/auth/profile");
});
+
+ it("should use custom route when provided", async () => {
+ fetchSpy.mockResolvedValueOnce(
+ new Response(JSON.stringify(initialUser), {
+ status: 200,
+ headers: { "Content-Type": "application/json" }
+ })
+ );
+
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
+ new Map() }}>
+ {children}
+
+ );
+
+ const { result } = renderHook(() => useUser({ route: "/custom/profile" }), {
+ wrapper
+ });
+
+ // Wait for the initial data to load
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
+
+ expect(result.current.user).toEqual(initialUser);
+ expect(result.current.error).toBe(null);
+ expect(fetchSpy).toHaveBeenCalledOnce();
+ expect(fetchSpy).toHaveBeenCalledWith("/custom/profile");
+ });
+
+ it("should use custom route and invalidate correctly", async () => {
+ // Mock fetch to return initial data first
+ fetchSpy.mockResolvedValueOnce(
+ new Response(JSON.stringify(initialUser), {
+ status: 200,
+ headers: { "Content-Type": "application/json" }
+ })
+ );
+
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
+ new Map() }}>
+ {children}
+
+ );
+
+ const { result } = renderHook(
+ () => useUser({ route: "/tenant-a/auth/profile" }),
+ { wrapper }
+ );
+
+ // Wait for the initial data to load
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
+
+ expect(result.current.user).toEqual(initialUser);
+ expect(fetchSpy).toHaveBeenCalledWith("/tenant-a/auth/profile");
+
+ // Mock fetch to return updated data for the next call
+ fetchSpy.mockResolvedValueOnce(
+ new Response(JSON.stringify(updatedUser), {
+ status: 200,
+ headers: { "Content-Type": "application/json" }
+ })
+ );
+
+ // Call invalidate to trigger re-fetch
+ await act(async () => {
+ result.current.invalidate();
+ });
+
+ // Wait for the hook to reflect the updated data
+ await waitFor(() => expect(result.current.user).toEqual(updatedUser));
+
+ // Assert both fetch calls used the custom route
+ expect(fetchSpy).toHaveBeenCalledTimes(2);
+ expect(fetchSpy).toHaveBeenNthCalledWith(1, "/tenant-a/auth/profile");
+ expect(fetchSpy).toHaveBeenNthCalledWith(2, "/tenant-a/auth/profile");
+ });
});
diff --git a/src/client/hooks/use-user.ts b/src/client/hooks/use-user.ts
index 1358a36a0..ccec6dabd 100644
--- a/src/client/hooks/use-user.ts
+++ b/src/client/hooks/use-user.ts
@@ -5,10 +5,24 @@ import useSWR from "swr";
import type { User } from "../../types/index.js";
import { normalizeWithBasePath } from "../../utils/pathUtils.js";
-export function useUser() {
+/**
+ * Options for the useUser hook.
+ */
+export type UseUserOptions = {
+ /**
+ * Custom route for the profile endpoint.
+ * Useful for multi-tenant applications where different tenants require different route configurations.
+ * If not specified, falls back to the NEXT_PUBLIC_PROFILE_ROUTE environment variable or "/auth/profile".
+ *
+ * @example '/tenant-a/auth/profile'
+ */
+ route?: string;
+};
+
+export function useUser(options: UseUserOptions = {}) {
const { data, error, isLoading, mutate } = useSWR(
normalizeWithBasePath(
- process.env.NEXT_PUBLIC_PROFILE_ROUTE || "/auth/profile"
+ options.route || process.env.NEXT_PUBLIC_PROFILE_ROUTE || "/auth/profile"
),
(...args) =>
fetch(...args).then((res) => {
diff --git a/src/client/index.ts b/src/client/index.ts
index 5ee646139..e4b6f3624 100644
--- a/src/client/index.ts
+++ b/src/client/index.ts
@@ -1,8 +1,14 @@
-export { useUser } from "./hooks/use-user.js";
-export { getAccessToken } from "./helpers/get-access-token.js";
+export { useUser, type UseUserOptions } from "./hooks/use-user.js";
+export {
+ getAccessToken,
+ type AccessTokenOptions
+} from "./helpers/get-access-token.js";
export {
withPageAuthRequired,
WithPageAuthRequired,
WithPageAuthRequiredOptions
} from "./helpers/with-page-auth-required.js";
-export { Auth0Provider } from "./providers/auth0-provider.js";
+export {
+ Auth0Provider,
+ type Auth0ProviderProps
+} from "./providers/auth0-provider.js";
diff --git a/src/client/providers/auth0-provider.tsx b/src/client/providers/auth0-provider.tsx
index ad5a629bd..8d6eb6d8e 100644
--- a/src/client/providers/auth0-provider.tsx
+++ b/src/client/providers/auth0-provider.tsx
@@ -5,18 +5,41 @@ import { SWRConfig } from "swr";
import { User } from "../../types/index.js";
-export function Auth0Provider({
- user,
- children
-}: {
+/**
+ * Props for the Auth0Provider component.
+ */
+export type Auth0ProviderProps = {
+ /**
+ * Initial user data to populate the SWR cache.
+ */
user?: User;
+ /**
+ * Child components to render within the provider.
+ */
children: React.ReactNode;
-}) {
+ /**
+ * Custom route for the profile endpoint.
+ * Useful for multi-tenant applications where different tenants require different route configurations.
+ * If not specified, falls back to the NEXT_PUBLIC_PROFILE_ROUTE environment variable or "/auth/profile".
+ *
+ * @example '/tenant-a/auth/profile'
+ */
+ profileRoute?: string;
+};
+
+export function Auth0Provider({
+ user,
+ children,
+ profileRoute
+}: Auth0ProviderProps) {
+ const route =
+ profileRoute || process.env.NEXT_PUBLIC_PROFILE_ROUTE || "/auth/profile";
+
return (
diff --git a/src/server/auth-client.test.ts b/src/server/auth-client.test.ts
index 3caa4b39b..73b8fe7bc 100644
--- a/src/server/auth-client.test.ts
+++ b/src/server/auth-client.test.ts
@@ -956,6 +956,82 @@ ca/T0LLtgmbMmxSv/MmzIg==
expect(authClient.handleBackChannelLogout).toHaveBeenCalled();
});
+ it("should call the profile handler when custom route is configured via routes option", async () => {
+ const secret = await generateSecret(32);
+ const transactionStore = new TransactionStore({
+ secret
+ });
+ const sessionStore = new StatelessSessionStore({
+ secret
+ });
+ const authClient = new AuthClient({
+ transactionStore,
+ sessionStore,
+
+ domain: DEFAULT.domain,
+ clientId: DEFAULT.clientId,
+ clientSecret: DEFAULT.clientSecret,
+
+ secret,
+ appBaseUrl: DEFAULT.appBaseUrl,
+
+ fetch: getMockAuthorizationServer(),
+
+ routes: {
+ ...getDefaultRoutes(),
+ profile: "/custom-profile"
+ }
+ });
+ const request = new NextRequest(
+ new URL("/custom-profile", DEFAULT.appBaseUrl),
+ {
+ method: "GET"
+ }
+ );
+
+ authClient.handleProfile = vi.fn();
+ await authClient.handler(request);
+ expect(authClient.handleProfile).toHaveBeenCalled();
+ });
+
+ it("should call the accessToken handler when custom route is configured via routes option", async () => {
+ const secret = await generateSecret(32);
+ const transactionStore = new TransactionStore({
+ secret
+ });
+ const sessionStore = new StatelessSessionStore({
+ secret
+ });
+ const authClient = new AuthClient({
+ transactionStore,
+ sessionStore,
+
+ domain: DEFAULT.domain,
+ clientId: DEFAULT.clientId,
+ clientSecret: DEFAULT.clientSecret,
+
+ secret,
+ appBaseUrl: DEFAULT.appBaseUrl,
+
+ fetch: getMockAuthorizationServer(),
+
+ routes: {
+ ...getDefaultRoutes(),
+ accessToken: "/custom-access-token"
+ }
+ });
+ const request = new NextRequest(
+ new URL("/custom-access-token", DEFAULT.appBaseUrl),
+ {
+ method: "GET"
+ }
+ );
+
+ authClient.handleAccessToken = vi.fn();
+ await authClient.handler(request);
+ expect(authClient.handleAccessToken).toHaveBeenCalled();
+ });
+
it("should call the profile handler when the configured route is called", async () => {
process.env.NEXT_PUBLIC_PROFILE_ROUTE = "/custom-profile";
diff --git a/src/server/auth-client.ts b/src/server/auth-client.ts
index ccb56e782..0182e3b46 100644
--- a/src/server/auth-client.ts
+++ b/src/server/auth-client.ts
@@ -161,12 +161,7 @@ export interface Routes {
backChannelLogout: string;
connectAccount: string;
}
-export type RoutesOptions = Partial<
- Pick<
- Routes,
- "login" | "callback" | "logout" | "backChannelLogout" | "connectAccount"
- >
->;
+export type RoutesOptions = Partial;
export interface AuthClientOptions {
transactionStore: TransactionStore;