Google function HTTP trigger - authentication problem server to server with service account
T

1

4

What i want to do: To call a google function from my server/machine & limit it usage with a (simple) authentication.

What i use: Node.js, google-auth-library library for authentication.

What have I done/tried:

1) Created a project in Google Cloud Functions

2) Created a simple google function

 exports.helloWorld = (req, res) => {
  let message = req.query.message || req.body.message || 'Hello World!';
  res.status(200).send(message);
};

3) Set my custom service account

4) Enabled api: - Cloud Functions API - IAM Service Account Credentials API - Cloud Run API - Compute Engine API - IAM Service Account Credentials API

5) Given to my server account necessary authorization (project owner, cloud function admin, IAM project admin... (need more?)

6) Generated key from my service account and saved it in json format

NB: with allUser permission (without authorization required), i can call my endpoint without problem

7) From my project i tried to auth my function in this way

const { JWT } = require('google-auth-library');
const fetch = require('node-fetch');
const keys = require('./service-account-keys.json');


async function callFunction(text) {
  const url = `https://europe-west1-myFunction.cloudfunctions.net/test`;

  const client = new JWT({
    email: keys.client_email,
    keyFile: keys,
    key: keys.private_key,
    scopes: [
      'https://www.googleapis.com/auth/cloud-platform',
      'https://www.googleapis.com/auth/iam',
    ],
  });

  const res = await client.request({ url });
  const tokenInfo = await client.getTokenInfo(client.credentials.access_token);

  try {
    const response = await fetch(url, {
      method: 'GET',
      headers: {
        Authorization: `Bearer ${client.credentials.access_token}`,
      },
    });
    if (response.status !== 200) {
      console.log(response);
      return {};
    }
    return response.json();
  } catch (e) {
    console.error(e);
  }
} 

ℹ️ if i try to pass at client.request() url without name of function (https://europe-west1-myFunction.cloudfunctions.net), i not received error, but when use the JWT token obtained in fetch call, i received the same error.

RESULT:

 Error: 
<html><head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<title>401 Unauthorized</title>
</head>
<body text=#000000 bgcolor=#ffffff>
<h1>Error: Unauthorized</h1>
<h2>Your client does not have permission to the requested URL <code>/test1</code>.</h2>
<h2></h2>
</body></html>

❓ How do I call a google function with any protection to prevent anyone from using it? (I don't need specific security, just that random users don't use it) Thanks in advance for any help

Thornie answered 8/5, 2020 at 11:15 Comment(1)
Have you added Service Account Token Creator and Service Account User to your Service Account? Full info can be found here.Isborne
P
4

When you call a private function (or a private Cloud Run) you have to use a google signed identity token.

In your code, you use an access token

      headers: {
        Authorization: `Bearer ${client.credentials.access_token}`,
      },

Access token work when you have to request Google Cloud API, not your services

And the google signed is important, because you can easily generate a self signed identity token with the google auth lib, but it won't work

You have code sample here and I wrote a tool in Go if you want to have a try on it

** EDIT **

I worked on an example, and, even if I never liked Javascript, I have to admit that I'm jealous!! It's so simple in Node!!

Here my working example

const {GoogleAuth} = require('google-auth-library');

async function main() {
    // Define your URL, here with Cloud Run but the security is exactly the same with Cloud Functions (same underlying infrastructure)
    const url = "https://go111-vqg64v3fcq-uc.a.run.app"
    // Here I use the default credential, not an explicit key like you
    const auth = new GoogleAuth();
    //Example with the key file, not recommended on GCP environment.
    //const auth = new GoogleAuth({keyFilename:"/path/to/key.json"})

    //Create your client with an Identity token.
    const client = await auth.getIdTokenClient(url);
    const res = await client.request({url});
    console.log(res.data);
}

main().catch(console.error);

Note: Only Service account can generate and identity token with audience. If you are in your local computer, don't use your user account with the default credential mode.

Pede answered 8/5, 2020 at 19:44 Comment(5)
Ok thanks for the reply. I had started to guess that no access token was needed, but I wasn't sure I saw from the documentation that to get an id token, IAP must be used. So create an App Engine and authenticate a service account? Isn't there really a more direct way to get a token id? Google functions should be the easiest way to create a "microservice", but it becomes more complicatedThornie
I've been working at this for a long time and your sample code finally allowed me to run my function. Thanks! One question, you commented "//Example with the key file, not recommended on GCP environment." I have been using this code exactly but with "const auth = new GoogleAuth({keyFilename:"/path/to/key.json"})" instead of "const auth = new GoogleAuth();." Why is that not recommended? Is it a security issue? FYI my "key.json" credentials are for a service account and my cloud function is ONLY authorized to be called by that service account.Benford
If fact, you don't need it. If you deep dive in ADC, you will see that the client library can get the credentials automatically from the metadata server loaded on each Google Cloud instance (Cloud Function, Cloud Run, App Engine, Compute Engine (in cluster or not),...). So, you don't need an additional file to provide that credential. Set the correct service account when you deploy your Cloud Functions, and that's fine!!! And yes, having a secret data in a file require to manage it with caution.Pede
I'm running my app on a digital ocean droplet and from that app, reaching out to my Google Cloud Function. That means I have to keep the credentials in a local file right? Would it still be (at least quasi) best practices if I keep the keystore in a seperate folder in the droplet and just set export GOOGLE_APPLICATION_CREDENTIALS="/home/user/Downloads/service-account-file.json" before I run the app?Benford
Yes exactly. Like that, you can keep your code without any changes, and you simply change the environment configuration.Pede

© 2022 - 2024 — McMap. All rights reserved.