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 · 9 comments
Open

Combine createMiddleware with additional auth middleware #2639

brandanking-decently opened this issue Dec 19, 2024 · 9 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.

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

2 participants