From c622e346226366f2ac0bd29372ea89c82180737e Mon Sep 17 00:00:00 2001 From: Adam Mcgrath Date: Thu, 3 Aug 2023 12:56:59 +0100 Subject: [PATCH 1/3] Add support for customising returnTo in middleware --- src/helpers/with-middleware-auth-required.ts | 56 ++++++++++++++++--- .../with-middleware-auth-required.test.ts | 29 ++++++++++ 2 files changed, 78 insertions(+), 7 deletions(-) diff --git a/src/helpers/with-middleware-auth-required.ts b/src/helpers/with-middleware-auth-required.ts index 005242314..49f3e7b28 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); +}; + /** * 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 (opts && 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); diff --git a/tests/helpers/with-middleware-auth-required.test.ts b/tests/helpers/with-middleware-auth-required.test.ts index 372e701bf..8c380d96b 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'); From 688a16eda9c6954ef1743ed35d8e4a20878e2524 Mon Sep 17 00:00:00 2001 From: Adam Mcgrath Date: Thu, 3 Aug 2023 13:15:56 +0100 Subject: [PATCH 2/3] Update src/helpers/with-middleware-auth-required.ts Co-authored-by: Rita Zerrizuela --- src/helpers/with-middleware-auth-required.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/with-middleware-auth-required.ts b/src/helpers/with-middleware-auth-required.ts index 49f3e7b28..a27feebd6 100644 --- a/src/helpers/with-middleware-auth-required.ts +++ b/src/helpers/with-middleware-auth-required.ts @@ -2,7 +2,7 @@ import { NextMiddleware, NextRequest, NextResponse } from 'next/server'; import { SessionCache } from '../session'; /** - * Pass custom options to {@link WithMiddlewareAuthRequired} + * Pass custom options to {@link WithMiddlewareAuthRequired}. * * @category Server */ From 1f23495758941622089791f51529844bf93e09d0 Mon Sep 17 00:00:00 2001 From: Adam Mcgrath Date: Thu, 3 Aug 2023 16:00:26 +0100 Subject: [PATCH 3/3] Update src/helpers/with-middleware-auth-required.ts --- src/helpers/with-middleware-auth-required.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/with-middleware-auth-required.ts b/src/helpers/with-middleware-auth-required.ts index a27feebd6..2a0ce6730 100644 --- a/src/helpers/with-middleware-auth-required.ts +++ b/src/helpers/with-middleware-auth-required.ts @@ -94,7 +94,7 @@ export default function withMiddlewareAuthRequiredFactory( let middleware: NextMiddleware | undefined; const { pathname, origin, search } = req.nextUrl; let returnTo = `${pathname}${search}`; - if (opts && typeof opts === 'function') { + if (typeof opts === 'function') { middleware = opts; } else if (opts) { middleware = opts.middleware;