Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Combine createMiddleware with additional auth middleware #2639

Open
brandanking-decently opened this issue Dec 19, 2024 · 16 comments
Open

Combine createMiddleware with additional auth middleware #2639

brandanking-decently opened this issue Dec 19, 2024 · 16 comments

Comments

@brandanking-decently
Copy link

I was wondering how I can combine the createMiddleware method with other middleware such as NextAuth

@davidmytton
Copy link
Contributor

Hey @brandanking-decently - our createMiddleware function takes another middleware as the second parameter, so you can pass it through directly. For example with Auth.js you could do this:

// This example is for Auth.js 5, the successor to NextAuth 4
import arcjet, { createMiddleware, shield } from "@arcjet/next";
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
// @ts-ignore
import type { NextAuthConfig, NextAuthRequest } from "next-auth";

export const config = {
  // matcher tells Next.js which routes to run the middleware on.
  // This runs the middleware on all routes except for static assets.
  matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};
const aj = arcjet({
  key: process.env.ARCJET_KEY!, // Get your site key from https://app.arcjet.com
  rules: [
    // Protect against common attacks with Arcjet Shield
    shield({
      mode: "LIVE", // will block requests. Use "DRY_RUN" to log only
    }),
  ],
});

export const authConfig = {
  providers: [GitHub],
} satisfies NextAuthConfig;

const { auth } = NextAuth(authConfig);

export const authMiddleware = auth(async (req: NextAuthRequest) => {
  if (!req.auth) {
    // If the user is not authenticated, return a 401 Unauthorized response. You
    // may wish to redirect to a login page instead.
    return Response.json({ message: "Unauthorized" }, { status: 401 });
  }
});

export default createMiddleware(aj, authMiddleware);

For multiple middleware then you may want to use a helper library like https://nemo.rescale.build

Have you tried either of these approaches? Did you have any problems?

@brandanking-decently
Copy link
Author

I was trying to use the @nosecone/next package which doesn't seem to accept another middleware into it? Is this achievable in that package?

@davidmytton
Copy link
Contributor

Ah, for Nosecone you would need to call it within your own custom middleware function. If you're using Auth.js with Next.js then you could create a middleware.ts file like this:

import { type NoseconeOptions, createMiddleware, defaults } from "@nosecone/next";
import { auth } from "auth";

// Nosecone security headers configuration
// https://docs.arcjet.com/nosecone/quick-start
const noseconeOptions: NoseconeOptions = {
  ...defaults,
};

const securityHeaders = createMiddleware(noseconeOptions);

export default auth(async (req) => {
  if (!req.auth && !req.nextUrl.pathname.startsWith("/auth")) {
    const newUrl = new URL("/auth/signin", req.nextUrl.origin)
    return Response.redirect(newUrl)
  }

  return securityHeaders();
})

export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
}

This assumes Auth.js 5 beta and that the auth API path is app/auth/[...nextauth]/route.ts. There is a redirect to the default signin URL, so you will probably need to change that.

Does that work?

@brandanking-decently
Copy link
Author

My middleware looks like this, we also use Next Intl to handle i18n. Not sure this is possible to chain currently

export default auth(
	async function middleware(request: KindeRequest) {
		return intlMiddleware(request);
	},
);

@davidmytton
Copy link
Contributor

Chaining more than 2 layers of middleware gets fiddly, so I'd suggest using https://nemo.rescale.build/ to have Nosecone run "before", then set up auth, and have the intlMiddleware run "after".

@brandanking-decently
Copy link
Author

I believe I was able to get it close to working based on what you said, however, whenever I use the headerMiddleware (needed to rename as multiple createMiddleware functions) I now always get a 404. Do you have any idea what might be causing it?

const intlMiddleware = createIntlMiddleware(routing);

const noseconeOptions: NoseconeOptions = {
	...defaults,
};

const before: MiddlewareFunction[] = [headerMiddleware(noseconeOptions)];

const after: MiddlewareFunction[] = [
	async ({ request }: MiddlewareFunctionProps) => {
		return withAuth(request, { isReturnToCurrentPage: true, publicPaths: ['/'] });
	},
	async ({ request }: MiddlewareFunctionProps) => {
		return intlMiddleware(request);
	},
];

export const middleware = createMiddleware({}, { before, after });

export const config = { matcher: ['/((?!api|_next|.*\\..*).*)'] };

@davidmytton
Copy link
Contributor

Can you provide the imports and package.json so I can see which packages these are coming from please?

@brandanking-decently
Copy link
Author

import { withAuth } from '@kinde-oss/kinde-auth-nextjs/middleware';
import { type NoseconeOptions, defaults, createMiddleware as headerMiddleware } from '@nosecone/next';
import { type MiddlewareFunction, type MiddlewareFunctionProps, createMiddleware } from '@rescale/nemo';
import createIntlMiddleware from 'next-intl/middleware';
import { routing } from './i18n/routing';

const intlMiddleware = createIntlMiddleware(routing);

const noseconeOptions: NoseconeOptions = {
	...defaults,
};

const before: MiddlewareFunction[] = [headerMiddleware(noseconeOptions)];

const after: MiddlewareFunction[] = [
	async ({ request }: MiddlewareFunctionProps) => {
		return withAuth(request, { isReturnToCurrentPage: true, publicPaths: ['/'] });
	},
	async ({ request }: MiddlewareFunctionProps) => {
		return intlMiddleware(request);
	},
];

export const middleware = createMiddleware({}, { before, after });

export const config = { matcher: ['/((?!api|_next|.*\\..*).*)'] };

@davidmytton
Copy link
Contributor

I had a play around with Nemo and couldn't get it to work properly either. We have an internal tool that handles the basic Nemo functionality to chain middleware, so I've tidied that up here. Can you try this please:

1.npm install path-to-regexp
2. Set this up as your middleware.ts:

import { type NoseconeOptions, defaults, createMiddleware as noseconeMiddleware } from "@nosecone/next";
import {
    type NextFetchEvent,
    type NextMiddleware,
    type NextRequest,
    NextResponse,
} from "next/server";
import { match } from "path-to-regexp";

// Next.js middleware config
export const config = {
    matcher: ['/((?!_next/|_static|_vercel|[\\w-]+\\.\\w+).*)'],
};

// Nosecone security headers configuration
// https://docs.arcjet.com/nosecone/quick-start
const noseconeOptions: NoseconeOptions = {
    ...defaults,
};

const securityHeaders = noseconeMiddleware(noseconeOptions);

// Add any paths you want to run different middleware for. They use
// path-to-regexp which is the same as the Next.js config. You can provide a
// single middleware or an array of middlewares.
export default router({
    // Run nosecone middleware on any path
    "/{*path}": [securityHeaders],
});

// Simplified version of nemo. This could be extracted into a utility library
function router(
    pathMiddlewareMap: Record<string, NextMiddleware | NextMiddleware[]>,
): NextMiddleware {
    const middleware = Object.entries(pathMiddlewareMap).map(
        ([path, middleware]) => {
            if (Array.isArray(middleware)) {
                return [match(path), middleware] as const;
            } else {
                return [match(path), [middleware]] as const;
            }
        },
    );

    return async (
        request: NextRequest,
        event: NextFetchEvent,
    ): Promise<NextResponse | Response> => {
        const path = request.nextUrl.pathname || "/";
        const addedHeaders = new Headers();

        for (const [matchFunc, middlewareFuncs] of middleware) {
            const m = matchFunc(path);
            if (m) {
                for (const fn of middlewareFuncs) {
                    const resp = await fn(request, event);
                    // TODO: better response guards
                    if (typeof resp !== "undefined" && resp !== null) {
                        resp.headers.forEach((value, key) => {
                            addedHeaders.set(key, value);
                        });
                    }
                }
            }
        }

        addedHeaders.set("x-middleware-next", "1");

        return new Response(null, {
            headers: addedHeaders,
        });
    };
}
  1. Assuming you want to use your existing middleware on all routes, you would add it to the router array e.g.
export default router({
    // Run nosecone middleware on any path
    "/{*path}": [securityHeaders, intlMiddleware]
});

If this works for you then I'll update our docs to include it as a proper example.

@brandanking-decently
Copy link
Author

Unfortunately I just can't seem to get this working with the auth middleware as well. Thank you for your help on this however.

@davidmytton
Copy link
Contributor

What happens when you use the code in #2639 (comment) ? If you can describe what’s happening and show your current implementation I can try and reproduce it.

@brandanking-decently
Copy link
Author

Below is my implementation, I just keep getting sent to a 404 page.
This seems to be how the router function is handling the auth middleware.

import { withAuth } from '@kinde-oss/kinde-auth-nextjs/server';
import { type NoseconeOptions, defaults, createMiddleware as noseconeMiddleware } from '@nosecone/next';
import createIntlMiddleware from 'next-intl/middleware';
import type { NextFetchEvent, NextMiddleware, NextRequest, NextResponse } from 'next/server';
import { match } from 'path-to-regexp';
import { routing } from './i18n/routing';

// Next.js middleware config

export const config = { matcher: ['/((?!api|_next|.*\\..*).*)'] };

// Nosecone security headers configuration
// https://docs.arcjet.com/nosecone/quick-start
const noseconeOptions: NoseconeOptions = {
	...defaults,
};

const securityHeaders = noseconeMiddleware(noseconeOptions);
const authMiddleware = async (request: NextRequest) => withAuth(request, { isReturnToCurrentPage: true });
const intlMiddleware = createIntlMiddleware(routing);

// Add any paths you want to run different middleware for. They use
// path-to-regexp which is the same as the Next.js config. You can provide a
// single middleware or an array of middlewares.
export default router({
	// Run nosecone middleware on any path
	'/{*path}': [securityHeaders, authMiddleware, intlMiddleware],
});

// Simplified version of nemo. This could be extracted into a utility library
function router(pathMiddlewareMap: Record<string, NextMiddleware | NextMiddleware[]>): NextMiddleware {
	const middleware = Object.entries(pathMiddlewareMap).map(([path, middleware]) => {
		if (Array.isArray(middleware)) {
			return [match(path), middleware] as const;
		}
		return [match(path), [middleware]] as const;
	});

	return async (request: NextRequest, event: NextFetchEvent): Promise<NextResponse | Response> => {
		const path = request.nextUrl.pathname || '/';
		const addedHeaders = new Headers();

		for (const [matchFunc, middlewareFuncs] of middleware) {
			const m = matchFunc(path);
			if (m) {
				for (const fn of middlewareFuncs) {
					const resp = await fn(request, event);
					// TODO: better response guards
					if (typeof resp !== 'undefined' && resp !== null) {
						resp.headers.forEach((value, key) => {
							addedHeaders.set(key, value);
						});
					}
				}
			}
		}

		addedHeaders.set('x-middleware-next', '1');

		return new Response(null, {
			headers: addedHeaders,
		});
	};
}

This is my current implementation with Nemo that works but excluding the securityHeaders

import { withAuth } from '@kinde-oss/kinde-auth-nextjs/middleware';
// import { type NoseconeOptions, defaults, createMiddleware as securityHeaders } from '@nosecone/next';
import { type MiddlewareFunction, type MiddlewareFunctionProps, createMiddleware } from '@rescale/nemo';
import createIntlMiddleware from 'next-intl/middleware';
import { routing } from './i18n/routing';

const intlMiddleware = createIntlMiddleware(routing);

// const noseconeOptions: NoseconeOptions = {
// 	...defaults,
// };

// const before: MiddlewareFunction[] = [securityHeaders(noseconeOptions)];

const after: MiddlewareFunction[] = [
	async ({ request }: MiddlewareFunctionProps) => {
		return withAuth(request, { isReturnToCurrentPage: true });
	},
	async ({ request }: MiddlewareFunctionProps) => {
		return intlMiddleware(request);
	},
];

export const middleware = createMiddleware({}, { after });

export const config = { matcher: ['/((?!api|_next|.*\\..*).*)'] };

@davidmytton
Copy link
Contributor

davidmytton commented Dec 22, 2024

I was able to reproduce this using next-intl. The issue comes from the custom router method ignoring a status code from other chained middleware. When loading the root path, next-intl will redirect to a locale e.g. localhost:3000/ to localhost:3000/en. Our router code currently doesn't pass the status code through which triggers the redirect.

Note that the downside of this implementation is that security headers won't be set on the redirecting page. This is an improvement that can be made later once we've tested this works for you:

Here's the fixed implementation with next-intl installed. Drop the Kinde auth in and let me know how it goes:

import { routing } from "@/i18n/routing";
import { type NoseconeOptions, defaults, createMiddleware as noseconeMiddleware } from "@nosecone/next";
import createMiddleware from 'next-intl/middleware';
import {
    type NextFetchEvent,
    type NextMiddleware,
    type NextRequest,
    NextResponse,
} from "next/server";
import { match } from "path-to-regexp";

// Next.js middleware config
export const config = {
    matcher: ['/((?!_next/|_static|_vercel|[\\w-]+\\.\\w+).*)'],
};

// Nosecone security headers configuration
// https://docs.arcjet.com/nosecone/quick-start
const noseconeOptions: NoseconeOptions = {
    ...defaults,
};
const securityHeaders = noseconeMiddleware(noseconeOptions);

const intlMiddleware = createMiddleware(routing);

// Add any paths you want to run different middleware for. They use
// path-to-regexp which is the same as the Next.js config. You can provide a
// single middleware or an array of middlewares.
export default router({
    // Run nosecone middleware on any path
    "/{*path}": [securityHeaders, intlMiddleware],
});

// A simple middleware router that allows you to run different middleware based
// on the path of the request.
function router(
    pathMiddlewareMap: Record<string, NextMiddleware | NextMiddleware[]>,
): NextMiddleware {
    const middleware = Object.entries(pathMiddlewareMap).map(
        ([path, middleware]) => {
            if (Array.isArray(middleware)) {
                return [match(path), middleware] as const;
            } else {
                return [match(path), [middleware]] as const;
            }
        },
    );

    return async (
        request: NextRequest,
        event: NextFetchEvent,
    ): Promise<NextResponse | Response> => {
        const path = request.nextUrl.pathname || "/";
        const addedHeaders = new Headers();

        for (const [matchFunc, middlewareFuncs] of middleware) {
            const m = matchFunc(path);
            if (m) {
                for (const fn of middlewareFuncs) {
                    const resp = await fn(request, event);
                    // TODO: better response guards
                    if (typeof resp !== "undefined" && resp !== null) {
                        // If it's a redirect or auth status, bail immediately.
                        if (resp.status >= 300 && resp.status < 500) {
                            return resp;
                        }

                        resp.headers.forEach((value, key) => {
                            addedHeaders.set(key, value);
                        });
                    }
                }
            }
        }

        addedHeaders.set("x-middleware-next", "1");

        return new Response(null, {
            headers: addedHeaders,
        });
    };
}

@trevorpfiz
Copy link

I am trying to implement @nosecone/next with Supabase Auth, @supabase/ssr, based on the middleware implementation in https://supabase.com/docs/guides/auth/server-side/nextjs?queryGroups=router&router=app. How should I be doing this? Thanks!

@davidmytton
Copy link
Contributor

@trevorpfiz it looks like the supabase updateSession runs on every route so you'll need to use the router example above in #2639 (comment) to chain the middleware with Nosecone. If you can't get it working, can you open a new issue with the example code and the results you're seeing please? We can then take a look.

@trevorpfiz
Copy link

@davidmytton Thanks! It seems to be working, but I will let you know if I run into any issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants