Welcome to the Kinde community

Updated 10 months ago

Create a guide on how to implement multitenancy in NextJS

At a glance

The community member is looking to implement multitenancy with NextJS and KindeAuth. They are seeking best practices on handling this in the middleware and protecting routes. The community members provide several suggestions:

- Use the withAuth middleware for route protection, which redirects unauthenticated users to the login page.

- Implement a custom isAuthorized function to check if the authenticated user has the necessary permissions within a specific organization to access the route.

- Leverage Kinde's support for organizations to manage users and their access within the application.

- Use Kinde's SDK to fetch organization and permission data for the authenticated user, and implement logic to show or hide features based on the user's permissions.

The community members also discuss a specific implementation approach that ensures the organization code is always present in the URL, and provide a conceptual example of how to handle different permissions for different paths within the isAuthorized function.

I'm looking on implementing multitenancy with NextJS and KindeAuth. While it's great that Kinde supports this with organisations, it would be very helpful on a best practice approach on how to handle this. More particularly, on how to handle this in the middleware and properly protecting the routes. Loving the product btw!
A
W
9 comments
I'm glad to hear you're enjoying Kinde!

When it comes to handling multitenancy in middleware and protecting routes, here are some best practices and steps you can follow:

Use the withAuth Middleware for Route Protection: The withAuth helper function provided by Kinde is designed to protect routes. If a user is not authenticated, they are redirected to the login page. Once authenticated, they will be redirected back to the protected page they initially requested access to.

Example:

Plain Text
 
 import {withAuth} from "@kinde-oss/kinde-auth-nextjs/middleware";
   export default function middleware(req) {
       return withAuth(req, {
           isReturnToCurrentPage: true
       });
   }
   export const config = {
       matcher: ["/admin"]
   };
   



Custom Authorization with isAuthorized Function: For more granular control, especially in a multitenant setup, you can define a custom isAuthorized function. This function can check if the authenticated user has the necessary permissions within a specific organization to access the route.

Example:
Plain Text
   import {withAuth} from "@Kinde-oss/kinde-auth-nextjs/middleware";
   export default withAuth(
       async function middleware(req) {
           console.log("look at me", req.kindeAuth);
       },
       {
           isAuthorized: ({token}) => {
               // Example: The user is authorized if they have the 'edit:projects' permission
               return token.permissions.includes("edit:projects");
           }
       }
   );
   export const config = {
       matcher: ["/admin"]
   };
   



Leverage Organizations for User Management: Utilize Kinde's support for organizations to manage users and their access within your application. You can control who can access specific parts of your application and set roles and permissions for each user in an organization.

Fetching Organization and Permission Data: Use Kinde's SDK to fetch organization and permission data for the authenticated user. This can help you implement logic in your application to show or hide certain features based on the user's permissions within an organization.

Example:
Plain Text
   const {getUserOrganizations, getPermission} = useKindeAuth();
   console.log(getUserOrganizations());
   // Check if the user has permission to create todos
   const permission = getPermission("create:todos");
   if (permission.isGranted) {
       // Show the feature
   }
   



Remember, when implementing multitenancy, it's crucial to ensure that your application's logic correctly segregates data and access based on the organization context. This includes not only protecting routes but also filtering data and actions based on the user's organization memberships and permissions.

For more detailed information and examples, you can refer to the Kinde documentation on multi-tenancy using organizations, NextJS SDK, and NextJS Pages Router SDK v2.
Thanks for the detailed response
This is how I'm currently handling it (handling roles & permissions still to-do); but I wanted to also always have the organisation code in the URL as well and added some redirects for partial paths or when manually overwriting the orgCode in the URL for UX, so this is how I handled this and maybe it could be useful if somebody else stumbles across a similar case:
Plain Text
import { NextRequest, NextResponse } from "next/server";
import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server";
import { withAuth } from "@kinde-oss/kinde-auth-nextjs/middleware";

export default withAuth(
  async function middleware(
    req: NextRequest & { kindeAuth: { user: any; token: string } }
  ) {
    // Get current organisation from user
    const { getOrganization } = getKindeServerSession();
    const org = await getOrganization();
    const orgCode = org?.orgCode;

    // Ensure always the correct orgCode is in the path of the user on the firm paths
    if (req.nextUrl.pathname.startsWith("/firm")) {
      if (
        req.nextUrl.pathname === "/firm" ||
        req.nextUrl.pathname === `/firm/${orgCode}`
      ) {
        return NextResponse.redirect(
          new URL(`/firm/${orgCode}/dashboard`, req.nextUrl)
        );
      }

      // Match any firm path where the orgCode is different from the user's orgCode
      const firmPathMatch = req.nextUrl.pathname.match(/\/firm\/([^\/]+)(.*)/);

      if (firmPathMatch && firmPathMatch[1] !== orgCode) {
        return NextResponse.redirect(
          new URL(`/firm/${orgCode}${firmPathMatch[2]}`, req.nextUrl)
        );
      }
    }
  },
  {
    isAuthorized: ({ token }: any) => {
      // The user will be considered authorized if they have the permission 'eat:chips'
      console.log("token permissions", token.permissions);

      return true;
    },
  }
);

export const config = {
  matcher: ["/firm/:path*"],
};
Although I'm not yet sure how this isAuthorized will work if I need different permissions for different paths since there doesn't seem to be a direct way to fetch the current path in isAuthorized (or I'm missing this)
Regarding your concern about handling different permissions for different paths within the isAuthorized function, you're correct that the current setup doesn't directly provide access to the current path. However, you can work around this limitation by leveraging the request object (req) that you have access to in the middleware function, before the isAuthorized check.

Here's a conceptual approach to achieve this:

Extract the Current Path in Middleware: Before the isAuthorized check, you can extract the current path from the req object and attach it to the req.kindeAuth object or another custom property that you pass along to the isAuthorized function.

Use the Attached Path in isAuthorized: Within the isAuthorized function, you can then access this path and implement logic to check for specific permissions based on the current path.

Here's a simplified example of how you might adjust your middleware to include the current path:

Plain Text
export default withAuth(
  async function middleware(
    req: NextRequest & { kindeAuth: { user: any; token: string; currentPath?: string } }
  ) {
    // Your existing logic here...

    // Attach the current path to the kindeAuth object
    req.kindeAuth.currentPath = req.nextUrl.pathname;

    // Continue with the rest of your middleware logic...
  },
  {
    isAuthorized: ({ token, currentPath }: any) => {
      console.log("Current path:", currentPath);
      console.log("Token permissions:", token.permissions);

      // Implement your path-specific permission logic here
      if (currentPath.startsWith("/firm/special") && !token.permissions.includes("special:access")) {
        return false;
      }

      return true;
    },
  }
);


Please note that this is a conceptual example and might need adjustments to fit into your specific implementation. The key idea is to leverage the middleware's ability to modify or augment the request object before it reaches the isAuthorized check, allowing you to pass additional context (like the current path) that can be used for more granular authorization decisions.

This approach provides a flexible way to handle different permissions for different paths by utilizing the middleware's capabilities to enrich the authorization context dynamically.

Keep in mind that as your application and authorization logic grow more complex, maintaining clarity and manageability of your authorization checks becomes increasingly important. Consider structuring your permission logic in a way that's easy to understand and modify as your application evolves.
Thanks a lot for your help! πŸ™πŸ™
Saved me a lot of time in figuring it out
Yay, awesome to hear
Add a reply
Sign up and join the conversation on Discord