node.jsfirebasefirebase-authenticationgoogle-cloud-functionsjwt

Can Firebase Auth custom claims in JWT tokens be tampered with in onCall functions?


I’m developing a serverless backend using Firebase products, and my endpoints are implemented as onCall cloud functions (v2). I’ve created a middleware function to ensure that only authenticated users can access specific functions. Additionally, some of my APIs require a custom claim { roles: ["admin", "superadmin"] } in the Firebase auth token.

I recently discovered that it’s possible to modify the roles attribute in the Firebase Auth token (JWT), which is generated from custom claims registered in Firebase Auth. This raises concerns about whether users can tamper with the token to escalate privileges (e.g., giving themselves admin rights).

I want to confirm whether this is a real security issue or if I’m missing something in my code.

Below is the middleware I’m using to handle authentication and role-based access control:

export const withAuthentication =
   (
      requiredAccessRoles?: string[] // Roles are optional
   ) =>
   (
      handler: (request: CallableRequest) => Promise<any> // Function to handle the request
   ): ((request: CallableRequest) => Promise<any>) => {
      return async (request) => {
         const authHeader = request.rawRequest.headers.authorization as
            | string
            | undefined;

         // Check if Authorization header is present and properly formatted
         if (!authHeader || !authHeader.startsWith("Bearer "))
            throw new HttpsError("unauthenticated", "Invalid or missing ID token.");

         if (!request.auth || !request.auth.uid)
            throw new HttpsError("unauthenticated", "Unauthenticated user");

         // Extract the ID token from the header
         const idToken = authHeader.slice(7).trim();

         try {
            // Validate the ID token
            await getAuth().verifyIdToken(idToken);
         } catch (error) {
            throw new HttpsError("unauthenticated", "Invalid or expired ID token.");
         }

         // If roles are required, perform the role check
         if (requiredAccessRoles && requiredAccessRoles.length > 0) {
            // Check if the user has at least one of the required roles
            const hasAnyRole = requiredAccessRoles.some((role) =>
               request.auth?.token.roles.includes(role)
            );

            if (!hasAnyRole)
               throw new HttpsError("permission-denied", "Insufficient privileges");
         }

         // Proceed with the original handler
         return handler(request);
      };
   };

Here’s the onCall cloud function that handles the request and uses the middleware to enforce role-based access control:

export const testApiEndpoint = onCall(
   withAuthentication(["admin", "superadmin"])(async (request) => {
      try {
         return {
            message: "access granted"
         };
      } catch (error) {
         throw new HttpsError("internal", "Something went wrong");
      }
   })
);

Questions:

  1. Users can modify their Firebase Auth token (JWT) and add roles like "admin" or "superadmin" to bypass access control. What can I do to prevent such a breach?
  2. Is there a best practice for securely verifying roles from custom claims in Firebase Auth?

Any insights would be greatly appreciated!

UPDATED: Here are the steps to reproduce the problem:

  1. User signs in with email and password using Firebase Auth client SDK on a web page.
  2. Firebase Auth generates the Bearer token, including the custom claims. For example, the token will contain { "roles": ["basic"] }.
  3. Extract the Bearer token, decode it from base64, and replace the roles claim from basic to admin: { "roles": ["admin"] }.
  4. Encode the forged Bearer token back to base64, copy it, and use it in Postman as the Authorization Bearer token.
  5. Voilà: The user with the basic role has successfully promoted themselves to admin privileges.

Firebase Auth does prevent forgery when running getAuth().verifyIdToken(idToken), but it doesn’t check for forged custom claims. For example, if you try to change the expiration date of a token, verifyIdToken(idToken) will throw an error, but not for modified custom claims.


Solution

  • I recently discovered that it’s possible to modify the roles attribute in the Firebase Auth token (JWT), which is generated from custom claims registered in Firebase Auth.

    To mint a Firebase Authentication JWT for a project you must have access to the administrative credentials of that project. The only way they can get those is if you added them as an administrator to the project, or if an administrator shared the credentials with them.

    Once someone has access to those administrative credentials, they can do whatever they want on the project. They don't need to mint a JWT to do so, as they already have full access with the administrative credentials


    Update:

    Your original second question:

    Users can modify their Firebase Auth token (JWT) and add roles like "admin" or "superadmin" to bypass access control. What can I do to prevent such a breach?

    And then in step 4 you say:

    Encode the forged Bearer token back to base64, copy it, and use it in Postman as the Authorization Bearer token.

    As explained above, this step of minting a JWT for a project requires that you specify the administrative credentials for the project.


    Update 2: As @samthecodingman figured out, you're using the Firebase emulator suite, and those don't actually validate the JWT signature. So while this hack is possible with the emulated Firebase Authentication, it won't work in production - as that validates the JWT signature.