i am quite new to the GCP world and i am asking here just to have some other informations about my issue.
I would like to create an HTTP Google Cloud Function that performs some operations over a certain project.
At the same way, i need to invoke this function from an external service (let's say from a Gsheet for which the SA that own the function could edit it).
I saw that there is GoogleAppScripts, and in particular the URLFetchApp service.
My idea is to call that function from that service.
In order to have authentication i saw that the library OAuth2 is needed, in this case, is it necessary to create a ClientID and a ClientSecret for the account that manage the function? If this is the case, how the AppScript could create the related service?
Thank you in advance.
You can query the identity token via google apps script and then use it to call the gcloud function.
The first thing we have to do is to have a cloud function and make sure you have permissions to invoke it (try executing it using another method).
After that, go to your Apps Script project manifest and set at least 2 specific scopes:
{
...
"oauthScopes": [
"openid",
"https://www.googleapis.com/auth/script.external_request"
],
...
}
openid allows us to get an identity token to call the function with and .../script.external_request allows us to use a fetch.
Then you can add the code:
const response = UrlFetchApp.fetch(
'https://gcloud-function-url',
{
muteHttpExceptions: true,
headers: {
'Authorization': `Bearer ${ScriptApp.getIdentityToken()}`
}
}
)
// use response
As documented in the reference, ScriptApp.getIdentityToken() returns the the OpenID token that we need.
References
Authenticating Developers, Functions, and End-users
(Google Cloud guides)
UrlFetchApp.fetch(url, params) (Google Apps Script reference)
ScriptApp.getIdentityToken() (Google Apps Script reference)
This works as stated in #Martí 's answer, but I spent a very frustrating day having it not work because I missed a tiny but crucial step: You need to redeploy the Cloud Function after making the Apps Script, otherwise it won't have the Client ID validated! The Apps Script is assigned this ID when you associate it with the same GCP Project, so redeploy the Function after that.
Related
I have multiple Google Apps Scripts deployed as libraries and added into Google Sheets.
The problem starts when I share these sheets with other users.
To even let them see the user menu load, I needed to give them edit access to my library scripts. View only access wasn't enough. Then I got the following error message from them "Exception: Service Admin SDK API has not been enabled for your Apps Script-managed Cloud Platform project"
And I know I'm getting this for using function AdminDirectory.Members.insert(...);
I don't want add API privileges for them in GSuite.
The following oauthScopes are in use:
"oauthScopes": [
"https://www.googleapis.com/auth/drive",
"https://www.googleapis.com/auth/spreadsheets",
"https://www.googleapis.com/auth/documents",
"https://www.googleapis.com/auth/script.send_mail",
"https://www.googleapis.com/auth/script.external_request",
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/admin.directory.group.member"
]
What I'm looking for is a solution to create these scripts in a way that I would able to add them to my Google Sheets, where user menu could be loaded from the scripts and called by users. All script code from the library/add-on (eg,: GSuite create new user / add user to group / open, edit docs / open, edit sheets) should be run through my credential, not with theirs. Users shouldn't be able to view/edit the libraries/add-on scripts' code.
The only code they should able to see and edit is the tiny onOpen script in the Sheet's script file. They have to have access to the script library / add-on but only to run it, call functions from it. They shouldn't be able to read and edit the library/add-on script code. Their code in the spreadsheet would be really small. Just an onOpen trigger which would load the menu from the script and give access to the main functions which could be called from the menu. Those would be public functions. The rest is private.
Script from Sheet
Library called "Script"
This is how my script is loading the menu in Sheets and calls the functions through it.
Issue:
If I understand your situation correctly:
You have an editor add-on through which you want to execute functions that access Admin SDK (via a custom menu).
You want users in your domain, which do not have privileges to access Admin SDK, to be able to execute these functions.
You are a domain admin and can access the desired Admin SDK services.
You won't accomplish this by placing your code in a library, since there's no way to "delegate" the execution of a library function: the library function will run under the authority of the user executing the menu function.
Solution:
In that case, I'd suggest the following alternative:
Create a service account.
Follow this guide to grant the service account domain-wide delegation, so that it can be used to impersonate any account in your domain: in this case, your account.
Import and use the library OAuth2 for Apps Script in order to use the service account in your Apps Script project.
Use UrlFetchApp to call your desired API, using the access token from the service account.
Code sample:
For example, if you wanted to call Directory API's users.get to retrieve data from the user currently executing this, you would do something like this:
function getService() {
const service = OAuth2.createService("Service account")
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
.setPrivateKey(SERVICE_ACCOUNT_PRIVATE_KEY)
.setIssuer(SERVICE_ACCOUNT_EMAIL)
.setSubject(IMPERSONATED_EMAIL)
.setPropertyStore(PropertiesService.getScriptProperties())
.setParam('access_type', 'offline')
.setScope('https://www.googleapis.com/auth/admin.directory.user')
return service;
}
function getActiveUserData() {
const service = getService();
if (service.hasAccess()) {
const userKey = Session.getActiveUser();
const url = `https://admin.googleapis.com/admin/directory/v1/users/${userKey}`;
const options = {
headers: {
'Authorization': "Bearer " + service.getAccessToken(),
'Content-Type': 'application/json'
},
muteHttpExceptions: true
}
const resp = UrlFetchApp.fetch(url, options);
const userData = JSON.parse(resp.getContentText());
return userData;
}
}
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. USER#COMPANY.com, 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 USER#COMPANY.com, 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
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.
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.
Thanks to your comment you can do 2 things. But before, you have to know that you can't (or I least I never achieve this), create a valid identity token for being authenticated by Cloud Function and Cloud Run with a user credential. I opened a question on this
But you can call Google Cloud API with user credential! So
You can use the function test call API. The quotas limit you to 16 calls per 100 minutes (of course, it's design for test!)
You can publish a message into PubSub and plug your function on it. In this pattern your call is asynchronous.
I'm using Apps Script API to run a function with the service account's credential.
I added all scopes required in Rest resource API https://developers.google.com/apps-script/api/reference/rest/v1/scripts/run.
But when i run this script below it failed.
function run(){
var CREDENTIALS = {
"private_key": "Your Private key",
"client_email": "Your Client email",
"client_id": "Your Client ID",
"user_email": "Your Email address",
"api_key": "Your API key"
};
var service = getService(CREDENTIALS.client_email,CREDENTIALS.private_key);
service.reset();
if (service.hasAccess()) {
var url = 'https://script.googleapis.com/v1/projects/[SCRIPT ID]:run';
var body = {
"function": [FUNCTION NAME]
};
var params = {
headers: {
Authorization: 'Bearer ' + service.getAccessToken()
},
method: 'post',
playload : JSON.stringify(body),
contentType: 'application/json',
muteHttpExceptions: true
};
var response = UrlFetchApp.fetch(url, params);
Logger.log(response);
}
else {
Logger.log(service.getLastError());
}
}
function getService(email, privateKey) {
return OAuth2.createService('Service Account')
// Set the endpoint URL.
.setTokenUrl('https://oauth2.googleapis.com/token')
// Set the private key and issuer.
.setPrivateKey(privateKey)
.setIssuer(email)
// Set the name of the user to impersonate. This will only work for
// Google Apps for Work/EDU accounts whose admin has setup domain-wide
// delegation:
// https://developers.google.com/identity/protocols/OAuth2ServiceAccount#delegatingauthority
.setSubject([USER EMAIL])
// Set the property store where authorized tokens should be persisted.
.setPropertyStore(PropertiesService.getScriptProperties())
// Set the scope. This must match one of the scopes configured during the
// setup of domain-wide delegation.
.setScope('https://www.googleapis.com/auth/script.external_request');
}
I've got a 404 Error and I think it comes from the scopes list.
So I can't run a script deployed as an API Executable with the OAuth2.0 token.
Which scopes should I choose to run a function via an HTTP request?
In your run function, for the params object you should have payload not playload.
You want to use Apps Script API with the service account.
You want to achieve this using Google Apps Script.
If my understanding is correct, how about this answer? Please think of this as just one of several possible answers.
Issue and workaround:
Unfortunately, in the current stage, the method of scripts.run in Apps Script API cannot be used with the service account. The official document says as follows. About this, when I tested this, I could confirm that the method of scripts.run in Apps Script API cannot be used with the service account.
Warning: The Apps Script API doesn't work with service accounts.
From above situation, as the workaround, how about using the access token retrieved by OAuth2? In order to use the Apps Script API with OAuth2, it is required to link Cloud Platform Project to Google Apps Script Project. About this, you can see the flow for linking them at here.
Note:
I think that when you use OAuth2, Oleg Valter's comment and TheAddonDepot's answer are very useful.
References:
Executing Functions using the Apps Script API
Linking Cloud Platform Project to Google Apps Script Project
If this was not the direction you want, I apologize.
Added:
You want to make several users run the script as the owner who is you.
From your replying, I could understand like above. When Apps Script API is used for above situation, the credential information is required to give each user. When each user uses the access token retrieved by your credential information, your goal can be achieve. But I cannot recommend this. So in your case, I would like to use Web Apps to achieve your goal. The flow is as follows.
1. Prepare script.
Please prepare your script. For example, in the current stage, you want to make users run a function of myFunction(), please put the following sample script.
function doGet(e) {
var values = e; // When you want to give the values by requesting, you can use the event object like "e".
var res = myFunction(values);
return ContentService.createTextOutput(res);
}
In this case, the GET method is used. When you want to only run the function, you can use this script. When you want to run the function by giving the large data, you can use doPost() instead of doGet().
2. Deploy Web Apps.
On the script editor, Open a dialog box by "Publish" -> "Deploy as web app".
Select "Me" for "Execute the app as:".
By this, the script is run as the owner.
Here, when "Anyone" is set, the script is run as each user. In this case, it is required to share the script to each user. And the access token is required to be used. Please be careful this.
Select "Anyone, even anonymous" for "Who has access to the app:".
In this case, no access token is required to be request. I think that as the test case, I recommend this setting.
Of course, you can also use the access token. At that time, please set this to "Anyone".
Click "Deploy" button as new "Project version".
Automatically open a dialog box of "Authorization required".
Click "Review Permissions".
Select own account.
Click "Advanced" at "This app isn't verified".
Click "Go to ### project name ###(unsafe)"
Click "Allow" button.
Click "OK".
Copy the URL of Web Apps. It's like https://script.google.com/macros/s/###/exec.
When you modified the Google Apps Script, please redeploy as new version. By this, the modified script is reflected to Web Apps. Please be careful this.
3. Run the function using Web Apps.
This is a sample curl command for executing myFunction with Web Apps. Please set your Web Apps URL. At above settings of Web Apps, each user can access by the following curl command.
curl -GL \
-d "key=value" \
"https://script.google.com/macros/s/###/exec"
When key=value is used as the query parameter like above, at doGet(e), you can retrieve value using e.parameter.key.
References:
Web Apps
Taking advantage of Web Apps with Google Apps Script
You can deploy the script as a Web App. To do so, go to Publish > Deploy as web app. Set the Execute the app as: field to Me (youremail). This way you can share the script as a browser link, and any user will run the script with your credentials.
You can add a some user interface with a confirmation message so the users know they have successfully executed the script. You can find the documentation in this link.
I'm trying to write sort of a proxy to be able to query a Google Spreadsheet with Google Chart API without giving rights directly to the spreadsheet to people accessing the visualization.
In order to do that, I want to replace the calls like
var query = new google.visualization.Query('http://spreadsheets.google.com/tq?key=THE_KEY');
query.send(handleQueryResponse);
by calls of my Apps Script proxy (with JSONP).
I wrote the proxy as following :
(I'll add later an option to encapsulate the result in a function - JSONP)
function doGet(e) {
return ContentService
.createTextOutput(
UrlFetchApp
.fetch("http://spreadsheets.google.com/tq?tq="+e.parameter.q+"&key="+e.parameter.key))
.setMimeType(ContentService.MimeType.JSON);
}
When trying to get a response, I obtain this error : google.visualization.Query.setResponse({"version":"0.6","status":"error","errors":[{"reason":"user_not_authenticated","message":"User not signed in","detailed_message":"\u003ca target=\u0022_blank\u0022 href=\u0022https://spreadsheets.google.com/spreadsheet/\u0022\u003eSign in\u003c/a\u003e"}]});
like if the user was not authenticated. I think it's weird because my script is deployed as a web app to be executed by my user, who has the rights on the spreadsheet. I guess we must use some OAuth, but I don't know how to authenticate the query that way.
So my question is :
How to authenticate a query with Apps Script to access data with the access rights I defined when deploying as web app ?
See the urlfetch docs. You are missing all the oauth stuff.
The way you are doing it will only work for a public ss.
I want to store google documents on google cloud storage. I am a admin user and have access for google cloud storage.
I using this code:
function myFunction() {
var base="https://www.googleapis.com/auth/devstorage.full_control"
var fetchArgs=googleOAuth_('provisioning',base)
fetchArgs.payload=<content_data_that you want to put on GCS>
fetchArgs.method='PUT'
fetchArgs. contentType=<content_type>
fetchArgs.host="<bucket_name>.storage.googleapis.com"
var url='https://storage.googleapis.com/<bucket_name>/<file_name>'
var result=UrlFetchApp.fetch(url,fetchArgs)
}
function googleOAuth_(name,scope) {
var oAuthConfig = UrlFetchApp.addOAuthService(name);
oAuthConfig.setRequestTokenUrl("https://www.google.com/accounts/OAuthGetRequestToken?scope="+scope);
oAuthConfig.setAuthorizationUrl("https://www.google.com/accounts/OAuthAuthorizeToken");
oAuthConfig.setAccessTokenUrl("https://www.google.com/accounts/OAuthGetAccessToken");
oAuthConfig.setConsumerKey(consumerKey);
oAuthConfig.setConsumerSecret(consumerSecret);
return {oAuthServiceName:name, oAuthUseToken:"always"};
}
When I run this code, it gives 503 server error.
Any suggestion would be helpful to me.
This problem has been solved. Updated the code with correction.
The Google Cloud Storage docs mention that if you use OAuth 1.0 you also need to pass the API key.
https://developers.google.com/storage/docs/json_api/v1/how-tos/authorizing
I haven't tried it but it looks like it might simplify you life:
OAuthService
by James Ferreira - GoogleScriptExamples.com
This Library takes the work out of using OAuth. It has one method
getAuth(' ', optUserScope) and works with all of the
Google APIs. See example here.
Project Key: MM5uQxqjwkMiuJW2zE50SgUF7jOZt2NQ8
See:
https://sites.google.com/site/scriptsexamples/custom-methods/google-oauth
I was also trying to use the Google Cloud Storage API from Google Apps Script. I found the following library:
https://github.com/googleworkspace/apps-script-oauth2
The documentation there was actually even more helpful because it showed me how I didn't need the library at all:
Even if your API is not covered by either, you can still use Apps Script to obtain the OAuth2 token for you. Simply edit the script's manifest to include the additional scopes that your API requires. When the user authorizes your script they will also be asked to approve those additional scopes. Then use the method ScriptApp.getOAuthToken() in your code to access the OAuth2 access token the script has acquired and pass it in the Authorization header of a UrlFetchApp.fetch() call.
Visit the sample NoLibrary to see an example of how this can be done.
The key takeaways for me (and posterity):
Make appsscript.json visible and add oauthScopes with all the API scopes desired plus https://www.googleapis.com/auth/script.external_request.
Set UrlFetchApp.fetch() second param to headers: {Authorization: 'Bearer ' + ScriptApp.getOAuthToken()}
Ignore the &key=[YOUR_API_KEY] seen in Google API examples.