Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Securely calling a Google Cloud Function via a Google Apps Script

How can I securely call a Google Cloud Function via a Google Apps Script?

✅ I have a Google Cloud Function, which I can access at https://MY_REGION-MY_PROJECT.cloudfunctions.net/MY_FUNCTION, and which I would like to allow certain users to invoke via an Apps Script.

✅ To secure the Cloud Function, I have set Cloud Function Invoker to only include known email (e.g. [email protected], where this is a valid Google email).

✅ I am able to successfully invoke the Cloud Function via curl, while logged into gcloud with this email, by running: curl https://MY_REGION-MY_PROJECT.cloudfunctions.net/MY_FUNCTION -H "Authorization: Bearer $(gcloud auth print-identity-token)".

✅ I have granted the following oauthScopes in my Apps Script's manifest:

  • "https://www.googleapis.com/auth/script.external_request"
  • "https://www.googleapis.com/auth/userinfo.email"
  • "https://www.googleapis.com/auth/cloud-platform"

⛔️ However, when I attempt to invoke the Cloud Function via a Google Apps Script, while logged in with the email [email protected], I am unable to invoke it and instead returned a 401. Here is how I have attempted to invoke the Cloud Function:

const token = ScriptApp.getIdentityToken();
const options = {
  headers: {'Authorization': 'Bearer ' + token}
}
UrlFetchApp.fetch("https://MY_REGION-MY_PROJECT.cloudfunctions.net/MY_FUNCTION", options);

ℹ️ I have also tried the following:

  • Using ScriptApp.getOAuthToken()
  • Adding additional oauthScopes, e.g. openid.
  • Creating an OAuth Client ID with https://script.google.com set as an Authorized Javascript origin.
  • Deploying the Apps Script.
  • Crying out to the sky in utter, abject despair
like image 608
zachary-reachodin Avatar asked May 13 '20 17:05

zachary-reachodin


People also ask

Is Google Apps Script secure?

Most likely it is safe since the script is only accessible to the script owner and Workspace Admins if it is for Google workspace (which may or may not be an issue).

How do I call another cloud function?

The Cloud Functions for Firebase client SDKs let you call functions directly from a Firebase app. To call a function from your app in this way, write and deploy an HTTPS Callable function in Cloud Functions, and then add client logic to call the function from your app.

What is http trigger in cloud function?

In Cloud Functions, an HTTP trigger enables a function to run in response to HTTP(S) requests. When you specify an HTTP trigger for a function, the function is assigned a URL at which it can receive requests. HTTP triggers support the GET , POST , PUT , DELETE , and OPTIONS request methods.


2 Answers

I struggled very much authenticating from Apps Script to invoke a Cloud Run application and just figured it out, and I believe it's similar for calling any Google Cloud application including Cloud Functions. Essentially the goal is to invoke an HTTP method protected by Google Cloud IAM using the authentication information you already have running Apps Script as the user.

The missing step I believe is that the technique you're using will only work if the Apps Script script and Google Cloud Function (or Run container in my case) are in the same GCP project. (See how to associate the script with the GCP project.)

Setting it up this way is much simpler than otherwise: when you associate the script with a GCP project, this automatically creates an OAuth Client ID configuration to the project, and Apps Script's getIdentityToken function returns an identity token that is only valid for that client ID (it's coded into the aud field field of the token). If you wanted an identity token that works for another project, you'd need to get one another way.

If you are able to put the script and GCP function or app in the same GCP project, you'll also have to do these things, many of which you already did:

  • Successfully test authentication of your cloud function via curl https://MY_REGION-MY_PROJECT.cloudfunctions.net/MY_FUNCTION -H "Authorization: Bearer $(gcloud auth print-identity-token)" (as instructed here). If this fails then you have a different problem than is asked in this Stack Overflow question, so I'm omitting troubleshooting steps for this.

  • Ensure you are actually who the script is running as. You cannot get an identity token from custom function in a spreadsheet as they run anonymously. In other cases, the Apps Script code may be running as someone else, such as certain triggers.

  • Redeploy the Cloud Function as mentioned here (or similarly redeploy the Cloud Run container as mentioned here) so the app will pick up any new Client ID configuration. This is required after any new Client ID is created, including the one created automatically by adding or re-adding the script to the GCP project. (If you move the script to another GCP project and then move it back again, it seems to create another Client ID rather than reuse the old one and the old one will stop working.)

  • Add the "openid" scope (and all other needed scopes, such as https://www.googleapis.com/auth/script.external_request) explicitly in the manifest. getIdentityToken() will return null without the openid scope which can cause this error. Note to readers: read this bullet point carefully - the scope name is literally just "openid" - it's not a URL like the other scopes.

    "oauthScopes": ["openid", "https://...", ...]
    
  • Use getIdentityToken() and do NOT use getOAuthToken(). According to what I've read, getOAuthToken() returns an access token rather than an identity token. Access tokens do not prove your identity; rather they just give prove authorization to access some resources.

If you are not able to add the script to the same project as the GCP application, I don't know what to do as I've never successfully tried it. Generally you're tasked with obtaining an OAuth identity token tied to one of your GCP client ids. I don't think one app (or GCP project) is supposed to be able to obtain an identity token for a different OAuth app (different GCP project). Anyway, it may still be possible. Google discusses OAuth authentication at a high level in their OpenID Connect docs. Perhaps an HTML service to do a regular Google sign-in flow with a web client, would work for user-present operations if you get the user to click the redirect link as Apps Script doesn't allow browser redirects. If you just need to protect your service from the public, perhaps you could try other authentication options that involve service accounts. (I haven't tried this either.) If the service just needs to know who the user is, perhaps you could parse the identity token and send the identifier of the user as part of the request. If the service needs to access their Google resources, then maybe you could have the user sign in to that app separately and use OAuth generally for long term access to their resources, using it as needed when called by Apps Script.

like image 104
Alexander Taylor Avatar answered Oct 13 '22 01:10

Alexander Taylor


The answer above is very good. But since I am new with this I still had to spend a lot of time trying to figure it out.

This worked for me:

Apps Script code:

async function callCloudFunction() {
  const token = ScriptApp.getIdentityToken();
  const options = {
    headers: {'Authorization': 'Bearer ' + token}
  }
  const data = JSON.parse(await UrlFetchApp.fetch("https://MY_REGION-MY_PROJECT.cloudfunctions.net/MY_FUNCTION", options).getContentText())
  return data
}
  • Make sure that in project config you have the same project where your function is created.
  • After that, you can add the emails of the users you want to access the script on the Permission section in the function.
  • And as @alexander-taylor mentioned as well, make sure to add the scopes to your manifest file. You can make the manifest visible from the configuration tab in apps script. It took me some time to get that too.
like image 33
Fernão de Rocha Guerra Avatar answered Oct 13 '22 00:10

Fernão de Rocha Guerra