kubetail-org / edge-csrf

CSRF protection library for JavaScript that runs on the edge runtime (with Next.js, SvelteKit, Express, Node-HTTP integrations)
MIT License
140 stars 7 forks source link

How to update middleware to latest version #39

Closed devrenzz closed 3 months ago

devrenzz commented 3 months ago

Hello, I have this old edge-csrf version 1.09 and it working fine but with the new version. I don't have any idea how I can update it based on README in NextJS. As of now, I handle protected routes thru middleware and also I don't want to run the csrf validation if user is on login page. Thankyou!

import csrf from 'edge-csrf';
// Access Allowed Only for Signed-In Users:
const PrivatePaths: string[] = [ROUTES.Profiles];

// Access Denied for Signed-In Users:
const RestrictedPaths: string[] = ['/auth'];

const imageExtensions = ['.png', '.jpg', '.jpeg', '.svg', '.gif', '.webp'];

export const config = {
  matcher: [
    /*
     * Match all paths except for:
     * 1. /api routes
     * 2. /_next (Next.js internals)
     * 3. /_static (inside /public)
     * 4. all root files inside /public (e.g. /favicon.ico)
     */
    '/((?!api/|_next/|_static/|_vercel|[\\w-]+\\.\\w+).*)'
    // '/((?!_next/static|_next/image|favicon.ico).*)'
  ]
};

// initalize protection function
const csrfProtect = csrf({
  cookie: {
    secure: process.env.NODE_ENV === 'production',
    name: CSRF_TOKEN_KEY
  }
});

export default async function middleware(req: NextRequest) {
  const { supabase, response: res } = createMiddlewareClient(req);

  const url = req.nextUrl.clone();

  //skip if image
  if (imageExtensions.some((ext) => url.pathname.includes(ext))) return res;

  const {
    data: { user }
  } = await supabase.auth.getUser();

  const path = url.pathname;
  const hasAccountType = user?.user_metadata?.account_type;

  if (!path.startsWith('/auth')) {
    // csrf protection
    const csrfError = await csrfProtect(req, res);
    // check result
    if (csrfError) {
      return new NextResponse('Invalid csrf token', { status: 403 });
    }
  }

  if (req.nextUrl.pathname === '/csrf-token') {
    return NextResponse.json({
      csrfToken: res.headers.get('X-CSRF-Token') || 'missing'
    });
  }

  const aPrivatePath = Boolean(
    PrivatePaths.find((pathname) => path.includes(pathname))
  );
  const aRestrictedPath = Boolean(
    RestrictedPaths.find((pathname) => path.includes(pathname))
  );

  if (user && aPrivatePath && !hasAccountType) {
    return NextResponse.redirect(new URL(`/account-type`, req.url));
  }

  if (!user && aPrivatePath) {
    return NextResponse.redirect(new URL(ROUTES.Login, req.url));
  }

  if (user && aRestrictedPath) {
    const team = await getLatestTeam(user.id);

    const latestProfile = team?.profiles[team.profiles.length - 1];
    const route = `${ROUTES.Profiles}/${latestProfile?.uuid}`;

    return NextResponse.redirect(new URL(`/${team?.slug}${route}`, req.url));
  }

  if (path === '/') {
    return NextResponse.redirect(new URL(ROUTES.Login, req.url));
  }

  if (
    path === ROUTES.EmailConfirmation &&
    (!url.searchParams.has('email') ||
      !url.searchParams.get('email')?.match(REGEX.Email))
  ) {
    return NextResponse.redirect(new URL(ROUTES.Login, req.url));
  }

  return res;
}
amorey commented 3 months ago

To migrate use the createCsrfProtect named export and catch the CsrfError error instead of looking at the response object.

Try this:

import { CsrfError, createCsrfProtect } from '@edge-csrf/nextjs';
// Access Allowed Only for Signed-In Users:
const PrivatePaths: string[] = [ROUTES.Profiles];

// Access Denied for Signed-In Users:
const RestrictedPaths: string[] = ['/auth'];

const imageExtensions = ['.png', '.jpg', '.jpeg', '.svg', '.gif', '.webp'];

export const config = {
  matcher: [
    /*
     * Match all paths except for:
     * 1. /api routes
     * 2. /_next (Next.js internals)
     * 3. /_static (inside /public)
     * 4. all root files inside /public (e.g. /favicon.ico)
     */
    '/((?!api/|_next/|_static/|_vercel|[\\w-]+\\.\\w+).*)'
    // '/((?!_next/static|_next/image|favicon.ico).*)'
  ]
};

// initalize protection function
const csrfProtect = createCsrfProtect({
  cookie: {
    secure: process.env.NODE_ENV === 'production',
    name: PRESSCART_CSRF_TOKEN_KEY
  }
});

export default async function middleware(req: NextRequest) {
  const { supabase, response: res } = createMiddlewareClient(req);

  const url = req.nextUrl.clone();

  //skip if image
  if (imageExtensions.some((ext) => url.pathname.includes(ext))) return res;

  const {
    data: { user }
  } = await supabase.auth.getUser();

  const path = url.pathname;
  const hasAccountType = user?.user_metadata?.account_type;

  if (!path.startsWith('/auth')) {
    // csrf protection
    try {
      await csrfProtect(req, res);
    } catch (err) {
      if (err instanceof CsrfError) return new NextResponse('invalid csrf token', { status: 403 });
      throw err;
    }
  }

  if (req.nextUrl.pathname === '/csrf-token') {
    return NextResponse.json({
      csrfToken: res.headers.get('X-CSRF-Token') || 'missing'
    });
  }

  const aPrivatePath = Boolean(
    PrivatePaths.find((pathname) => path.includes(pathname))
  );
  const aRestrictedPath = Boolean(
    RestrictedPaths.find((pathname) => path.includes(pathname))
  );

  if (user && aPrivatePath && !hasAccountType) {
    return NextResponse.redirect(new URL(`/account-type`, req.url));
  }

  if (!user && aPrivatePath) {
    return NextResponse.redirect(new URL(ROUTES.Login, req.url));
  }

  if (user && aRestrictedPath) {
    const team = await getLatestTeam(user.id);

    const latestProfile = team?.profiles[team.profiles.length - 1];
    const route = `${ROUTES.Profiles}/${latestProfile?.uuid}`;

    return NextResponse.redirect(new URL(`/${team?.slug}${route}`, req.url));
  }

  if (path === '/') {
    return NextResponse.redirect(new URL(ROUTES.Login, req.url));
  }

  if (
    path === ROUTES.EmailConfirmation &&
    (!url.searchParams.has('email') ||
      !url.searchParams.get('email')?.match(REGEX.Email))
  ) {
    return NextResponse.redirect(new URL(ROUTES.Login, req.url));
  }

  return res;
}
amorey commented 3 months ago

Did this work? Let me know if you're still having trouble upgrading.

devrenzz commented 3 months ago

Did this work? Let me know if you're still having trouble upgrading.

Hello, sorry for the late reply. It works perfectly. Thank you so much for the quick response!

amorey commented 3 months ago

Awesome! Happy to hear it's working.