0

I'm attempting to have one GCP Firebase Cloud Function call another. I am using https://cloud.google.com/functions/docs/securing/authenticating#authenticating_function_to_function_calls as the guide. I have everything working but when the function is invoked, it throws the following error:

Failed to validate auth token. FirebaseAuthError: Firebase ID token has incorrect "aud" (audience) claim. Expected "{projectId}" but got "https://{project-region}-{projectId}.cloudfunctions.net/{functionName}". Make sure the ID token comes from the same Firebase project as the service account used to authenticate this SDK.

I attempted to set the targetAudience to {projectId}, but then auth.getIdTokenClient(targetAudience); failed with a 401 Unauthorized response.

The called/invoked function is using functions.https.onCall to authenticate the request. If I switch it to functions.https.onRequest, it works, but I don't know how to validate the request and I think that's a pretty poor workaround anyway as it should be working with the onCall method.

For the functions.https.onRequest method, it passes through a Google Auth signed JWT Authorization header, but const decodedToken = await admin.auth().verifyIdToken(req.headers.authorization ?? ''); (source) fails with the error:

Error: Decoding Firebase ID token failed. Make sure you passed the entire string JWT which represents an ID token.
Ryan Saunders
  • 359
  • 1
  • 13
  • Have you checked with these similar issue cases [Link1](https://stackoverflow.com/questions/38335127) and [Link2](https://stackoverflow.com/questions/59825250). – Sandeep Vokkareni Mar 20 '23 at 08:16
  • @SandeepVokkareni yes, I've investigated quite a few possible similar cases. The first one you linked has to do with client oAuth, which I'm not using and cannot manipulate with `firebase.auth()` because that's a frontend function. The second is regarding the API gateway, which I'm not using for function-to-function and I do not have the `x-apigateway-api-userinfo` to extract. Also, my function's `initializeApp` is appropriately setting the projectId. – Ryan Saunders Mar 21 '23 at 13:24
  • It seems that the aud claim in the token is compared against the projectId. Make sure this is set properly? Check with this [Link1](https://stackoverflow.com/questions/71874966), [Link2](https://stackoverflow.com/questions/42646862) & [Link3](https://stackoverflow.com/questions/60579544). – Sandeep Vokkareni Mar 22 '23 at 13:42
  • @SandeepVokkareni yeah I agree that it's likely an issue with the projectId, but not in a way that I as the caller have power to change. The projectId is set correctly on the receiving/invoked function; it's the caller that's sending the wrong `aud` by setting it to the full function path rather than the projectId, but this is what `GoogleAuth.getIdTokenClient` requires (the full URL) as its targetAudience. It throws an error if I give it just the projectId instead. See https://cloud.google.com/functions/docs/securing/authenticating#authenticating_function_to_function_calls – Ryan Saunders Mar 22 '23 at 14:23
  • Can you tell reason behind using [`GoogleAuth.getIdTokenClient`](https://cloud.google.com/nodejs/docs/reference/google-auth-library/latest/google-auth-library/googleauth#google_auth_library_GoogleAuth_getIdTokenClient_member_1_) instead of [`FirebaseUser.getToken()`](https://firebase.google.com/docs/auth/admin/verify-id-tokens) as you are using firebase function? – Sandeep Vokkareni Mar 23 '23 at 13:43
  • @SandeepVokkareni `firebase` (the node module that contains `FirebaseUser.getToken()`) is meant for client apps, not cloud functions. I'm trying to do function-to-function communication, not client-to-function. – Ryan Saunders Mar 25 '23 at 17:25
  • Make sure the project issuing token must be the same as the one verifying. Also are you creating the token by following this [Document](https://firebase.google.com/docs/auth/admin/create-custom-tokens). And even the custom token is created by the function, it still needs to be exchanged using `signInWithCustomToken()` – Sandeep Vokkareni Mar 30 '23 at 08:44

1 Answers1

0

I needed to use Google Auth's OAuth2Client.verifyIdToken. This is not well documented. I had to find the solution in a sample code file; even then, it wasn't clear how you should verify the token's payload (their example verification method seemed rather weak). So here is my example of handling the full request:


import { OAuth2Client } from 'google-auth-library';

export const exampleFunction = functions.https.onRequest(async (req, res) => {
  // Note that I couldn't find a way to get the function name from the request object. :(
  const functionName = 'exampleFunction';
  // Note that you may have a different service account email if your Cloud Function 
  // is managed by a different account than the default.
  const expectedServiceAccountEmail = `your-project-id@appspot.gserviceaccount.com`;
  const parts = req.headers.authorization?.split(' ');
  if (!parts || parts.length !== 2 || parts[0] !== 'Bearer' || !parts[1]) {
    console.error('Bad header format: Authorization header not formated as \'Bearer [token]\'', req.headers);
    throw new functions.https.HttpsError('unauthenticated', 'user not authenticated');
  }
  try {
  const audience = `${req.protocol}://${req.hostname}/${functionName}`;
  const googleOAuth2Client = new OAuth2Client();
    const decodedToken = await googleOAuth2Client.verifyIdToken({
      idToken: parts[1],
      audience,
    });
    const payload = decodedToken.getPayload();
    if (!payload) {
      console.error('unpexpected state; missing payload', decodedToken);
      throw new Error('no payload');
    }
    if (payload.aud !== audience) {
      console.error('bad audience', payload);
      throw new functions.https.HttpsError('permission-denied', 'bad audience');
    }
    if (payload.iss !== 'https://accounts.google.com') {
      console.error('bad issuer', payload);
      throw new functions.https.HttpsError('permission-denied', 'bad issuer');
    }
    if (payload.exp < Date.now() / 1000) {
      console.error('expired token', payload);
      throw new functions.https.HttpsError('permission-denied', 'expired token');
    }
    if (!payload.email_verified) {
      console.error('email not verified', payload);
      throw new functions.https.HttpsError('permission-denied', 'email not verified');
    }
    if (payload.email !== expectedServiceAccountEmail) {
      console.error('invalid email', payload);
      throw new functions.https.HttpsError('permission-denied', 'invalid email');
    }
  } catch (e) {
    console.error(e);
    throw new functions.https.HttpsError('permission-denied', 'bad authorization id token');
  }
  res.status(200).send('ok');
});
Ryan Saunders
  • 359
  • 1
  • 13