Repository: auth0/nextjs-auth0 (ts)

Issue:
withPageAuthRequired returnTo path with basePath [SDK-2373]
### Description

Using `withPageAuthRequired()` with the `basePath` configured in the `next.config.js` doesn't redirect the user to the expected page. HOC uses the `router.asPath` prop: https://github.com/auth0/nextjs-auth0/blob/main/src/frontend/with-page-auth-required.tsx#L76

### Reproduction

- Set your `basePath` to `/app`
- Wrap a page with `withPageAuthRequired`
- Go to the protected page and login
- You should be redirected to `/` instead of `/app`

Here's a log of the router object from the `withPageAuthRequired()`:
```
{
  "pathname": "/",
  "route": "/",
  "query": {},
  "asPath": "/",
  "components": {
    "/": {
      "initial": true,
      "props": {
        "pageProps": {}
      }
    },
    "/_app": {
      "styleSheets": []
    }
  },
  "isFallback": false,
  "basePath": "/app",
  "isReady": true,
  "events": {}
}
```

### Environment

- **Version of this library used:** 1.1.0
- **Version of the platform or framework used, if applicable:** nextjs 10.0.7
- **Other relevant versions (language, server software, OS, browser):** macOS
- **Other modules/plugins/libraries that might be involved:**


Pull request description:
Fix missing basePath [SDK-2373]
### Description

This PR prepends the `returnTo` URL in the client-side `withPageAuthRequired` with the `basePath`, if there's one configured. Also the `router.push` call was replaced with `window.location.assign`.

### References

Fixes #312

### Testing

- [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
- [X] 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/with-page-auth-required.tsx b/src/frontend/with-page-auth-required.tsx
index 59b79a55..9ac11037 100644
--- a/src/frontend/with-page-auth-required.tsx
+++ b/src/frontend/with-page-auth-required.tsx
@@ -62,6 +62,7 @@ export interface WithPageAuthRequiredOptions {
  *
  * @category Client
  */
+// eslint-disable-next-line @typescript-eslint/ban-types
 export type WithPageAuthRequired = <P extends object>(
   Component: ComponentType<P>,
   options?: WithPageAuthRequiredOptions
@@ -73,16 +74,17 @@ export type WithPageAuthRequired = <P extends object>(
 const withPageAuthRequired: WithPageAuthRequired = (Component, options = {}) => {
   return function withPageAuthRequired(props): JSX.Element {
     const router = useRouter();
-    const { returnTo = router.asPath, onRedirecting = defaultOnRedirecting, onError = defaultOnError } = options;
+    const {
+      returnTo = `${router.basePath ?? ''}${router.asPath}`,
+      onRedirecting = defaultOnRedirecting,
+      onError = defaultOnError
+    } = options;
     const { loginUrl } = useConfig();
     const { user, error, isLoading } = useUser();
 
     useEffect(() => {
       if ((user && !error) || isLoading) return;
-
-      (async (): Promise<void> => {
-        await router.push(`${loginUrl}?returnTo=${returnTo}`);
-      })();
+      window.location.assign(`${loginUrl}?returnTo=${returnTo}`);
     }, [user, error, isLoading]);
 
     if (error) return onError(error);
diff --git a/src/handlers/callback.ts b/src/handlers/callback.ts
index fa963f76..a7353560 100644
--- a/src/handlers/callback.ts
+++ b/src/handlers/callback.ts
@@ -84,7 +84,7 @@ export type HandleCallback = (req: NextApiRequest, res: NextApiResponse, options
 /**
  * @ignore
  */
-export default function handleLoginFactory(handler: BaseHandleCallback): HandleCallback {
+export default function handleCallbackFactory(handler: BaseHandleCallback): HandleCallback {
   return async (req, res, options): Promise<void> => {
     assertReqRes(req, res);
     return handler(req, res, options);


Test patch:
diff --git a/tests/frontend/with-page-auth-required.test.tsx b/tests/frontend/with-page-auth-required.test.tsx
index a9bd9ce4..8aace4ff 100644
--- a/tests/frontend/with-page-auth-required.test.tsx
+++ b/tests/frontend/with-page-auth-required.test.tsx
@@ -8,15 +8,26 @@ import { render, screen, waitFor } from '@testing-library/react';
 import { fetchUserUnsuccessfulMock, fetchUserErrorMock, withUserProvider, user } from '../fixtures/frontend';
 import { withPageAuthRequired } from '../../src/frontend';
 
-const routerMock = {
-  push: jest.fn(),
+const windowLocation = window.location;
+const routerMock: {
+  basePath?: string;
+  asPath: string;
+} = {
+  basePath: undefined,
   asPath: '/'
 };
 
 jest.mock('next/router', () => ({ useRouter: (): any => routerMock }));
 
 describe('with-page-auth-required csr', () => {
+  beforeAll(() => {
+    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+    // @ts-ignore window.location is non-optional
+    delete window.location;
+    window.location = { ...windowLocation, assign: jest.fn() };
+  });
   afterEach(() => delete (global as any).fetch);
+  afterAll(() => (window.location = windowLocation));
 
   it('should deny access to a CSR page when not authenticated', async () => {
     (global as any).fetch = fetchUserUnsuccessfulMock;
@@ -24,7 +35,7 @@ describe('with-page-auth-required csr', () => {
     const ProtectedPage = withPageAuthRequired(MyPage);
 
     render(<ProtectedPage />, { wrapper: withUserProvider() });
-    await waitFor(() => expect(routerMock.push).toHaveBeenCalledTimes(1));
+    await waitFor(() => expect(window.location.assign).toHaveBeenCalledTimes(1));
     await waitFor(() => expect(screen.queryByText('Private')).not.toBeInTheDocument());
   });
 
@@ -33,7 +44,7 @@ describe('with-page-auth-required csr', () => {
     const ProtectedPage = withPageAuthRequired(MyPage);
 
     render(<ProtectedPage />, { wrapper: withUserProvider({ user }) });
-    await waitFor(() => expect(routerMock.push).not.toHaveBeenCalled());
+    await waitFor(() => expect(window.location.assign).not.toHaveBeenCalled());
     await waitFor(() => expect(screen.getByText('Private')).toBeInTheDocument());
   });
 
@@ -81,7 +92,7 @@ describe('with-page-auth-required csr', () => {
     const ProtectedPage = withPageAuthRequired(MyPage, { returnTo: '/foo' });
 
     render(<ProtectedPage />, { wrapper: withUserProvider() });
-    await waitFor(() => expect(routerMock.push).toHaveBeenCalledWith(expect.stringContaining('?returnTo=/foo')));
+    await waitFor(() => expect(window.location.assign).toHaveBeenCalledWith(expect.stringContaining('?returnTo=/foo')));
   });
 
   it('should use a custom login url', async () => {
@@ -91,7 +102,24 @@ describe('with-page-auth-required csr', () => {
     const ProtectedPage = withPageAuthRequired(MyPage);
 
     render(<ProtectedPage />, { wrapper: withUserProvider() });
-    await waitFor(() => expect(routerMock.push).toHaveBeenCalledWith(expect.stringContaining('/api/foo')));
+    await waitFor(() => expect(window.location.assign).toHaveBeenCalledWith(expect.stringContaining('/api/foo')));
     delete process.env.NEXT_PUBLIC_AUTH0_LOGIN;
   });
+
+  it('should prepend the basePath to the returnTo URL', async () => {
+    const asPath = routerMock.asPath;
+    const basePath = routerMock.basePath;
+    routerMock.basePath = '/foo';
+    routerMock.asPath = '/bar';
+    (global as any).fetch = fetchUserUnsuccessfulMock;
+    const MyPage = (): JSX.Element => <>Private</>;
+    const ProtectedPage = withPageAuthRequired(MyPage);
+
+    render(<ProtectedPage />, { wrapper: withUserProvider() });
+    await waitFor(() =>
+      expect(window.location.assign).toHaveBeenCalledWith(expect.stringContaining('?returnTo=/foo/bar'))
+    );
+    routerMock.basePath = basePath;
+    routerMock.asPath = asPath;
+  });
 });