Repository: auth0/nextjs-auth0 (ts)

Issue:
Support returnTo in withMiddlewareAuthRequiredFactory
### Checklist

- [X] I have looked into the [Readme](https://github.com/auth0/nextjs-auth0#readme), [Examples](https://github.com/auth0/nextjs-auth0/blob/main/EXAMPLES.md), and [FAQ](https://github.com/auth0/nextjs-auth0/blob/main/FAQ.md) and have not found a suitable solution or answer.
- [X] I have looked into the [API documentation](https://auth0.github.io/nextjs-auth0/) and have not found a suitable solution or answer.
- [X] I have searched the [issues](https://github.com/auth0/nextjs-auth0/issues) and have not found a suitable solution or answer.
- [X] I have searched the [Auth0 Community](https://community.auth0.com) forums and have not found a suitable solution or answer.
- [X] I agree to the terms within the [Auth0 Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md).

### Describe the problem you'd like to have solved

On successful auth using `withMiddlewareAuthRequiredFactory`, I want to `returnTo` my own specified URL, but cannot because `withMiddlewareAuthRequiredFactory` [hard-codes](https://github.com/auth0/nextjs-auth0/blob/dc893143d23b8797bcfbb0317fec0bccaf029083/src/helpers/with-middleware-auth-required.ts#L79) a value.

### Describe the ideal solution

Support an input into the middleware constructor OR (better) the middleware fn itself to accept a returnTo param, as with similar APIs in this SDK.

### Alternatives and current workarounds

I have to redirect or nav my user from within my application, which is undesirable because it adds a highly undesirable entry into my user's navigation history/stack.

### Additional context

This is for the next@beta, at the time of writing!

Pull request description:
Add support for customizing returnTo in middleware
### 📋 Changes

Adding support for customizing the `returnTo` in middleware:

```js
import { withMiddlewareAuthRequired, getSession } from '@auth0/nextjs-auth0/edge';

// Existing
export default withMiddlewareAuthRequired(async function middleware(req) {
  return NextResponse.next();
});

// To provide a custom `returnTo` url to login:
export default withMiddlewareAuthRequired({
  returnTo: '/foo',
  async middleware(req) { return NextResponse.next(); } // Custom middleware can be here
});

// You can also provide a method for `returnTo` that takes the req as an argument.
// Useful for adding i18n or basePath
export default withMiddlewareAuthRequired({
  returnTo(req) { return `${req.nextURL.basePath}${req.nextURL.pathname}`};
});
```

### 📎 References

fixes #1322 #1329


Patch:
diff --git a/src/helpers/with-middleware-auth-required.ts b/src/helpers/with-middleware-auth-required.ts
index 00524231..2a0ce673 100644
--- a/src/helpers/with-middleware-auth-required.ts
+++ b/src/helpers/with-middleware-auth-required.ts
@@ -1,6 +1,16 @@
-import { NextMiddleware, NextResponse } from 'next/server';
+import { NextMiddleware, NextRequest, NextResponse } from 'next/server';
 import { SessionCache } from '../session';
 
+/**
+ * Pass custom options to {@link WithMiddlewareAuthRequired}.
+ *
+ * @category Server
+ */
+export type WithMiddlewareAuthRequiredOptions = {
+  middleware?: NextMiddleware;
+  returnTo?: string | ((req: NextRequest) => Promise<string> | string);
+};
+
 /**
  * Protect your pages with Next.js Middleware. For example:
  *
@@ -41,9 +51,35 @@ import { SessionCache } from '../session';
  * });
  * ```
  *
+ * To provide a custom `returnTo` url to login:
+ *
+ * ```js
+ * // middleware.js
+ * import { withMiddlewareAuthRequired, getSession } from '@auth0/nextjs-auth0/edge';
+ *
+ * export default withMiddlewareAuthRequired({
+ *   returnTo: '/foo',
+ *   // Custom middleware is provided with the `middleware` config option
+ *   async middleware(req) { return NextResponse.next(); }
+ * });
+ * ```
+ *
+ * You can also provide a method for `returnTo` that takes the req as an argument.
+ *
+ * ```js
+ * // middleware.js
+ * import { withMiddlewareAuthRequired, getSession } from '@auth0/nextjs-auth0/edge';
+ *
+ * export default withMiddlewareAuthRequired({
+ *   returnTo(req) { return `${req.nextURL.basePath}${req.nextURL.pathname}`};
+ * });
+ * ```
+ *
  * @category Server
  */
-export type WithMiddlewareAuthRequired = (middleware?: NextMiddleware) => NextMiddleware;
+export type WithMiddlewareAuthRequired = (
+  middlewareOrOpts?: NextMiddleware | WithMiddlewareAuthRequiredOptions
+) => NextMiddleware;
 
 /**
  * @ignore
@@ -52,10 +88,18 @@ export default function withMiddlewareAuthRequiredFactory(
   { login, callback }: { login: string; callback: string },
   getSessionCache: () => SessionCache
 ): WithMiddlewareAuthRequired {
-  return function withMiddlewareAuthRequired(middleware?): NextMiddleware {
+  return function withMiddlewareAuthRequired(opts?): NextMiddleware {
     return async function wrappedMiddleware(...args) {
       const [req] = args;
+      let middleware: NextMiddleware | undefined;
       const { pathname, origin, search } = req.nextUrl;
+      let returnTo = `${pathname}${search}`;
+      if (typeof opts === 'function') {
+        middleware = opts;
+      } else if (opts) {
+        middleware = opts.middleware;
+        returnTo = (typeof opts.returnTo === 'function' ? await opts.returnTo(req) : opts.returnTo) || returnTo;
+      }
       const ignorePaths = [login, callback, '/_next', '/favicon.ico'];
       if (ignorePaths.some((p) => pathname.startsWith(p))) {
         return;
@@ -75,15 +119,13 @@ export default function withMiddlewareAuthRequiredFactory(
             { status: 401 }
           );
         }
-        return NextResponse.redirect(
-          new URL(`${login}?returnTo=${encodeURIComponent(`${pathname}${search}`)}`, origin)
-        );
+        return NextResponse.redirect(new URL(`${login}?returnTo=${encodeURIComponent(returnTo)}`, origin));
       }
       const res = await (middleware && middleware(...args));
 
       if (res) {
         const nextRes = new NextResponse(res.body, res);
-        let cookies = authRes.cookies.getAll();
+        const cookies = authRes.cookies.getAll();
         if ('cookies' in res) {
           for (const cookie of res.cookies.getAll()) {
             nextRes.cookies.set(cookie);


Test patch:
diff --git a/tests/helpers/with-middleware-auth-required.test.ts b/tests/helpers/with-middleware-auth-required.test.ts
index 372e701b..8c380d96 100644
--- a/tests/helpers/with-middleware-auth-required.test.ts
+++ b/tests/helpers/with-middleware-auth-required.test.ts
@@ -73,6 +73,29 @@ describe('with-middleware-auth-required', () => {
     expect(redirect.searchParams.get('returnTo')).toEqual('/foo/bar?baz=hello');
   });
 
+  test('return to provided url string', async () => {
+    const res = await setup({ url: 'http://example.com/foo/bar?baz=hello', middleware: { returnTo: '/qux' } });
+    const redirect = new URL(res.headers.get('location') as string);
+    expect(redirect).toMatchObject({
+      hostname: 'example.com',
+      pathname: '/api/auth/login'
+    });
+    expect(redirect.searchParams.get('returnTo')).toEqual('/qux');
+  });
+
+  test('return to provided url fn', async () => {
+    const res = await setup({
+      url: 'http://example.com/foo/bar?baz=hello',
+      middleware: { returnTo: (req: NextRequest) => Promise.resolve(`/qux${new URL(req.url).search}`) }
+    });
+    const redirect = new URL(res.headers.get('location') as string);
+    expect(redirect).toMatchObject({
+      hostname: 'example.com',
+      pathname: '/api/auth/login'
+    });
+    expect(redirect.searchParams.get('returnTo')).toEqual('/qux?baz=hello');
+  });
+
   test('should ignore static urls', async () => {
     const res = await setup({ url: 'http://example.com/_next/style.css' });
     expect(res).toBeUndefined();
@@ -134,6 +157,12 @@ describe('with-middleware-auth-required', () => {
     expect(middleware).toHaveBeenCalled();
   });
 
+  test('should accept custom middleware in options obj', async () => {
+    const middleware = jest.fn();
+    await setup({ middleware: { middleware }, user: { name: 'dave' } });
+    expect(middleware).toHaveBeenCalled();
+  });
+
   test('should honor redirects in custom middleware for authenticated users', async () => {
     const middleware = jest.fn().mockImplementation(() => {
       return NextResponse.redirect('https://example.com/redirect');