Search code examples
google-cloud-platformgoogle-cloud-functionsgoogle-authenticationgoogle-cloud-iamserver-to-server

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


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


Solution

  • 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.