awinogrodzki/next-firebase-auth-edge

Error: MISSING_CREDENTIALS: Missing credentials

steve-marmalade opened this issue · 33 comments

Hello! I am seeing lots of log lines like the following:

Error: MISSING_CREDENTIALS: Missing credentials

I believe this came after updating to 1.8 but not 100% sure.

Can you provide some more background on this? For reference, it's expected that a lot of our traffic is logged-out so I don't think of missing credentials as an error state. So is this something I should ignore (and if so, is there a way to silence this message?) or is it indicating some kind of configuration error on my part?

Thank you.

Hey @steve-marmalade,

Thanks for reporting!

In 1.8 I overhauled how credentials are parsed and serialized into cookies, which might've introduced this issue.

Do I understand that you get this error in handleError callback?

Could you provide more details, ideally a stack trace?

Hi @awinogrodzki, I just started using v1.8.0 and similarly I'm also seeing this error in my logs whenever the authMiddleware is run. Here is the stack trace on my end:

    at (node_modules/.pnpm/next-firebase-auth-edge@1.8.0_next@14.2.9_@babel+core@7.25.2_react-dom@18.3.1_react@18.3.1__react@18.3.1_/node_modules/next-firebase-auth-edge/browser/next/cookies/parser/SingleCookieParser.js:13:0)
    at (node_modules/.pnpm/next-firebase-auth-edge@1.8.0_next@14.2.9_@babel+core@7.25.2_react-dom@18.3.1_react@18.3.1__react@18.3.1_/node_modules/next-firebase-auth-edge/browser/next/tokens.js:18:0)
    at (node_modules/.pnpm/next-firebase-auth-edge@1.8.0_next@14.2.9_@babel+core@7.25.2_react-dom@18.3.1_react@18.3.1__react@18.3.1_/node_modules/next-firebase-auth-edge/browser/next/middleware.js:107:29)
    at (middleware.ts:14:9)
    at (node_modules/.pnpm/next@14.2.9_@babel+core@7.25.2_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/next/dist/esm/server/web/adapter.js:175:17)
    at (node_modules/.pnpm/next@14.2.9_@babel+core@7.25.2_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/next/dist/esm/server/async-storage/request-async-storage-wrapper.js:95:0)
    at (node_modules/.pnpm/next@14.2.9_@babel+core@7.25.2_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/next/dist/esm/server/web/adapter.js:166:45)
    at (node_modules/.pnpm/next@14.2.9_@babel+core@7.25.2_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/next/dist/esm/server/lib/trace/tracer.js:114:0)
    at (node_modules/.pnpm/next@14.2.9_@babel+core@7.25.2_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/next/dist/compiled/@opentelemetry/api/index.js:1:7052)
    at (node_modules/.pnpm/next@14.2.9_@babel+core@7.25.2_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/next/dist/compiled/@opentelemetry/api/index.js:1:480)

Hey @teresal92 !

Thanks for chiming in! Could you share your middleware.ts file?

I am interested in what's on line 14: at (middleware.ts:14:9)

I am also getting the same error, after having following the docs.

Here is my middleware.ts:

import type { NextRequest } from "next/server";
import { authMiddleware } from "next-firebase-auth-edge";

export async function middleware(request: NextRequest) {
  console.log(process.env.NEXT_PUBLIC_FIREBASE_API_KEY)
  return authMiddleware(request, {
    debug: true,
    loginPath: "/api/login",
    logoutPath: "/api/logout",
    apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
    cookieName: "AuthToken",
    cookieSignatureKeys: ["Key-Should-Be-at-least-32-bytes-in-length"],
    cookieSerializeOptions: {
      path: "/",
      httpOnly: true,
      secure: false, // Set this to true on HTTPS environments
      sameSite: "lax" as const,
      maxAge: 12 * 60 * 60 * 24, // Twelve days
    },
    serviceAccount: {
      projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
      clientEmail: process.env.NEXT_PUBLIC_FIREBASE_CLIENT_EMAIL,
      privateKey: process.env.NEXT_FIREBASE_PRIVATE_KEY,
    },
  });
}

export const config = {
  matcher: ["/api/login", "/api/logout", "/", "/((?!_next|favicon.ico|api|.*\\.).*)"],
};

Thank you @goncaloalves! Would you be able to share the stack trace as well? I just want to rule out different sources for the issue

i just have this:

ⓘ next-firebase-auth-edge: Handle request                                                                                                                         
path: /signin
ⓘ next-firebase-auth-edge: Token is missing or has incorrect formatting. This is expected and usually means that user has not yet logged in                       
message: MISSING_CREDENTIALS: Missing credentials                                                                                                        
reason: MISSING_CREDENTIALS                                                                                                                             
 stack: Error: MISSING_CREDENTIALS: Missing credentials
    at SingleCookieParser.parseCookies (webpack-internal:///(middleware)/./node_modules/next-firebase-auth-edge/browser/next/cookies/parser/SingleCookieParser.js:20:19)
    at getRequestCookiesTokens (webpack-internal:///(middleware)/./node_modules/next-firebase-auth-edge/browser/next/tokens.js:37:19)
    at authMiddleware (webpack-internal:///(middleware)/./node_modules/next-firebase-auth-edge/browser/next/middleware.js:126:97)
    at Object.middleware [as handler] (webpack-internal:///(middleware)/./middleware.ts:10:83)
    at eval (webpack-internal:///(middleware)/./node_modules/next/dist/esm/server/web/adapter.js:197:31)
    at AsyncLocalStorage.run (node:async_hooks:346:14)
    at Object.wrap (webpack-internal:///(middleware)/./node_modules/next/dist/esm/server/async-storage/request-async-storage-wrapper.js:105:24)
    at eval (webpack-internal:///(middleware)/./node_modules/next/dist/esm/server/web/adapter.js:188:122)
    at eval (webpack-internal:///(middleware)/./node_modules/next/dist/esm/server/lib/trace/tracer.js:115:36)
    at NoopContextManager.with (webpack-internal:///(middleware)/./node_modules/@opentelemetry/api/build/esm/context/NoopContextManager.js:58:24)

@goncaloalves does the issue still log when you change to debug: false, in authMiddleware?

yes, I placed debug true to try and debug it myself.

Hey @awinogrodzki, here's my middleware.ts:

line 14 is returning authMiddleware

import {
  authMiddleware,
  redirectToPath,
  redirectToLogin,
} from 'next-firebase-auth-edge'
import { serverConfig, clientConfig } from './lib/firebase/config'

const PUBLIC_PATHS = ['/signup', '/login', '/forgot-password', '/']
const PROTECTED_PATHS = ['/home', '/home/onboarding']

export async function middleware(request: NextRequest) {
  // https://github.com/awinogrodzki/next-firebase-auth-edge/blob/main/examples/next-typescript-minimal/middleware.ts
  return authMiddleware(request, {
    loginPath: '/api/login',
    logoutPath: '/api/logout',
    apiKey: clientConfig.apiKey,
    cookieName: serverConfig.cookieName,
    cookieSignatureKeys: serverConfig.cookieSignatureKeys,
    cookieSerializeOptions: serverConfig.cookieSerializeOptions,
    serviceAccount: serverConfig.serviceAccount,
    handleValidToken: async ({ token }, headers) => {
      // Authenticated user should not be able to access /login, /signup and /forgot-password routes
      if (PUBLIC_PATHS.includes(request.nextUrl.pathname) && token) {
        return redirectToPath(request, '/home')
      }

      return NextResponse.next({
        request: {
          headers,
        },
      })
    },
    handleInvalidToken: async (reason) => {
      if (PROTECTED_PATHS.includes(request.nextUrl.pathname)) {
        console.info('Missing or malformed credentials', { reason })

        // redirect to /login if the user is not authenticated
        return redirectToLogin(request, {
          path: '/login',
          publicPaths: PUBLIC_PATHS,
        })
      }
      return NextResponse.next()
    },
    handleError: async (error) => {
      console.error('Unhandled authentication error', { error })

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

export const config = {
  matcher: [
    '/api/login',
    '/api/logout',
    '/((?!api|_next/static|_next/image|.*\\.).*)',
  ],
}

Thank you @teresal92!

That's interesting, I am not able to reproduce the issue myself – @teresal92, do you have the rest of the stack trace? Does it log out as Unhandled authentication error or Missing or malformed credentials?

Ultimately, I am most interested if the error is logged in handleInvalidToken or handleError method.
console.info('Missing or malformed credentials', { reason }) is purely informative and can be safely removed. handleInvalidToken is expected to be called.

If the error is logged out insidehandleError, then it's something that I need to pin-point and fix

There is also a third case where the error is unhandled by neither handleInvalidToken nor handleError, which would also be identifiable by the rest of the stack trace

@goncaloalves could you share the logs of the error with debug: false? I want to understand if the error is somehow recoverable. If yes, what is the source of it

I'm seeing the same issue, btw - it started with the 1.8 update.

Error: MISSING_CREDENTIALS: Missing credentials at (../../node_modules/next-firebase-auth-edge/browser/next/cookies/parser/SingleCookieParser.js:13:0)

Everything works fine in localhost, but I get the error on my Vercel staging site.

Hey @michael5r,

Does the app stop from working after this error, or is it recoverable?

Could you share the rest of the stack-trace? SingleCookieParser can be used in multiple places. I need to understand whether the issue originates from getTokens, authMiddleware or one of the error handling functions

I think I was finally able to reproduce the issue on Vercel – thanks everyone for cooperation – I will start working on a fix shortly

It seems that authMiddleware error is not catched by neither handleInvalidError nor handleError. The following condition returns false on some Edge Middleware executions:

error instanceof InvalidTokenError

It's not consistent. Most of the time the check works as expected. I think it has something to do with a changes I did around ESM, Browser and Node modules. One reason for it might be that Vercel is using different type of InvalidTokenError constructor between throwing an error and checking it's instance using instanceof operator.

I've added some additional logs in latest canary release and will provide further updates soon

I think I know what's happening. Looks like Vercel is trying to guess and log errors that resulted from unhandled promises. It may involve a Proxy over Promise and Error classes.

The app handles the error correctly, but despite that it is picked up by Vercel and logged as error. It can be somehow related to the v1.8 though

@steve-marmalade, @teresal92, @goncaloalves, @michael5r can I ask you to confirm that you all see the error in Vercel, but not locally (with debug: false)? Most likely it's a false positive due to some change on how Vercel reports Errors.

I will research further if we could mark this as error as handled in Vercel

@awinogrodzki Yep, it doesn't happen locally - only on Vercel.

I have found a potential fix for the issue.

It's related to the problem mentioned in the previous comment.
The details can be found in PR description

Could you install next-firebase-auth-edge@1.8.2-canary.10 and let me know if it works for you?

@awinogrodzki Fixed it for me. Thanks!

Awesome @laurensnl! Thanks for the update. I will prepare 1.8.2 release shortly 👍

Fixed it for me as well - thanks!

The fix was released in next-firebase-auth-edge@1.8.2. Thanks all for helping with finding the issue! 🎉

Thanks for the deep-dive @awinogrodzki (esp since this seems like a false-positive error)! Confirmed that I am no longer seeing these log messages after upgrading to 1.8.2

Thanks @awinogrodzki so much! this fixed the issue for me as well. apologies for the delayed response.

Hi,

I am still getting this error. It works perfectly locally but once deployed to Firebase, I am getting the following error:

textPayload: "Missing or malformed credentials { reason: 'MISSING_CREDENTIALS' }"

Cheers
Olaf

Hey @nolafs,

Could you share the stack trace? If the missing credentials is logged with reason, probably you are logging it in "handleInvalidToken" method. In such case "missing credentials" is expected

This is my logs with debug turned on:

DEFAULT 2024-11-12T17:17:43.291599Z ⓘ next-firebase-auth-edge: Handle request
DEFAULT 2024-11-12T17:17:43.291721Z path: /login
DEFAULT 2024-11-12T17:17:43.291858Z ⓘ next-firebase-auth-edge: Attempt to fetch request cookies tokens
DEFAULT 2024-11-12T17:17:43.292150Z ⓘ next-firebase-auth-edge: Extracted jwt cookie
DEFAULT 2024-11-12T17:17:43.292161Z ⓘ next-firebase-auth-edge: Jwt cookie not found. Throwing InvalidTokenError.
DEFAULT 2024-11-12T17:17:43.292531Z ⓘ next-firebase-auth-edge: Token is missing or has incorrect formatting. This is expected and usually means that user has not yet logged in
DEFAULT 2024-11-12T17:17:43.292690Z message: MISSING_CREDENTIALS: Missing credentials
DEFAULT 2024-11-12T17:17:43.292699Z reason: MISSING_CREDENTIALS
DEFAULT 2024-11-12T17:17:43.292749Z stack: Error: MISSING_CREDENTIALS: Missing credentials at iq.parseCookies (/workspace/.next/server/src/middleware.js:15:55426) at i0 (/workspace/.next/server/src/middleware.js:15:60042) at no (/workspace/.next/server/src/middleware.js:15:64528) at nc (/workspace/.next/server/src/middleware.js:15:66531) at handler (/workspace/.next/server/src/middleware.js:15:67735) at AsyncLocalStorage.run (node:async_hooks:346:14) at /workspace/.next/server/src/middleware.js:13:32396 at AsyncLocalStorage.run (node:async_hooks:346:14) at /workspace/.next/server/src/middleware.js:13:32383 at /workspace/.next/server/src/middleware.js:13:20903
DEFAULT 2024-11-12T17:17:43.292955Z Missing or malformed credentials { reason: 'MISSING_CREDENTIALS' }

Thanks for sharing the logs @nolafs.

Can I ask you also to share your middleware.ts file?

import {NextRequest, NextResponse} from "next/server";
import {authMiddleware, redirectToHome, redirectToLogin} from "next-firebase-auth-edge";
import {clientConfig, serverConfig} from "./config";

const PUBLIC_PATHS = ['/register', '/login'];

export async function middleware(request: NextRequest) {

  
  console.info('Request to', serverConfig);

  return authMiddleware(request, {
    debug: serverConfig.debug,
    loginPath: "/api/login",
    logoutPath: "/api/logout",
    apiKey: clientConfig.apiKey,
    cookieName: serverConfig.cookieName,
    cookieSignatureKeys: serverConfig.cookieSignatureKeys,
    cookieSerializeOptions: serverConfig.cookieSerializeOptions,
    serviceAccount: serverConfig.serviceAccount,
    handleValidToken: async ({token, decodedToken, customToken}, headers) => {
      console.info('Authenticated', token, decodedToken, customToken);
      // 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) => {
      console.info('Missing or malformed credentials', {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
      });
    }
  });
}

export const config = {
  matcher: [
    "/",
    "/((?!_next|api|.*\\.).*)",
    "/api/login",
    "/api/logout",
  ],
};
export const serverConfig = {
  debug: true,
  cookieName: process.env.AUTH_COOKIE_NAME!,
  cookieSignatureKeys: [process.env.AUTH_COOKIE_SIGNATURE_KEY_CURRENT!, process.env.AUTH_COOKIE_SIGNATURE_KEY_PREVIOUS!],
  cookieSerializeOptions: {
    path: "/",
    httpOnly: true,
    secure: process.env.USE_SECURE_COOKIES === "true",
    sameSite: "lax" as const,
    maxAge: 12 * 60 * 60 * 24,
  },
  serviceAccount: {
    projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID!,
    clientEmail: process.env.FB_ADMIN_CLIENT_EMAIL!,
    privateKey: process.env.FB_ADMIN_PRIVATE_KEY?.replace(/\\n/g, "\n") || "",
  }
};


type ClientConfigType = {
  projectId?: string;
  apiKey: string;
  authDomain?: string;
  databaseURL?: string;
  messagingSenderId?: string;
}

export const clientConfig: ClientConfigType = {
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY!,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL,
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID
};

@nolafs,

There is actually no issue within your code. You just need to remove the console.info statement

    handleInvalidToken: async (reason) => {
-      console.info('Missing or malformed credentials', {reason});

handleInvalidToken is called when something expected happens. One of those expected events is user seeing your app for the first time, or after cookies are expired

Sure, and my local version does work as expected. The deployed version does not redirect at all and when try opening root, it just redirects me back to the login. I can see the cookie but it seems it is not reading it at all. I get simple ?redirect=%2F and it stays on the login after submitting.

If you're using Firebase Hosting, probably worth checking out this issue: #146

Does authentication work if you change cookieName to __session?

Doh, I seen that issue earlier and just ignored it. Sorry, thanks for your help.