Open kevinmitch14 opened 4 months ago
I suspect the problem here is the line
return NextResponse.rewrite(request.nextUrl);
In your configuration, authkit-nextjs will attempt to redirect the user if not authenticated, as seen here: https://github.com/workos/authkit-nextjs/blob/main/src/session.ts#L46
I think that before the redirect can happen (which is what authkitMiddleware
is returning) you're rewriting the NextResponse
to send the user to request.nextUrl
.
If that doesn't do the trick, can you provide an example repo recreating the issue?
Hi! We're also having this issue — we're looking at using AuthKit, but also need to keep our existing Content Security Policy middleware. The existing authkitMiddleware
interface is too high-level, since we also need to update the NextResponse
with our own custom headers and footers at some paths. Here's a simple repro of what we're trying to accomplish:
Please let me know what the best approach is here. Thanks!
Hey @ankitr, I think you should be able to do this (adding custom headers to the request) by passing in a new request to the AuthKit middleware:
export default async function middleware(request: NextRequest) {
const newRequestHeaders = new Headers(request.headers);
newRequestHeaders.set("x-custom", "foo");
const newRequest = NextResponse.next({
request: { headers: newRequestHeaders },
});
return authkitMiddleware()(newRequest);
}
Bear in mind that you need to be on Next v13 for the above to work. Let me know if that helps!
Hey @PaulAsjes haven't got a chance to look into this more but when I have time I will provide more info. Using the node package instead for now.
For a bit more context, we have a multi-tenant app where each tenant is housed on a subdomain, so we are using middleware to manage that.
This is the setup with node client in middleware. (which is working as expected)
export async function middleware(request: NextRequest) {
const host =
request.headers.get("x-forwarded-host") ?? request.headers.get("host");
let subdomain = getSubdomain(host);
const url = request.nextUrl.clone();
const path = url.pathname;
if (SAFE_AUTH_DOMAINS.includes(subdomain)) {
return NextResponse.next();
}
const workosCookie = request.cookies.get(WORK_OS_COOKIE_NAME);
const session = await getSessionFromCookie(workosCookie?.value);
const { authInitFlowUrl } = getAuthConfig(request);
if (!session) {
return NextResponse.redirect(authInitFlowUrl);
}
request.nextUrl.pathname = `/${subdomain}/publisher${path}`;
const response = NextResponse.rewrite(request.nextUrl);
const hasValidSession = await verifyAccessToken(session.accessToken);
if (!hasValidSession) {
try {
const { accessToken, refreshToken } =
await workosClient.userManagement.authenticateWithRefreshToken({
clientId: process.env.WORKOS_CLIENT_ID,
refreshToken: session.refreshToken,
});
const encryptedSession = await sealData(
{
accessToken,
refreshToken,
user: session.user,
impersonator: session.impersonator,
},
{ password: process.env.WORKOS_COOKIE_PASSWORD },
);
response.cookies.set({
name: WORK_OS_COOKIE_NAME,
value: encryptedSession,
httpOnly: true,
path: "/",
secure: process.env.NODE_ENV === "production",
});
} catch (e) {
response.cookies.delete(WORK_OS_COOKIE_NAME);
return NextResponse.redirect(authInitFlowUrl);
}
}
return response;
}
export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};
Also having this issue building a multi-tenant app @PaulAsjes. I want to perform some logic on a subdomain before requiring authentication on the main app domain / dashboard.
I can get the response of authkit to come after this logic, but then if I try to use any other functions inside the app I get the error:
Error: You are calling `some authkit function` on a path that isn’t covered by the AuthKit middleware. Make sure it is running on all paths you are calling `getUser` from by updating your middleware config in `middleware.(js|ts)`.
Any guidance on this?
I was hitting this issue myself, the solution I found is that you'll want to perform the WorkOS auth middleware last and make sure to return it's response. Not returning the response from the authkitMiddleware function is what causes the error you mentioned.
Here's a simplified example of how I'm combining it with a rate limit check beforehand:
import { authkitMiddleware } from '@workos-inc/authkit-nextjs';
import { ratelimit } from 'lib/upstash/rateLimiter';
import { NextResponse } from 'next/server';
async function authMiddleware(request) {
const response = await authkitMiddleware({
middlewareAuth: {
// Enable the middleware on all routes by default
enabled: true,
// Allow logged out users to view these paths
unauthenticatedPaths: [],
},
})(request);
return response;
}
async function rateLimitMiddleware(request, context) {
const ip = request.ip ?? '127.0.0.1';
const { success } = await ratelimit.limit(ip);
if (!success) {
// Return a 429 status code when the rate limit is exceeded
console.warn(`Rate limit exceeded for IP: ${ip}`);
return NextResponse.json('You have exceeded the rate limit.', {
status: 429,
});
}
return null; // Return null to indicate that the request can proceed
}
export default async function middleware(request, context) {
try {
// Check the rate limit first
const rateLimitResponse = await rateLimitMiddleware(request, context);
// Return the rate limit response if it is not null
// This will stop the middleware chain and return the response
if (rateLimitResponse) {
return rateLimitResponse;
}
// Continue with the auth middleware when rate limit is not exceeded
// This will check if the user is authenticated
return authMiddleware(request);
} catch (error) {
console.error('Error in middleware:', error);
return NextResponse.json('Internal Server Error', { status: 500 });
}
}
export const config = {
// Don't match on static files, images and the favicon
matcher: ['/((?!_next/static|_next/image|favicon.ico)'],
};
@josh-respectx thanks, this makes sense and I can make that work, but what if you have two host patterns, one that is public (no authentication) and one that is private (requires authentication)?
How can you call authKitMiddleware only on the dashboard routes while still having logic to rewrite the requests?
My desired logic:
import { authkitMiddleware } from '@workos-inc/authkit-nextjs';
import { ratelimit } from 'lib/upstash/rateLimiter';
import { NextResponse } from 'next/server';
async function authMiddleware(request) {
const response = await authkitMiddleware({
middlewareAuth: {
// Enable the middleware on all routes by default
enabled: true,
// Allow logged out users to view these paths
unauthenticatedPaths: [],
},
})(request);
return response;
}
export default async function middleware(request, context) {
// get the host
let host = req.headers.get("host")
if(host === `app.myapp.com`){
// this is the dashboard, check auth
const authRes = await authMiddleware(req);
if (authRes?.ok) {
// Rewrite dashboard requests
return NextResponse.rewrite(new URL(`/app${path === "/" ? "" : path}`, req.url));
} else {
console.log("User not logged in");
return authRes;
}
} else {
// this is a public page, just continue as usual
return NextResponse.next();
}
}
export const config = {
// Don't match on static files, images and the favicon
matcher: ['/((?!_next/static|_next/image|favicon.ico)'],
};
What's the reasoning for the multiple host patterns? Is it the same application that is served from multiple subdomains or domains?
Edit: Ah wait, I see what you're doing with the rewrite. Without actually getting in there and testing this myself, my guess is that you might still want to run the authMiddleware on all requests, but adjust it's unauthenticatedPaths matcher to a regex pattern with a negative lookahead like:
// Allow logged out users to view all paths except those starting with '/app'
unauthenticatedPaths: [/^(?!\/app).*$/],
This will allow you to always return the authRes response, regardless of route, which should help avoid the error you mentioned above.
What's the reasoning for the multiple host patterns? Is it the same application that is served from multiple subdomains or domains?
Edit: Ah wait, I see what you're doing with the rewrite. Without actually getting in there and testing this myself, my guess is that you might still want to run the authMiddleware on all requests, but adjust it's unauthenticatedPaths matcher to a regex pattern with a negative lookahead like:
// Allow logged out users to view all paths except those starting with '/app' unauthenticatedPaths: [/^(?!\/app).*$/],
This will allow you to always return the authRes response, regardless of route, which should help avoid the error you mentioned above.
This is very clever, I had hopes.. but now I'm getting the error "Error: Error parsing routes for middleware auth. Reason: Capturing groups are not allowed at 2".
@PaulAsjes any guidance or ideas here?
Again to state clearly what I'm trying to do:
It seems as though it is impossible to configure authkitmiddleware to work with this.
"Error: Error parsing routes for middleware auth. Reason: Capturing groups are not allowed at 2".
I guess what is the alternative here? How can we perform rewrites while also running the request through authkit?
"Error: Error parsing routes for middleware auth. Reason: Capturing groups are not allowed at 2".
That looks like a RegExp issue. We could go down that particular rabbit hole, but I think the real solution here is that you likely don't want to use middlewareAuth
mode for your use case. That mode is for if you want everything to be protected barring some exceptions. In your case it sounds like you want the opposite where everything can be viewed whilst logged out except for /app
routes.
In which case I think the easiest solution is to just specify that directly:
import { authkitMiddleware } from '@workos-inc/authkit-nextjs';
import { ratelimit } from 'lib/upstash/rateLimiter';
import { NextResponse } from 'next/server';
export default async function middleware(request, context) {
// get the host
let host = req.headers.get("host")
if(host === `app.myapp.com`){
// this is the dashboard, check auth
const authRes = await authkitMiddleware()(req);
if (authRes?.ok) {
// Rewrite dashboard requests
return NextResponse.rewrite(new URL(`/app${path === "/" ? "" : path}`, req.url));
} else {
console.log("User not logged in");
return authRes;
}
} else {
// this is a public page, just continue as usual
return NextResponse.next();
}
}
export const config = {
// Only match on /app routes
matcher: ['/app/*'],
};
"Error: Error parsing routes for middleware auth. Reason: Capturing groups are not allowed at 2".
That looks like a RegExp issue. We could go down that particular rabbit hole, but I think the real solution here is that you likely don't want to use
middlewareAuth
mode for your use case. That mode is for if you want everything to be protected barring some exceptions. In your case it sounds like you want the opposite where everything can be viewed whilst logged out except for/app
routes.In which case I think the easiest solution is to just specify that directly:
import { authkitMiddleware } from '@workos-inc/authkit-nextjs'; import { ratelimit } from 'lib/upstash/rateLimiter'; import { NextResponse } from 'next/server'; export default async function middleware(request, context) { // get the host let host = req.headers.get("host") if(host === `app.myapp.com`){ // this is the dashboard, check auth const authRes = await authkitMiddleware()(req); if (authRes?.ok) { // Rewrite dashboard requests return NextResponse.rewrite(new URL(`/app${path === "/" ? "" : path}`, req.url)); } else { console.log("User not logged in"); return authRes; } } else { // this is a public page, just continue as usual return NextResponse.next(); } } export const config = { // Only match on /app routes matcher: ['/app/*'], };
@PaulAsjes this logic works as expected but then you get errors like
Error: You are calling `some authkit function` on a path that isn’t covered by the AuthKit middleware. Make sure it is running on all paths you are calling `getUser` from by updating your middleware config in `middleware.(js|ts)`.
I believe because that rewrite isn't returning the authkit response.
Looks like the only option is to use the nodejs package instead.
I think the issue here is with the RegExp in your matcher. To match all routes on /app
you need slightly different syntax according to the Next.js docs:
export const config = {
// Only match on /app routes
matcher: ['/app/:path*'],
};
Hey, we are trying to implement AuthKit in our multi-tenant nextjs app. We are having trouble composing the
authKitMiddleware
with other middleware operations mainly rewrites.What I am currently trying is something like the following: In the debug logs, it is displaying "
Unauthenticated user on protected route, redirecting to AuthKit
", but there is no redirect occurring. Any idea on how to get this working with more than just the example from the docs?Note, this is not just for rewrites, it would be good to understand how to use this a custom middleware!