Repository: auth0/nextjs-auth0
Pull request #422 (2021-06-22 17:04:34)

Support cross domain credentials
### Description

I have two or more Next.js applications which are deployed independently. They provide different services to users, and live under different subdomains e.g. `login.mywebapp.com` and `people.mywebapp.com`.  For the user this is a single web application with a single session.

I'd like for the "login" web application to implement the nextjs-auth0 APIs, and have the other applications make use nextjs-auth0 frontend helpers without needing to implement the APIs as well. The other applications should use the APIs under the "login" sub domain.

I can almost make this work by 
- Setting the environment variables `NEXT_PUBLIC_AUTH0_LOGIN` and `NEXT_PUBLIC_AUTH0_PROFILE` in all web apps
- Setting the header `Access-Control-Allow-Origin` and `Access-Control-Allow-Credentials` in the API handler in the login web app.
- Setting the session.cookie.domain as the `.mywebapp.com` so that it is shared across sub domains.

But the cookie is not sent with the request to `https://login.mywebapp.com/api/auth/me`, returning an "unauthorised" response.

This PR makes it possible to send the auth cookie across sub domains by setting the `{ credentials: 'include' }` option in the `checkSession()` fetch call.

### Testing

- Set the environment variable `NEXT_PUBLIC_AUTH0_PROFILE` to a different URL, and add any cookie to that domain.  Upon loading the page a request will be sent to `https://login.mywebapp.com/api/auth/me` with the cookie sent on the request.

- [x] This change adds test coverage for new/changed/fixed functionality

### Checklist

- [ ] I have added documentation for new/changed functionality in this PR or in auth0.com/docs
- [ ] All active GitHub checks for tests, formatting, and security are passing
- [ ] The correct base branch is being used, if not `main`



Patch:
diff --git a/src/frontend/use-user.tsx b/src/frontend/use-user.tsx
index c3076d89..46cd5812 100644
--- a/src/frontend/use-user.tsx
+++ b/src/frontend/use-user.tsx
@@ -30,6 +30,11 @@ export type UserContext = {
   checkSession: () => Promise<void>;
 };
 
+/**
+ * @ignore
+ */
+type UserFetcher = (url: string) => Promise<UserProfile | undefined>;
+
 /**
  * Configure the {@link UserProvider} component.
  *
@@ -61,7 +66,9 @@ export type UserContext = {
  *
  * @category Client
  */
-export type UserProviderProps = React.PropsWithChildren<{ user?: UserProfile; profileUrl?: string } & ConfigContext>;
+export type UserProviderProps = React.PropsWithChildren<
+  { user?: UserProfile; profileUrl?: string; fetcher?: UserFetcher } & ConfigContext
+>;
 
 /**
  * @ignore
@@ -130,18 +137,26 @@ type UserProviderState = {
   isLoading: boolean;
 };
 
+/**
+ * @ignore
+ */
+const userFetcher: UserFetcher = async (url) => {
+  const response = await fetch(url);
+  return response.ok ? response.json() : undefined;
+};
+
 export default ({
   children,
   user: initialUser,
   profileUrl = process.env.NEXT_PUBLIC_AUTH0_PROFILE || '/api/auth/me',
-  loginUrl
+  loginUrl,
+  fetcher = userFetcher
 }: UserProviderProps): ReactElement<UserContext> => {
   const [state, setState] = useState<UserProviderState>({ user: initialUser, isLoading: !initialUser });
 
   const checkSession = useCallback(async (): Promise<void> => {
     try {
-      const response = await fetch(profileUrl);
-      const user = response.ok ? await response.json() : undefined;
+      const user = await fetcher(profileUrl);
       setState((previous) => ({ ...previous, user, error: undefined }));
     } catch (_e) {
       const error = new Error(`The request to ${profileUrl} failed`);


Test patch:
diff --git a/tests/fixtures/frontend.tsx b/tests/fixtures/frontend.tsx
index 5f0c1883..bb9599ff 100644
--- a/tests/fixtures/frontend.tsx
+++ b/tests/fixtures/frontend.tsx
@@ -18,9 +18,14 @@ export const user: UserProfile = {
   updated_at: null
 };
 
-export const withUserProvider = ({ user, profileUrl, loginUrl }: UserProviderProps = {}): React.ComponentType => {
+export const withUserProvider = ({
+  user,
+  profileUrl,
+  loginUrl,
+  fetcher
+}: UserProviderProps = {}): React.ComponentType => {
   return (props: any): React.ReactElement => (
-    <UserProvider {...props} user={user} profileUrl={profileUrl} loginUrl={loginUrl} />
+    <UserProvider {...props} user={user} profileUrl={profileUrl} loginUrl={loginUrl} fetcher={fetcher} />
   );
 };
 
diff --git a/tests/frontend/use-user.test.tsx b/tests/frontend/use-user.test.tsx
index 516d5e59..1ac9ad89 100644
--- a/tests/frontend/use-user.test.tsx
+++ b/tests/frontend/use-user.test.tsx
@@ -157,4 +157,22 @@ describe('context wrapper', () => {
 
     expect(result.current.user).toEqual({ foo: 'bar' });
   });
+
+  test('should use the override fetch behaviour', async () => {
+    const fetchSpy = jest.fn();
+    (global as any).fetch = fetchSpy;
+
+    const returnValue = 'foo';
+    const customFetcher = jest.fn().mockReturnValue(Promise.resolve(returnValue));
+
+    const { result, waitForValueToChange } = renderHook(() => useUser(), {
+      wrapper: withUserProvider({ fetcher: customFetcher })
+    });
+
+    await waitForValueToChange(() => result.current.isLoading);
+
+    expect(fetchSpy).not.toHaveBeenCalled();
+    expect(customFetcher).toHaveBeenCalledWith('/api/auth/me');
+    expect(result.current.user).toBe(returnValue);
+  });
 });