-
Notifications
You must be signed in to change notification settings - Fork 28
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
How to compose authKitMiddleware
with custom middleware?
#47
Comments
authKitMiddleware
with rewrites?authKitMiddleware
with custom middleware?
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: I think that before the redirect can happen (which is what 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 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:
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. |
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. |
I guess what is the alternative here? How can we perform rewrites while also running the request through authkit? |
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 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
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 export const config = {
// Only match on /app routes
matcher: ['/app/:path*'],
}; |
@kevinmitch14 were you able to come up with a solution for composing the middleware with subdomain logic and authkit? |
@naikaayush I had to do the same thing that Kevin did: basically rewrite authkit yourself in a server using the node client. WorkOS nodejs docs are pretty good and cover most of this. Unfortunately it looks like you can't use AuthKit for customized middleware, you have to roll your own Authkit... |
@naikaayush Yeah I ended up using the node client, which gives a lot more flexibility for more custom use-cases. |
@kevinmitch14 thanks for that, so the node client was used in server components, and not the middleware I'm assuming, and what about the client components, did you just end up passing the user object to the client components from the server components? |
I'm using it in middleware too, I believe it is 'edge-compatible'. And yeah you can pass data to client components. |
I'm going to look into making the middleware easier to work with for custom middleware. Does anyone have any strong feelings on what sort of shape this would take? I'm initially leaning towards a pattern of passing in a function, e.g. export default authkitMiddleware({
onAuth: (request, event) => {
// Run custom logic after the middleware has done its auth checks
}
}); |
@PaulAsjes it would be much more composable if we could get a response from authKit and then decide what to do with that response. For example, using your initial proposal, we would be limited in how we compose our middleware. This proposal requires authkit to be run (and run first) on all routes that the middleware is run on. export default authkitMiddleware({
onAuth: (request, event) => {
// this requires the authKit middleware to run on every request no matter what
}
}); Whereas if we were able to compose authkit to get a response we could basically perform any logic we wanted (as far as I can imagine): import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// we can perform any logic before or after authkit
if(req.url){
// whatever we want here
// multi-tenant apps, adding custom headers, etc.
}
const authResponse = authKitMiddleware(req);
const session = authResponse.session;
if(pathname === "/protected-route" && !session){
return authResponse.redirectURL
} else {
// Another opportunity to perform any custom logic based on granular scenarios + authkit responses
}
return NextResponse.redirect(new URL('/home', request.url))
}
// we can properly customize where the middleware runs
export const config = {
matcher: '/about/:path*',
}
}); Allow us to compose authkit inside of our nextjs middleware mimics classic middleware design, instead of forcing us to perform logic inside of the middleware that authkit provides. |
Agree with mattbf. Clerkjs does something similar for their next package (example). If it helps others, I have a workaround for getting route rewrites working. I'm doing this in the next.config.js file while keeping authkit-nextjs in the middleware file. I think this would also work for setting headers, etc. Their documentation is at https://vercel.com/guides/can-i-redirect-from-a-subdomain-to-a-subpath I have an /app/[domain] folder, so subdomain.rootdomain.extension:3000 maps to /app/[domain]/page.tsx (I'm specifying the port as this is on local).
|
Hi folks, I have a PR up that should hopefully fix this. I decided to go with @mattbf's suggestion and stick to classic middleware design. Here's an example of what this would look like: export default async function middleware(request: NextRequest) {
// Perform logic before or after AuthKit
// Auth object contains the session, response headers and an auhorization URL in the case that the session isn't valid
// This method will automatically handle setting the cookie and refreshing the session
const auth = await authkit(request, {
debug: true,
});
// Control of what to do when there's no session on a protected route is left to the developer
if (request.url.includes("/account") && !auth.session.user) {
console.log("No session on protected path");
return NextResponse.redirect(auth.authorizationUrl);
// Alternatively you could redirect to your own login page, for example if you want to use your own UI instead of hosted AuthKit
return NextResponse.redirect("/login");
}
// Headers need to be included in every non-redirect response to ensure that `withAuth` works as expected
return NextResponse.next({
headers: auth.headers,
});
}
// Match against the pages
export const config = { matcher: ["/", "/account/:path*", "/api/:path*"] }; Please let me know if you feel that this is missing something or if you have any other feedback! |
@PaulAsjes amazing thank you! This is looking great. No major comments or concerns right now, just two questions:
return NextResponse.rewrite(url, { request: { headers: auth.headers } });
Eager to try this out! |
Seems to work just fine in my testing!
Using any of the auth methods like |
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!
The text was updated successfully, but these errors were encountered: