Skip to content

Commit ae91e05

Browse files
authored
feat: support customizing profile and accessToken routes (#2451)
2 parents c745513 + 853e4df commit ae91e05

9 files changed

Lines changed: 301 additions & 22 deletions

File tree

EXAMPLES.md

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2895,15 +2895,23 @@ export const auth0 = new Auth0Client({
28952895
login: "/login",
28962896
logout: "/logout",
28972897
callback: "/callback",
2898-
backChannelLogout: "/backchannel-logout"
2898+
backChannelLogout: "/backchannel-logout",
2899+
profile: "/api/me",
2900+
accessToken: "/api/auth/token"
28992901
}
29002902
});
29012903
```
29022904
2903-
> [!NOTE]
2905+
> [!NOTE]
29042906
> 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.
29052907
2906-
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:
2908+
#### Configuring routes for client-side usage
2909+
2910+
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:
2911+
2912+
**Option 1: Using environment variables (recommended for most cases)**
2913+
2914+
Set the environment variables in your `.env.local` file:
29072915
29082916
```
29092917
# .env.local
@@ -2913,7 +2921,40 @@ NEXT_PUBLIC_PROFILE_ROUTE=/api/me
29132921
NEXT_PUBLIC_ACCESS_TOKEN_ROUTE=/api/auth/token
29142922
```
29152923
2916-
> [!IMPORTANT]
2924+
**Option 2: Passing routes programmatically (recommended for multi-tenant applications)**
2925+
2926+
For multi-tenant applications where routes may vary by tenant at runtime, you can pass the route directly to the client-side functions:
2927+
2928+
```tsx
2929+
import { useUser, getAccessToken, Auth0Provider } from "@auth0/nextjs-auth0/client";
2930+
2931+
// In your component
2932+
function MyComponent() {
2933+
const { user } = useUser({ route: "/tenant-a/auth/profile" });
2934+
2935+
const handleGetToken = async () => {
2936+
const token = await getAccessToken({
2937+
route: "/tenant-a/auth/access-token"
2938+
});
2939+
};
2940+
2941+
return <div>{user?.name}</div>;
2942+
}
2943+
2944+
// In your layout
2945+
export default function RootLayout({ children }) {
2946+
return (
2947+
<Auth0Provider profileRoute="/tenant-a/auth/profile">
2948+
{children}
2949+
</Auth0Provider>
2950+
);
2951+
}
2952+
```
2953+
2954+
> [!IMPORTANT]
2955+
> When using `useUser` with a custom route, ensure the `Auth0Provider` is configured with the same `profileRoute` to properly initialize the SWR cache.
2956+
2957+
> [!IMPORTANT]
29172958
> 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.
29182959
29192960
## Dynamic Application Base URLs

src/client/helpers/get-access-token.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ export const restHandlers = [
7676
}
7777

7878
return HttpResponse.json({ token });
79+
}),
80+
http.get("/custom/token", () => {
81+
return HttpResponse.json({ token: "<access_token_from_custom_route>" });
7982
})
8083
];
8184

@@ -168,6 +171,36 @@ describe("getAccessToken", () => {
168171
).rejects.toThrowError("The request is missing a required parameter.");
169172
});
170173

174+
it("should use custom route when provided", async () => {
175+
const result = await getAccessToken({
176+
route: "/custom/token"
177+
});
178+
179+
expect(result).toBe("<access_token_from_custom_route>");
180+
});
181+
182+
it("should use custom route with audience and scope", async () => {
183+
server.use(
184+
http.get("/custom/token", ({ request }) => {
185+
const url = new URL(request.url);
186+
const audience = url.searchParams.get("audience");
187+
const scope = url.searchParams.get("scope");
188+
189+
return HttpResponse.json({
190+
token: `<custom_token_${audience}_${scope}>`
191+
});
192+
})
193+
);
194+
195+
const result = await getAccessToken({
196+
route: "/custom/token",
197+
audience: "custom_audience",
198+
scope: "read:custom"
199+
});
200+
201+
expect(result).toBe("<custom_token_custom_audience_read:custom>");
202+
});
203+
171204
describe("mergeScopes", () => {
172205
it("should append mergeScopes=false to URL when mergeScopes is false", async () => {
173206
let capturedUrl: URL | undefined;

src/client/helpers/get-access-token.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ import { normalizeWithBasePath } from "../../utils/pathUtils.js";
2121
* const ordersToken = await getAccessToken({
2222
* audience: 'https://orders-api.example.com'
2323
* });
24+
*
25+
* // Custom route - useful for multi-tenant applications
26+
* const token = await getAccessToken({
27+
* route: '/tenant-a/auth/access-token'
28+
* });
2429
* ```
2530
*/
2631
export type AccessTokenOptions = {
@@ -50,6 +55,15 @@ export type AccessTokenOptions = {
5055
*/
5156
audience?: string;
5257

58+
/**
59+
* Custom route for the access token endpoint.
60+
* Useful for multi-tenant applications where different tenants require different route configurations.
61+
* If not specified, falls back to the NEXT_PUBLIC_ACCESS_TOKEN_ROUTE environment variable or "/auth/access-token".
62+
*
63+
* @example '/tenant-a/auth/access-token'
64+
*/
65+
route?: string;
66+
5367
/**
5468
* When true, returns the full response from the `/auth/access-token` endpoint
5569
* instead of only the access token string.
@@ -141,7 +155,9 @@ export async function getAccessToken(
141155
}
142156

143157
let url = normalizeWithBasePath(
144-
process.env.NEXT_PUBLIC_ACCESS_TOKEN_ROUTE || "/auth/access-token"
158+
options.route ||
159+
process.env.NEXT_PUBLIC_ACCESS_TOKEN_ROUTE ||
160+
"/auth/access-token"
145161
);
146162

147163
// Only append the query string if we have any url parameters to add

src/client/hooks/use-user.integration.test.tsx

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,4 +170,79 @@ describe("useUser Integration with SWR Cache", () => {
170170
expect(fetchSpy).toHaveBeenCalledOnce();
171171
expect(fetchSpy).toHaveBeenCalledWith("/auth/profile");
172172
});
173+
174+
it("should use custom route when provided", async () => {
175+
fetchSpy.mockResolvedValueOnce(
176+
new Response(JSON.stringify(initialUser), {
177+
status: 200,
178+
headers: { "Content-Type": "application/json" }
179+
})
180+
);
181+
182+
const wrapper = ({ children }: { children: React.ReactNode }) => (
183+
<swrModule.SWRConfig value={{ provider: () => new Map() }}>
184+
{children}
185+
</swrModule.SWRConfig>
186+
);
187+
188+
const { result } = renderHook(() => useUser({ route: "/custom/profile" }), {
189+
wrapper
190+
});
191+
192+
// Wait for the initial data to load
193+
await waitFor(() => expect(result.current.isLoading).toBe(false));
194+
195+
expect(result.current.user).toEqual(initialUser);
196+
expect(result.current.error).toBe(null);
197+
expect(fetchSpy).toHaveBeenCalledOnce();
198+
expect(fetchSpy).toHaveBeenCalledWith("/custom/profile");
199+
});
200+
201+
it("should use custom route and invalidate correctly", async () => {
202+
// Mock fetch to return initial data first
203+
fetchSpy.mockResolvedValueOnce(
204+
new Response(JSON.stringify(initialUser), {
205+
status: 200,
206+
headers: { "Content-Type": "application/json" }
207+
})
208+
);
209+
210+
const wrapper = ({ children }: { children: React.ReactNode }) => (
211+
<swrModule.SWRConfig value={{ provider: () => new Map() }}>
212+
{children}
213+
</swrModule.SWRConfig>
214+
);
215+
216+
const { result } = renderHook(
217+
() => useUser({ route: "/tenant-a/auth/profile" }),
218+
{ wrapper }
219+
);
220+
221+
// Wait for the initial data to load
222+
await waitFor(() => expect(result.current.isLoading).toBe(false));
223+
224+
expect(result.current.user).toEqual(initialUser);
225+
expect(fetchSpy).toHaveBeenCalledWith("/tenant-a/auth/profile");
226+
227+
// Mock fetch to return updated data for the next call
228+
fetchSpy.mockResolvedValueOnce(
229+
new Response(JSON.stringify(updatedUser), {
230+
status: 200,
231+
headers: { "Content-Type": "application/json" }
232+
})
233+
);
234+
235+
// Call invalidate to trigger re-fetch
236+
await act(async () => {
237+
result.current.invalidate();
238+
});
239+
240+
// Wait for the hook to reflect the updated data
241+
await waitFor(() => expect(result.current.user).toEqual(updatedUser));
242+
243+
// Assert both fetch calls used the custom route
244+
expect(fetchSpy).toHaveBeenCalledTimes(2);
245+
expect(fetchSpy).toHaveBeenNthCalledWith(1, "/tenant-a/auth/profile");
246+
expect(fetchSpy).toHaveBeenNthCalledWith(2, "/tenant-a/auth/profile");
247+
});
173248
});

src/client/hooks/use-user.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,24 @@ import useSWR from "swr";
55
import type { User } from "../../types/index.js";
66
import { normalizeWithBasePath } from "../../utils/pathUtils.js";
77

8-
export function useUser() {
8+
/**
9+
* Options for the useUser hook.
10+
*/
11+
export type UseUserOptions = {
12+
/**
13+
* Custom route for the profile endpoint.
14+
* Useful for multi-tenant applications where different tenants require different route configurations.
15+
* If not specified, falls back to the NEXT_PUBLIC_PROFILE_ROUTE environment variable or "/auth/profile".
16+
*
17+
* @example '/tenant-a/auth/profile'
18+
*/
19+
route?: string;
20+
};
21+
22+
export function useUser(options: UseUserOptions = {}) {
923
const { data, error, isLoading, mutate } = useSWR<User, Error, string>(
1024
normalizeWithBasePath(
11-
process.env.NEXT_PUBLIC_PROFILE_ROUTE || "/auth/profile"
25+
options.route || process.env.NEXT_PUBLIC_PROFILE_ROUTE || "/auth/profile"
1226
),
1327
(...args) =>
1428
fetch(...args).then((res) => {

src/client/index.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1-
export { useUser } from "./hooks/use-user.js";
2-
export { getAccessToken } from "./helpers/get-access-token.js";
1+
export { useUser, type UseUserOptions } from "./hooks/use-user.js";
2+
export {
3+
getAccessToken,
4+
type AccessTokenOptions
5+
} from "./helpers/get-access-token.js";
36
export {
47
withPageAuthRequired,
58
WithPageAuthRequired,
69
WithPageAuthRequiredOptions
710
} from "./helpers/with-page-auth-required.js";
8-
export { Auth0Provider } from "./providers/auth0-provider.js";
11+
export {
12+
Auth0Provider,
13+
type Auth0ProviderProps
14+
} from "./providers/auth0-provider.js";
915
export { mfa } from "./mfa/index.js";
1016
export type { ChallengeWithPopupOptions } from "./mfa/index.js";
1117
export type { AccessTokenResponse } from "./helpers/get-access-token.js";

src/client/providers/auth0-provider.tsx

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,41 @@ import { SWRConfig } from "swr";
55

66
import { User } from "../../types/index.js";
77

8-
export function Auth0Provider({
9-
user,
10-
children
11-
}: {
8+
/**
9+
* Props for the Auth0Provider component.
10+
*/
11+
export type Auth0ProviderProps = {
12+
/**
13+
* Initial user data to populate the SWR cache.
14+
*/
1215
user?: User;
16+
/**
17+
* Child components to render within the provider.
18+
*/
1319
children: React.ReactNode;
14-
}) {
20+
/**
21+
* Custom route for the profile endpoint.
22+
* Useful for multi-tenant applications where different tenants require different route configurations.
23+
* If not specified, falls back to the NEXT_PUBLIC_PROFILE_ROUTE environment variable or "/auth/profile".
24+
*
25+
* @example '/tenant-a/auth/profile'
26+
*/
27+
profileRoute?: string;
28+
};
29+
30+
export function Auth0Provider({
31+
user,
32+
children,
33+
profileRoute
34+
}: Auth0ProviderProps) {
35+
const route =
36+
profileRoute || process.env.NEXT_PUBLIC_PROFILE_ROUTE || "/auth/profile";
37+
1538
return (
1639
<SWRConfig
1740
value={{
1841
fallback: {
19-
[process.env.NEXT_PUBLIC_PROFILE_ROUTE || "/auth/profile"]: user
42+
[route]: user
2043
}
2144
}}
2245
>

0 commit comments

Comments
 (0)