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

Private Paths #273

Open
alexllao opened this issue Nov 5, 2024 · 7 comments
Open

Private Paths #273

alexllao opened this issue Nov 5, 2024 · 7 comments

Comments

@alexllao
Copy link

alexllao commented Nov 5, 2024

Is there any way to do it the other way around? What happens if most URLs are public and only those starting with /dashboard are private? Is there any way to make it easier? I guess I should modify the authMiddleware and adapt it to my taste and needs.

@awinogrodzki
Copy link
Owner

awinogrodzki commented Nov 5, 2024

Hey @alexllao,

Good question – I think it just boils down to how you orchestrate the redirects within handleValidToken, handleInvalidToken and handleError functions.

This is the default configuration with public paths:

    handleValidToken: async ({token, decodedToken, customToken}, headers) => {
      // Authenticated user should not be able to access /login, /register and /reset-password routes
      if (PUBLIC_PATHS.includes(request.nextUrl.pathname)) {
        return redirectToHome(request);
      }

      return NextResponse.next({
        request: {
          headers
        }
      });
    },
    handleInvalidToken: async (_reason) => {
      return redirectToLogin(request, {
        path: '/login',
        publicPaths: PUBLIC_PATHS
      });
    },
    handleError: async (error) => {
      console.error('Unhandled authentication error', {error});

      return redirectToLogin(request, {
        path: '/login',
        publicPaths: PUBLIC_PATHS
      });
    }

It works as follows:

  1. handleValidToken – If user is authenticated and they try to access public paths, we redirect them to home page, otherwise we let the page render by returning NextResponse.next
  2. handleInvalidToken and handleError – If user is unauthenticated and the page is not public, we redirect them to /login path, otherwise we let the page render.

We need to reverse this logic to support private paths, and make it so that:

  1. handleValidToken – If user is authenticated, just let them see every page
  2. handleInvalidToken and handleError – If user is unauthenticated and the page is private, redirect them to home page, otherwise render

First, we would need to write custom redirect function, instead of using built in redirectToHome or redirectToLogin, which are focused on public paths:

const PRIVATE_PATHS = [/^\/dashboard/];

export function redirectToLoginIfPrivate(
    request: NextRequest,
) {

  if (PRIVATE_PATHS.some(regexp => regexp.test(request.nextUrl.pathname))) {
    const url = request.nextUrl.clone();
    url.pathname = '/login';
    url.search = `redirect=${request.nextUrl.pathname}${url.search}`;
    return NextResponse.redirect(url);
  }

  return NextResponse.next();
}

Then, we could use the function in the new configuration:

    handleValidToken: async ({token, decodedToken, customToken}, headers) => {
      return NextResponse.next({
        request: {
          headers
        }
      });
    },
    handleInvalidToken: async (_reason) => {
      return redirectToLoginIfPrivate(request);
    },
    handleError: async (error) => {
      return redirectToLoginIfPrivate(request);
    }

It would be much simpler than initial configuration :-)

@alexllao
Copy link
Author

alexllao commented Nov 5, 2024

Thnks!

@awinogrodzki
Copy link
Owner

No worries, I am glad you brought up the issue. I will provide update to redirectToLogin to support private paths in the future and provide one or two examples of private paths approach. Thanks!

@alexllao
Copy link
Author

alexllao commented Nov 5, 2024

Thnks!!!!

@alexllao
Copy link
Author

alexllao commented Nov 6, 2024

I'm starting with what we talked about yesterday to do something basic mixed with intl. I haven't included the intlMiddleware yet, but I always see MISSING_CREDENTIALS in logs.

`
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import {
authMiddleware,
redirectToHome,
redirectToLogin,
} from "next-firebase-auth-edge";
import createMiddleware from "next-intl/middleware";

import { clientConfig, serverConfig } from "./config/auth-config";
import { routing } from "./i18n/routing";

const PUBLIC_PATHS = [
"/",
"/es",
"/ca",
"/auth/register",
"/auth/login",
"/auth/reset-password",
];

// const PROTECTED_PATHS = ["/(es|ca)/dashboard"];

const intlMiddleware = createMiddleware(routing);

export async function middleware(request: NextRequest) {
return authMiddleware(request, {
loginPath: "/api/login",
logoutPath: "/api/logout",
apiKey: clientConfig.apiKey,
cookieName: serverConfig.cookieName,
cookieSignatureKeys: serverConfig.cookieSignatureKeys,
cookieSerializeOptions: serverConfig.cookieSerializeOptions,
serviceAccount: serverConfig.serviceAccount,
// debug: process.env.NODE_ENV !== "production",

    handleValidToken: async ({ token }, headers) => {
        if (PUBLIC_PATHS.includes(request.nextUrl.pathname) && token) {
            return redirectToHome(request);
        }

        return NextResponse.next({
            request: { headers },
        });
    },
    handleInvalidToken: async (reason) => {
        console.error(reason);

        // Include actual language in the path
        return redirectToLogin(request, {
            path: "/auth/login",
            publicPaths: PUBLIC_PATHS,
        });
    },
    handleError: async (error) => {
        console.error(error);

        // Include actual language in the path
        return redirectToLogin(request, {
            path: "/auth/login",
            publicPaths: PUBLIC_PATHS,
        });
    },
});

}

export const config = {
matcher: [
"/",
"/(es|ca)/:path*",
"/api/login",
"/api/logout",
"/((?!_next|_vercel|api|.\..).*)",
],
};
`

@alexllao
Copy link
Author

alexllao commented Nov 6, 2024

It works!

`import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { authMiddleware } from "next-firebase-auth-edge";
import createMiddleware from "next-intl/middleware";

import { clientConfig, serverConfig } from "./config/auth-config";
import { routing } from "./i18n/routing";
import { withoutLocale } from "./i18n/utils";

const PUBLIC_PATHS_REGEX = process.env.PUBLIC_PATHS
? process.env.PUBLIC_PATHS.split(",").map((path) => new RegExp(^${path}$))
: [];

const PRIVATE_PATHS_REGEX = process.env.PRIVATE_PATHS
? process.env.PRIVATE_PATHS.split(",").map(
(path) => new RegExp(^${path}$),
)
: [];

const isPublicPath = (pathname: string) =>
PUBLIC_PATHS_REGEX.some((regex) => regex.test(pathname));

const isPrivatePath = (pathname: string) =>
PRIVATE_PATHS_REGEX.some((regex) => regex.test(pathname));

const intlMiddleware = createMiddleware(routing);

const redirectToLogin = (request: NextRequest) => {
if (withoutLocale(request.nextUrl.pathname) === "/auth/login") {
return intlMiddleware(request);
}

const url = request.nextUrl.clone();
url.pathname = process.env.LOGIN_PATH || `/auth/login`;
url.searchParams.set("redirect", request.nextUrl.pathname);
return NextResponse.redirect(url);

};

export async function middleware(request: NextRequest) {
// Normalize the pathname to remove the locale prefix
const pathname = withoutLocale(request.nextUrl.pathname);

// If the path is public or not private, we don't need to check for authentication
if (isPublicPath(pathname) || !isPrivatePath(pathname)) {
    return intlMiddleware(request);
}

// Here we check if the user is authenticated
return authMiddleware(request, {
    loginPath: "/api/login",
    logoutPath: "/api/logout",
    apiKey: clientConfig.apiKey,
    cookieName: serverConfig.cookieName,
    cookieSignatureKeys: serverConfig.cookieSignatureKeys,
    cookieSerializeOptions: serverConfig.cookieSerializeOptions,
    serviceAccount: serverConfig.serviceAccount,
    // debug: process.env.NODE_ENV !== "production",
    handleValidToken: async () => {
        return intlMiddleware(request);
    },
    handleInvalidToken: async (reason) => {
        console.info(reason);
        return redirectToLogin(request);
    },
    handleError: async (error) => {
        console.error(error);
        return redirectToLogin(request);
    },
});

}

export const config = {
matcher: [
"/",
"/(es|ca)/:path*",
"/api/login",
"/api/logout",
"/((?!_next|_vercel|.\..).*)",
],
};
`

@awinogrodzki
Copy link
Owner

@alexllao,

If missing credentials happens in handleInvalidToken it can be safely ignored. This is expected :-)

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