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:
Any insights would be greatly appreciated!
UPDATED: Here are the steps to reproduce the problem:
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.
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.