1

I have a google cloud function providing a Google Chat App. My GCF receives calls from Google Chat when users type input. It doesn't call Chat; it exposes a URL and Chat calls it, it computes for a while, and it sends a response back to Chat. So the GCF does not need authentication to work with Chat.

I seem to need to configure my Google Cloud Function to accept network requests to "Allow All Traffic". Google Chat does not seem to be part of my Google Cloud Project, so I cannot configure the connection into the GCF to be "Allow Internal Traffic Only". So anyone learning the URL of my GCF could flood it with fake traffic.

Ideally, I would like the Google networking system to only permit access to my function from Google Chat, but I cannot find a way to do this using a service account in Google IAM. Can I do this somehow?

Looking at the headers of the access from Chat to my GCF, it does contain an authorization: 'Bearer eyJhbGciO...' header. That turns out to be a JSON Web Token. That says

headers: {
  "alg": "RS256",
  "kid": "06ea3d3c9414b34d77d66407580cec7e10c0b7d3",
  "typ": "JWT"
}
payload: {
  "aud": "939021344830",
  "exp": 1680195982,
  "iat": 1680192382,
  "iss": "chat@system.gserviceaccount.com"
}

so it looks like that is a token generated by Google Chat and signed by Google to demonstrate that the source was indeed Google Chat. The kid should identify the key I need to validate the JWT. How can I verify that this token is properly signed by Google?

I looked at Secure Google Cloud Functions http trigger with auth but the solutions there do not seem to be applicable because I do not have control over how Google Chat makes the call to my GCF.

Thanks!

emrys57
  • 6,679
  • 3
  • 39
  • 49
  • The token in your post is an OAuth Access Token. That is the wrong type. Cloud Functions require an OIDC Identity Token. The `kid` is found in the service account JSON. The kid is the service account public certificate fingerprint that corresponds with the RSA private key also located in the service account JSON file. Example code that creates an OIDC Identity Token: https://cloud.google.com/functions/docs/securing/authenticating#functions-bearer-token-example-python – John Hanley Mar 30 '23 at 17:29
  • Note: Cloud Functions validates the OIDC Identity Token for you. Your function will never see an invalid one if you have authorization enabled (IAP). Invalid requests are rejected by IAP (Identity Aware Proxy). – John Hanley Mar 30 '23 at 17:33
  • The token in my post is the only one I have: it's the one generated by Google Chat and I don't think I have any way to change it. I need to verify that it is signed by Google. – emrys57 Mar 31 '23 at 07:51
  • https://developers.google.com/identity/gsi/web/guides/verify-google-id-token – John Hanley Mar 31 '23 at 08:07
  • Thanks for pointing me there, but this is not an ID token. Does that code work for non-id tokens? And how do I know the expected value of `aud`? – emrys57 Mar 31 '23 at 08:13
  • As a heads up. For Cloud Functions, Cloud Run, etc: when another service sends an Identity Token, IAP destroys the signature so that the token cannot be used again. See my answer [here](https://developers.google.com/identity/gsi/web/guides/verify-google-id-token) Another item is that Google has multiple identity systems (authorities). I don't know which one Google Chat uses to sign. See my answer [here](https://developers.google.com/identity/gsi/web/guides/verify-google-id-token) to give you an idea. – John Hanley Mar 31 '23 at 08:13
  • If you do not have an ID token, then you cannot use it with Cloud Functions. IAP **only** accepts OIDC Identity Tokens. Anything else will result in a 4xx failure. – John Hanley Mar 31 '23 at 08:14
  • You can use any token you want if the service is **public** and you use a private header - do not use the HTTP Authorization header. However, unless you know what type of token you have, I would not know how to validate it. – John Hanley Mar 31 '23 at 08:16
  • The value of `aud` is the Cloud Functions URL. That must be set when the token is created. – John Hanley Mar 31 '23 at 08:17
  • Sorry have to run just now, later! – emrys57 Mar 31 '23 at 08:22

2 Answers2

0

One good approach is to deploy in front of your GCF a Cloud Endpoints. This is a step-by-step https://cloud.google.com/endpoints/docs/openapi/set-up-cloud-functions-espv2

For the security part I recommended the following:

  1. Restrict to "Allow Internal traffic only" because all the traffic will be routed by the new Cloud Endpoint

  2. Disable the (unauthenticated) access, eliminating the AllUsers permission on the IAM, and set the Cloud Function Invoker role to the SA of the Cloud Endpoints only.

  3. Choose an Auth method

Use the security definition in the YAML to validate the JWT https://cloud.google.com/endpoints/docs/openapi/authenticating-users-custom#configuring_esp_to_support_client_authentication

    securityDefinitions:your_custom_auth_id:
     authorizationUrl: ""
     flow: "implicit"
     type: "oauth2"
     # The value below should be unique
     x-google-issuer: "chat@system.gserviceaccount.com"
     x-google-jwks_uri: "https://www.googleapis.com/service_accounts/v1/metadata/x509/chat@system.gserviceaccount.com"
     # Optional. Replace YOUR-CLIENT-ID with your client ID
     x-google-audiences: "939021344830"

Note that is replace with the values of the JWT that you provide:

  • x-google-issuer
  • x-google-jws-uri
  • x-google-audiences

With this approach you don't need to implement the logic to validate the JWT and you could reuse this YAML definition to multiples Cloud Functions.

Deyvid Martinez
  • 430
  • 3
  • 8
  • What is the benefit of Cloud Endpoints over CF for google chat app? The other user still can access the cloud endpoint(if know the URL). – Muhammad Dyas Yaskur Mar 30 '23 at 21:36
  • Is a combination of both the CF and the Cloud Endpoint, in the Cloud Endpoint you could include security (API KEYS, JWT, OAuth) for example https://cloud.google.com/endpoints/docs/openapi/restricting-api-access-with-api-keys#restricting_access_to_all_api_methods – Deyvid Martinez Mar 30 '23 at 23:22
  • This definitely looks like it is just moving the same problem to another function. That function still would have to be sure that the request came from google chat, and it would do that by verifying the JWT provided by Google chat. – emrys57 Mar 31 '23 at 07:52
  • @emrys57 is a good practice to decouple the logic of Auth from your service, that is the use case of Cloud Endpoint, and you could reutilize the security definition in a simple way (YAML) for other cloud functions. – Deyvid Martinez Mar 31 '23 at 17:44
  • @DeyvidMartinez don't you know about google chat apps? we can't use visitor auth with google chat app endpoint. Our apps will be usable if we use auth on the api endpoint. – Muhammad Dyas Yaskur Mar 31 '23 at 19:58
  • *unusable I meant... – Muhammad Dyas Yaskur Mar 31 '23 at 20:05
  • Sorry if I misunderstood the requirement, but the snippet solve this request "How can I verify that this token is properly signed by Google" – Deyvid Martinez Mar 31 '23 at 21:53
0

Here is my code to check the JWT, where token is the authorisation header stripped of the bearer prefix. This seems to be working.

const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
const client = jwksClient({
  jwksUri: 'https://www.googleapis.com/service_accounts/v1/metadata/jwk/chat%40system.gserviceaccount.com',
});
const promiseToVerify = (token) => {
  return new Promise((resolve, reject) => {
    const decoded = jwt.decode(token, {complete: true});
    if (!decoded || !decoded.header || !decoded.header.kid) {
      return reject(new Error('Invalid token'));
    }
    client.getSigningKey(decoded.header.kid, (err, key) => {
      if (err) {
        return reject(err);
      }
      const signingKey = key.publicKey || key.rsaPublicKey;
      jwt.verify(token, signingKey, (err, decoded) => {
        if (err) {
          return reject(err);
        }
        resolve(decoded);
      });
    });
  });
};

The aud which appears in the JWT is the project ID of a folder called system-gsuite which appears in our google cloud console project selector dropdown. I have not yet been able to find if that ID is unique to our code. aud needs checking to make sure it is the right value, as well as validating the JWT signature. jwt.verify above does check the validity time and complains if the JWT has expired.

Using this code, we can be confident that the JWT was signed by google, because the kid has to be that of a public key found at a google-controlled URL specific to Google Chat. And the aug is the ID of a google project known to our google cloud console. And the token has not yet expired.

emrys57
  • 6,679
  • 3
  • 39
  • 49