Repository: auth0/nextjs-auth0
Pull request #370 (2021-04-15 03:02:11)

Use `window.location.toString()` as the default `returnTo` value [SDK-2453]
### Description

This PR uses `window.location.toString()` as the default `returnTo` value, removing all remaining usages of the Next.js router.

### References

Fixes the client-side part of https://github.com/auth0/nextjs-auth0/issues/353.

### Testing

Unit tests have been added and also the change has been tested manually using a `basePath` value.

- [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`


vercel[bot]: This pull request is being automatically deployed with Vercel ([learn more](https://vercel.link/github-learn-more)).  
To see the status of your deployment, click below or on the icon next to each commit.

🔍 Inspect: [https://vercel.com/auth0/nextjs-auth0/DvKFjsQ1uP4HmHKTEwBJzLoPf1Ez](https://vercel.com/auth0/nextjs-auth0/DvKFjsQ1uP4HmHKTEwBJzLoPf1Ez)  
✅ Preview: *Canceled*



Patch:
diff --git a/src/frontend/with-page-auth-required.tsx b/src/frontend/with-page-auth-required.tsx
index c3d76884..9827b545 100644
--- a/src/frontend/with-page-auth-required.tsx
+++ b/src/frontend/with-page-auth-required.tsx
@@ -1,5 +1,4 @@
 import React, { ComponentType, useEffect } from 'react';
-import { useRouter } from 'next/router';
 
 import { useConfig } from './use-config';
 import { useUser } from './use-user';
@@ -73,18 +72,22 @@ export type WithPageAuthRequired = <P extends object>(
  */
 const withPageAuthRequired: WithPageAuthRequired = (Component, options = {}) => {
   return function withPageAuthRequired(props): JSX.Element {
-    const router = useRouter();
-    const {
-      returnTo = `${router.basePath ?? ''}${router.asPath}`,
-      onRedirecting = defaultOnRedirecting,
-      onError = defaultOnError
-    } = options;
+    const { returnTo, onRedirecting = defaultOnRedirecting, onError = defaultOnError } = options;
     const { loginUrl } = useConfig();
     const { user, error, isLoading } = useUser();
 
     useEffect(() => {
       if ((user && !error) || isLoading) return;
-      window.location.assign(`${loginUrl}?returnTo=${encodeURIComponent(returnTo)}`);
+      let returnToPath: string;
+
+      if (!returnTo) {
+        const currentLocation = window.location.toString();
+        returnToPath = currentLocation.replace(new URL(currentLocation).origin, '') || '/';
+      } else {
+        returnToPath = returnTo;
+      }
+
+      window.location.assign(`${loginUrl}?returnTo=${encodeURIComponent(returnToPath)}`);
     }, [user, error, isLoading]);
 
     if (error) return onError(error);


Test patch:
diff --git a/tests/frontend/use-config.test.ts b/tests/frontend/use-config.test.ts
index e4969d50..a17210ec 100644
--- a/tests/frontend/use-config.test.ts
+++ b/tests/frontend/use-config.test.ts
@@ -3,10 +3,6 @@ import { renderHook } from '@testing-library/react-hooks';
 import { withConfigProvider } from '../fixtures/frontend';
 import { useConfig } from '../../src/frontend/use-config';
 
-jest.mock('next/router', () => ({
-  useRouter: (): any => ({ asPath: '/' })
-}));
-
 describe('context wrapper', () => {
   test('should provide the default login url', async () => {
     const { result } = renderHook(() => useConfig(), {
diff --git a/tests/frontend/use-user.test.tsx b/tests/frontend/use-user.test.tsx
index 44d68433..516d5e59 100644
--- a/tests/frontend/use-user.test.tsx
+++ b/tests/frontend/use-user.test.tsx
@@ -11,10 +11,6 @@ import { useConfig } from '../../src/frontend';
 import { useUser, UserContext } from '../../src';
 import React from 'react';
 
-jest.mock('next/router', () => ({
-  useRouter: (): any => ({ asPath: '/' })
-}));
-
 describe('context wrapper', () => {
   afterEach(() => delete (global as any).fetch);
 
diff --git a/tests/frontend/with-page-auth-required.test.tsx b/tests/frontend/with-page-auth-required.test.tsx
index 2c6d396e..22af2f5c 100644
--- a/tests/frontend/with-page-auth-required.test.tsx
+++ b/tests/frontend/with-page-auth-required.test.tsx
@@ -9,22 +9,17 @@ import { fetchUserUnsuccessfulMock, fetchUserErrorMock, withUserProvider, user }
 import { withPageAuthRequired } from '../../src/frontend';
 
 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() };
+    window.location = {
+      ...windowLocation,
+      assign: jest.fn(),
+      toString: jest.fn(() => 'https://example.com')
+    };
   });
   afterEach(() => delete (global as any).fetch);
   afterAll(() => (window.location = windowLocation));
@@ -86,47 +81,56 @@ describe('with-page-auth-required csr', () => {
     await waitFor(() => expect(screen.getByText('Error')).toBeInTheDocument());
   });
 
-  it('should accept a returnTo url', async () => {
+  it('should use a custom login URL', async () => {
+    process.env.NEXT_PUBLIC_AUTH0_LOGIN = '/api/foo';
     (global as any).fetch = fetchUserUnsuccessfulMock;
     const MyPage = (): JSX.Element => <>Private</>;
-    const ProtectedPage = withPageAuthRequired(MyPage, { returnTo: '/foo' });
+    const ProtectedPage = withPageAuthRequired(MyPage);
+
+    render(<ProtectedPage />, { wrapper: withUserProvider() });
+    await waitFor(() => expect(window.location.assign).toHaveBeenCalledWith(expect.stringContaining('/api/foo')));
+    delete process.env.NEXT_PUBLIC_AUTH0_LOGIN;
+  });
+
+  it('should return to the root path', async () => {
+    window.location.toString = jest.fn(() => 'https://example.net');
+    (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=${encodeURIComponent('/foo')}`)
+        expect.stringContaining(`?returnTo=${encodeURIComponent('/')}`)
       )
     );
   });
 
-  it('should use a custom login url', async () => {
-    process.env.NEXT_PUBLIC_AUTH0_LOGIN = '/api/foo';
+  it('should return to the current path', async () => {
+    window.location.toString = jest.fn(() => 'https://example.net/foo');
     (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('/api/foo')));
-    delete process.env.NEXT_PUBLIC_AUTH0_LOGIN;
+    await waitFor(() =>
+      expect(window.location.assign).toHaveBeenCalledWith(
+        expect.stringContaining(`?returnTo=${encodeURIComponent('/foo')}`)
+      )
+    );
   });
 
-  it('should prepend the basePath to the returnTo URL', async () => {
-    const asPath = routerMock.asPath;
-    const basePath = routerMock.basePath;
-    routerMock.basePath = '/foo';
-    routerMock.asPath = '/bar';
+  it('should accept a custom returnTo URL', async () => {
     (global as any).fetch = fetchUserUnsuccessfulMock;
     const MyPage = (): JSX.Element => <>Private</>;
-    const ProtectedPage = withPageAuthRequired(MyPage);
+    const ProtectedPage = withPageAuthRequired(MyPage, { returnTo: '/foo' });
 
     render(<ProtectedPage />, { wrapper: withUserProvider() });
     await waitFor(() =>
       expect(window.location.assign).toHaveBeenCalledWith(
-        expect.stringContaining(`?returnTo=${encodeURIComponent('/foo/bar')}`)
+        expect.stringContaining(`?returnTo=${encodeURIComponent('/foo')}`)
       )
     );
-    routerMock.basePath = basePath;
-    routerMock.asPath = asPath;
   });
 
   it('should preserve multiple query params in the returnTo URL', async () => {