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

Farmhash-modern throws webassembly error in Nextjs #2627

Open
B-uchi opened this issue Jul 12, 2024 · 20 comments
Open

Farmhash-modern throws webassembly error in Nextjs #2627

B-uchi opened this issue Jul 12, 2024 · 20 comments
Assignees

Comments

@B-uchi
Copy link

B-uchi commented Jul 12, 2024

[REQUIRED] Step 2: Describe your environment

  • Operating System version: Linux Ubuntu 24.04
  • Firebase Admin SDK version: v12.2.0
  • Firebase Product: Admin auth
  • Node.js version: v20.11.1
  • NPM version: v10.2.4

[REQUIRED] Step 3: Describe the problem

I'm trying to configure firebase-admin SDK on my nextjs + TS project. Whenever i try to invoke any SDK function, i get a webAssembly error. In my case, i am trying to configure a middleware for the server side api, and when i call the verifyIdToken method it throws the error. I'll paste it below, i also noticed that the affected dependency farmhash-modern was just replaced in this version of firebase.

Steps to reproduce:

The error occured when i called the verifyIdToken() method in my middleware.ts file.

Relevant Code:

This is the middleware file.

import { auth } from "./lib/config/firebaseConfig";
import { NextResponse } from "next/server";
import { NextRequest } from "next/server";

interface ExtendedNextRequest extends NextRequest {
  uid?: string;
}

export async function middleware(req: ExtendedNextRequest, res: NextResponse) {
  try {
    let token = req.headers.get("Authorization");
    if (!token)
      return NextResponse.json({ message: "Access Denied" }, { status: 403 });
    if (token.startsWith("Bearer ")) {
      token = token.split(" ")[1];
    }

    let decodedToken = await auth.verifyIdToken(token);
    let uid = decodedToken.uid;
    req.uid = uid;

    return NextResponse.next();
  } catch (error: any) {
    console.log(error.errorInfo);
    return NextResponse.json({ message: "Invalid Token" }, { status: 401 });
  }
}

This is the error output

⨯ ./node_modules/farmhash-modern/bin/bundler/farmhash_modern_bg.wasm
Module parse failed: Unexpected character '' (1:0)
The module seem to be a WebAssembly module, but module is not flagged as WebAssembly module for webpack.
BREAKING CHANGE: Since webpack 5 WebAssembly is not enabled by default and flagged as experimental feature.
You need to enable one of the WebAssembly experiments via 'experiments.asyncWebAssembly: true' (based on async modules) or 'experiments.syncWebAssembly: true' (like webpack 4, deprecated).
For files that transpile to WebAssembly, make sure to set the module type in the 'module.rules' section of the config (e. g. 'type: "webassembly/async"').
(Source code omitted for this binary file)

Import trace for requested module:
./node_modules/farmhash-modern/bin/bundler/farmhash_modern_bg.wasm
./node_modules/farmhash-modern/bin/bundler/farmhash_modern.js
./node_modules/farmhash-modern/lib/browser.js
./node_modules/firebase-admin/lib/remote-config/condition-evaluator-internal.js
./node_modules/firebase-admin/lib/remote-config/remote-config.js
./node_modules/firebase-admin/lib/app/firebase-namespace.js
./node_modules/firebase-admin/lib/default-namespace.js
./node_modules/firebase-admin/lib/index.js
./lib/config/firebaseConfig.ts

This question is also on stackoverflow

@google-oss-bot
Copy link

I couldn't figure out how to label this issue, so I've labeled it for a human to triage. Hang tight.

@lahirumaramba
Copy link
Member

Does configuring webpack by setting asyncWebAssembly to true in your next.config.js work for you?
Something similar to the following:

module.exports = {
  webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {
    config.experiments = {
      asyncWebAssembly: true,
    };
    return config;
  },
};

@Keegan-lee
Copy link

I'm getting the same issue.. This code does seem to "fix" the issue, but src/middleware is now failing (hanging) to compile.

Does configuring webpack by setting asyncWebAssembly to true in your next.config.js work for you? Something similar to the following:

module.exports = {
  webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {
    config.experiments = {
      asyncWebAssembly: true,
    };
    return config;
  },
};

@raymelon
Copy link

raymelon commented Jul 22, 2024

Experiencing the same issue. For now, I followed the workaround at #2552 (comment), which is to downgrade to "firebase-admin": "^12.0.0",

Credits to @chandrasekhar2039

@matija2209
Copy link

matija2209 commented Jul 26, 2024

Downgrading to 12.0.0 did not work for me.

Module parse failed: Unexpected character '' (1:0)
The module seem to be a WebAssembly module, but module is not flagged as WebAssembly module for webpack.
BREAKING CHANGE: Since webpack 5 WebAssembly is not enabled by default and flagged as experimental feature.
You need to enable one of the WebAssembly experiments via 'experiments.asyncWebAssembly: true' (based on async modules) or 'experiments.syncWebAssembly: true' (like webpack 4, deprecated).
For files that transpile to WebAssembly, make sure to set the module type in the 'module.rules' section of the config (e. g. 'type: "webassembly/async"').
(Source code omitted for this binary file)

@B-uchi
Copy link
Author

B-uchi commented Jul 26, 2024

Does configuring webpack by setting asyncWebAssembly to true in your next.config.js work for you?
Something similar to the following:

module.exports = {
  webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {
    config.experiments = {
      asyncWebAssembly: true,
    };
    return config;
  },
};

I also tried this, but as @Keegan-lee said src/middleware doesn't compile. It displays the compiling middleware message and stays there forever.

@nicolopadovan
Copy link

nicolopadovan commented Jul 26, 2024

I am experiencing the same exact issue.
Adding the asyncWebAssembly flag indeed causes middleware to compile indefinetely.
Switching back to ^12.0.0 doesn't work either.

Workaround
For now, I have moved all the pages that needed that middleware into a specific route (can also be an anonymous route).
I am using the layout file as a kind of middleware: before displaying any data it runs a server action, where the error is not yielded.

Also, using ^12.0.0 raised an error regarding the usage of the os module, which may be a clue on where to debug this issue.

Edit: use export const revalidate = false on the layout to ensure no caching

@Lucasch1
Copy link

Lucasch1 commented Aug 14, 2024

Am having the same problem here, searched the best i could and it seems that there is no direct solution yet...

The best that i had saw is utilize another lib called next-firebase-auth-edge (https://github.com/awinogrodzki/next-firebase-auth-edge)

@nicolopadovan
Copy link

Am having the same problem here, searched the best i could and it seems that there is no direct solution yet...

The best that i had saw is utilize another lib called next-firebase-auth-edge (https://github.com/awinogrodzki/next-firebase-auth-edge)

I am guessing that Firebase Auth cannot be used on Edge runtime due to the need for some Node-only modules.
FWIW I suggest to use this library with extreme caution due to how it manipulates and generates tokens and other security-related Firebase Auth features.

@hassanzadeh
Copy link

Hey Guys,
I'm having the same issue, but I completely replaced auth part of firebase-admin with next-firebase-auth-edge, only using admin part and firestore, should I still be getting this error?

@nicolopadovan
Copy link

Hey Guys, I'm having the same issue, but I completely replaced auth part of firebase-admin with next-firebase-auth-edge, only using admin part and firestore, should I still be getting this error?

You shouldn’t get the error, the package should be able to smoothly run on all NextJS environments.

I however renew my suggestion of being cautious when using the package

@hassanzadeh
Copy link

Hey Guys, I'm having the same issue, but I completely replaced auth part of firebase-admin with next-firebase-auth-edge, only using admin part and firestore, should I still be getting this error?

You shouldn’t get the error, the package should be able to smoothly run on all NextJS environments.

I however renew my suggestion of being cautious when using the package

Are you sure? I see plenty of people complaining about the same thing (that it won't work when using middleware). I'm using firestore and admin (but not auth) from firebase-admin and it leads to that error.

@nicolopadovan
Copy link

Hey Guys, I'm having the same issue, but I completely replaced auth part of firebase-admin with next-firebase-auth-edge, only using admin part and firestore, should I still be getting this error?

You shouldn’t get the error, the package should be able to smoothly run on all NextJS environments.
I however renew my suggestion of being cautious when using the package

Are you sure? I see plenty of people complaining about the same thing (that it won't work when using middleware). I'm using firestore and admin (but not auth) from firebase-admin and it leads to that error.

How are you importing the library?

Also, yes I am sure, as per the documentation of the package:

The official firebase-admin library depends heavily on Node.js’s internal crypto library, which isn’t available in [Next.js Edge Runtime](https://nextjs.org/docs/api-reference/edge-runtime).

This library solves that problem by handling the creation and verification of [Custom ID Tokens](https://firebase.google.com/docs/auth/admin/verify-id-tokens) using the Web Crypto API, which works in Edge runtimes

@hassanzadeh
Copy link

hassanzadeh commented Sep 16, 2024

Here is my middleware,

import type { NextRequest, NextResponse } from "next/server";
import { authMiddleware } from "next-firebase-auth-edge/lib/next/middleware";
import { notFound } from "next/navigation"; // To send a 404 response
import { loadFirebaseCredentials } from "@lib/firebase-init";

const PROTECTED_PATH_REGEX = /^\/dashboard(\/|$)/; // Match `/dashboard` and sub-paths like `/dashboard/*`
const ADMIN_PATH_REGEX = /^\/admin(\/|$)/; // Match `/adminchi` and sub-paths like `/adminchi/*`

function redirectToLogin(request: NextRequest) {
  const url = request.nextUrl.clone();
  url.pathname = "/login";
  url.search = `redirect=${request.nextUrl.pathname}${url.search}`;
  return NextResponse.redirect(url);
}

function redirectToHome(request: NextRequest) {
  const url = request.nextUrl.clone();
  url.pathname = "/";
  url.search = "";
  return NextResponse.redirect(url);
}

export async function middleware(request: NextRequest) {
  const serviceAccount = loadFirebaseCredentials();

  return authMiddleware(request, {
    loginPath: "/api/users/login",
    logoutPath: "/api/users/logout",
    apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY!,
    cookieName: "AuthToken",
    cookieSerializeOptions: {
      path: "/",
      httpOnly: true,
      secure: !!process.env.NEXT_PUBLIC_SITE_URL?.startsWith("https"),
      sameSite: "lax",
      maxAge: 12 * 60 * 60 * 24, // twelve days
    },
    cookieSignatureKeys: [process.env.COOKIE_SIGNATURE_KEY!],
    serviceAccount,
    handleValidToken: async ({ token, decodedToken }, headers) => {
      // Check if user is accessing an admin route
      if (ADMIN_PATH_REGEX.test(request.nextUrl.pathname)) {
        const isAdminUser = /*await getAdminTokenFromApi();*/false // Check if the user is admin
        if (!isAdminUser) {
          // If the user is logged in but not an admin, return a 404 response
          return notFound();
        }
      }

      // Restrict access to any path that matches `/dashboard` or starts with `/dashboard/`
      if (PROTECTED_PATH_REGEX.test(request.nextUrl.pathname)) {
        return NextResponse.next({
          request: {
            headers,
          },
        });
      }

      // Allow access to public pages
      return NextResponse.next({
        request: {
          headers,
        },
      });
    },
    handleInvalidToken: async () => {
      // If the user tries to access an admin route without authentication, return a 404 response
      if (ADMIN_PATH_REGEX.test(request.nextUrl.pathname)) {
        return notFound();
      }

      // Redirect to login if accessing protected routes like `/dashboard`
      if (PROTECTED_PATH_REGEX.test(request.nextUrl.pathname)) {
        return redirectToLogin(request);
      }

      // Allow access to public pages without a valid token
      return NextResponse.next();
    },
    handleError: async (error) => {
      console.error("Unhandled authentication error", { error });
      return redirectToLogin(request);
    },
  });
}

export const config = {
  matcher: [
    "/((?!_next|favicon.ico|api|.*\\.).*)", // All paths except static files and API calls
    "/api/login",
    "/api/logout",
  ],
};

and here is the stack trace,

  Import trace for requested module:
  ./node_modules/farmhash-modern/bin/bundler/farmhash_modern_bg.wasm
  ./node_modules/farmhash-modern/bin/bundler/farmhash_modern.js
  ./node_modules/farmhash-modern/lib/browser.js
  ./node_modules/firebase-admin/lib/remote-config/condition-evaluator-internal.js
  ./node_modules/firebase-admin/lib/remote-config/remote-config.js
  ./node_modules/firebase-admin/lib/app/firebase-namespace.js
  ./node_modules/firebase-admin/lib/default-namespace.js
  ./node_modules/firebase-admin/lib/index.js
  ./lib/firebase-init.ts

and here is the content of firebase-init.ts file:

import * as admin from "firebase-admin";
import { getTokens } from "next-firebase-auth-edge";
import { cookies } from "next/headers";
import fs from "fs";
import path from "path";

// Function to load credentials from GOOGLE_APPLICATION_CREDENTIALS, environment variables, or service-account.json
export function loadFirebaseCredentials() {
  // Check if GOOGLE_APPLICATION_CREDENTIALS is set
  if (process.env.GOOGLE_APPLICATION_CREDENTIALS) {
    const serviceAccountPath = process.env.GOOGLE_APPLICATION_CREDENTIALS;

    if (fs.existsSync(serviceAccountPath)) {
      const serviceAccount = JSON.parse(
        fs.readFileSync(serviceAccountPath, "utf8")
      );

      return {
        projectId: serviceAccount.project_id,
        privateKey: serviceAccount.private_key,
        clientEmail: serviceAccount.client_email,
      };
    } else {
      throw new Error(
        `Service account file not found at path: ${serviceAccountPath}`
      );
    }
  }

  // If GOOGLE_APPLICATION_CREDENTIALS is not set, fall back to environment variables
  if (
    process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID &&
    process.env.FIREBASE_PRIVATE_KEY &&
    process.env.FIREBASE_CLIENT_EMAIL
  ) {
    return {
      projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
      privateKey: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, "\n"),
      clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
    };
  }

  // As a final fallback, load the service-account.json file from the project directory
  const serviceAccountPath = path.join(process.cwd(), "service-account.json");
  if (fs.existsSync(serviceAccountPath)) {
    const serviceAccount = JSON.parse(
      fs.readFileSync(serviceAccountPath, "utf8")
    );
    return {
      projectId: serviceAccount.project_id,
      privateKey: serviceAccount.private_key,
      clientEmail: serviceAccount.client_email,
    };
  } else {
    throw new Error(
      "Service account file not found and environment variables are missing."
    );
  }
}

// Initialize Firebase Admin SDK for Firestore if it's not already initialized
if (!admin.apps?.length) {
  const credentials = loadFirebaseCredentials();
  const credential = admin.credential.cert({
    projectId: credentials.projectId,
    privateKey: credentials.privateKey,
    clientEmail: credentials.clientEmail,
  });

  admin.initializeApp({
    credential,
  });
}

// Initialize Firestore for server-side use
const db = admin.firestore();

// Use `next-firebase-auth-edge` to handle authentication in edge/serverless environments
export async function getAuthToken(cookieName = "AuthToken") {
  try {
    // Get the token from the request's cookies based on the cookieName passed
    const cookieStore = cookies();
    const tokenCookie = cookieStore.get(cookieName)?.value;

    if (!tokenCookie) {
      throw new Error(`No token found in the cookie "${cookieName}"`);
    }

    const credentials = loadFirebaseCredentials();

    // Pass the entire cookies object and specify the cookie to use for validation
    const tokenData = await getTokens(cookies(), {
      apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY!,
      cookieName, // Dynamically use the cookie name passed to the function
      cookieSignatureKeys: [process.env.COOKIE_SIGNATURE_KEY!], // Ensure this is a 32-byte key for signing
      serviceAccount: {
        projectId: credentials.projectId,
        clientEmail: credentials.clientEmail,
        privateKey: credentials.privateKey,
      },
    });

    // Return the decoded token or throw an error if no token is found
    if (!tokenData || !tokenData.decodedToken) {
      throw new Error("Invalid or missing token");
    }

    return tokenData.decodedToken; // Return the decoded ID token
  } catch (error) {
    console.error(
      `Error fetching authentication token from cookie "${cookieName}":`,
      error
    );
    throw new Error("Authentication failed");
  }
}

// Firestore and Admin SDK export
export { db, admin };

// Action code settings for email verification, etc.
export const actionCodeSettings = (id: string, url: string) => ({
  url,
  handleCodeInApp: true,
  iOS: {
    bundleId: `com.${id}.ios`,
  },
  android: {
    packageName: `com.${id}.android`,
  },
  dynamicLinkDomain: process.env.DYNAMIC_LINK_DOMAIN,
});

@hassanzadeh
Copy link

updated my response above.

@brouza
Copy link

brouza commented Sep 17, 2024

As pointed out earlier, Firebase seems to work only in the Node server.
My workaround was to create an API endpoint with call API/session/route and export a response as JSON from the @auth session, then call the auth API in the middleware.
This works well for me, but I'm unsure about its security implications.

// /api/auth/session
import { auth } from "@/auth"

export async function GET(_request: Request){
const session = await auth()
if(!session) return Response.json(null, { status: 401 })
return new Response(JSON.stringify(session), {
status: 200,
})
}

// middleware.ts
export async function middleware(_request: NextRequest) {
const res = await fetch(new URL('/api/auth/session', _request.url), {
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
})
const schema = z.custom<Session | null>()
const isAuthenticated = schema.parse(await res.json())

console.log(isAuthenticated)

const respond = NextResponse.next()

return respond
}

@nicolopadovan
Copy link

First of all, I suggest that you all try to format the code in the thread correctly. You can do so via backticks.
Also, try to keep the thread clean! Don't turn it into a StackOverflow question! :D

@hassanzadeh I can't test your code right now, but the issue might lie in the fact that you are still using firebase-admin within firebase-init.ts, which you are then importing in your middleware.

@brouza Seems like an okay workaround as far as I'm concerned; you could even try using a server action to abstract away the REST paradigm. I would at least try to secure the endpoint, however, even though it's a server-to-server communication. There might be more security implications as well.

I am as well awaiting an official response regarding this issue.

@hassanzadeh
Copy link

Thanks @nicolopadovan ,
I updated my response to fix the backticks issue.
I'm not sure I understood your point, I'm importing firebase-init, but all I'm using there is admin and db, but not auth, is that a problem?

@hassanzadeh
Copy link

As pointed out earlier, Firebase seems to work only in the Node server. My workaround was to create an API endpoint with call API/session/route and export a response as JSON from the @auth session, then call the auth API in the middleware. This works well for me, but I'm unsure about its security implications.

// /api/auth/session import { auth } from "@/auth"

export async function GET(_request: Request){ const session = await auth() if(!session) return Response.json(null, { status: 401 }) return new Response(JSON.stringify(session), { status: 200, }) }

// middleware.ts export async function middleware(_request: NextRequest) { const res = await fetch(new URL('/api/auth/session', _request.url), { method: 'GET', credentials: 'include', headers: { 'Content-Type': 'application/json', }, }) const schema = z.custom<Session | null>() const isAuthenticated = schema.parse(await res.json())

console.log(isAuthenticated)

const respond = NextResponse.next()

return respond }

Thanks, I first need to know why my current code is not working especially that I'm not using firebase-admin auht at all.

@hassanzadeh
Copy link

Ok, I think you are right.
Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests