RemcoE33
RemcoE33

Reputation: 1620

Calling (private) cloud function from cloud function

I keep getting a 403 with the testcode below. Is it just me or is it overly complicated to call a function within the same project? I did some research here and here.

I've set the cloud function invoker on the default service account for both functions. And the allow internal traffic

So i have tried both codes below. The token is printed to the logs in the first function, so why do i still get a 403?

Script1:

const axios = require("axios");
/**
 * Responds to any HTTP request.
 *
 * @param {!express:Request} req HTTP request context.
 * @param {!express:Response} res HTTP response context.
 */
exports.helloWorld = async (req, res) => {
  console.log(JSON.stringify(process.env));

  const sample_api_url = `https://someRegionAndSomeProject.cloudfunctions.net/sample-api`;
  const metadataServerURL =
    "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=";
  const tokenUrl = metadataServerURL + sample_api_url;

  // Fetch the token
  const tokenResponse = await axios(tokenUrl, {
    method: "GET",
    headers: {
      "Metadata-Flavor": "Google",
    },
  });

  const token = tokenResponse.data;

  console.log(token);

  const functionResponse = await axios(sample_api_url, {
    method: "GET",
    headers: {
      Authorization: `Bearer ${token}`,
      "Content-Type": "application/json",
    },
  });
  
  const data = functionResponse.data;
  console.log(data);
  res.status(200).json({ token, data });
};

Script2:

const {GoogleAuth} = require('google-auth-library');
/**
 * Responds to any HTTP request.
 *
 * @param {!express:Request} req HTTP request context.
 * @param {!express:Response} res HTTP response context.
 */
exports.helloWorld = async (req, res) => {

const url = 'https://someRegionAndSomeProject.cloudfunctions.net/sample-api';
const targetAudience = url;
const auth = new GoogleAuth();
const client = await auth.getIdTokenClient(targetAudience);
const response = await client.request({url});

res.status(200).json({data: response.data})

};

Upvotes: 1

Views: 1432

Answers (2)

guillaume blaquiere
guillaume blaquiere

Reputation: 75930

There is 2 types of security on Cloud Functions

  • Identity based security: if you deploy your Cloud Functions without the allow-unauthenticated parameter, you have to send a request with an Authorization: bearer <token> header, with token is an identity token which has, at least, the cloud functions invoker role. You can also add the allUsers user with the cloud functions invoker role to make the function publicly reachable (no security header required)
  • Network based security: this time, only the request coming from your project VPCs or your VPC SC are allowed to access the Cloud Functions. If you try to reach the cloud functions from an unauthorized network, you get a 403 (your error).

You can combine the 2 security solutions if you want. In your code, you correctly add the security header. However, your request is rejected by the network check.


The solution is not so simple, not free, and I totally agree with you that this pattern should be simpler.

To achieve this, you must create a serverless VPC connector and attach it on your functions that perform the call. You also have to set the egress to ALL on that functions. That's all

The consequence are the following: The traffic originated from your function will be routed to your VPC thanks to the serverless VPC connector. The Cloud Functions URL being always a public URL, you have to set the egress to ALL, to route the traffic going to public URL through the serverless VPC connector.

Upvotes: 3

Kolban
Kolban

Reputation: 15266

Based on your post, I created a sample that worked for me. I created two GCP functions "func1" and "func2". I then determined how to call func2 from func1 where func2 is only exposed to be invoked by the service account identity which func1 runs as.

The final code for func1 is as follows:

const func2Url = 'https://us-central1-XXX.cloudfunctions.net/func2';
const targetAudience = func2Url;
const {GoogleAuth} = require('google-auth-library');
const auth = new GoogleAuth();

async function request() {
  console.info(`request ${func2Url} with target audience ${targetAudience}`);
  const client = await auth.getIdTokenClient(targetAudience);
  const res = await client.request({url: func2Url});
  console.info(res.data);
  return res;
}


exports.func1 = async (req, res) => {
  let message = `Hello from func1`;
  try {
    let response = await request();
    res.status(200).send(`${message} + ${response.data}`);
  }
  catch(e) {
    console.log(e);
    res.status(500).send('failed');
  }
};

The primary recipe used here is as described in the Google Docs here. I also found this medium article helpful.

Upvotes: 1

Related Questions