We are looking for a way to trigger a function in the add-on in the user's context (authorization) from our web app. We tried using Apps Script Execution API to trigger that. The flow is add-on (apps script) registers the access token in our web app and the web app triggers execution API using the access token. This works for the first time. But the problem is, the access token is valid for only a few minutes. After which the web app is not able to initiate function through execution API due to authorization failure.
Questions:
Is our approach for triggering add-on/ apps script function from another web app in the user's context correct by using execution API?
If so, how do we avoid access token from expiring? Apparently, there is no API in apps script to 'get the refresh token'/ 'refresh the token'. How do we go about refreshing the token so that the token is valid forever (until the user cancels)?
Is there any radically different approach that will help in this case?
I see that add-ons like "PearDeck", "Form Approvals", "Form Publisher" etc are successfully doing this (triggering add-on function from the web app with user's authorization).
Hope to get some expert advice in this forum. Thanks
Used Execution API - with the problem in token getting expired
function prepareToken() {
// send to web app
var token = ScriptApp.getOAuthToken();
URLFetchApp.fetch("url?token="+token);
}
function doSomeWorkFromUserContext() {
// impl here
}
POST https://script.googleapis.com/v1/scripts/{scriptId}:run
Request Header
Authorization: Bearer + {token}
Request Body
{
"function": "doSomeWorkFromUserContext"
}
The function doSomeWorkFromUserContext should get called all the time of invocation but getting Authorization error after some time.
When a user authorizes your app for the first time you receive a refresh token along with the access token. The access token typically expires after one hour and you have to use the refresh token to fetch a new access token.
Note that the refresh token is only returned when the user initially authorizes your app.
You'll need to revoke the user's authorization (see below) and have them reauthorize the app to get the refresh token. Revoking access can be achieved in one of 3 ways:
Have the user uninstall the app
Use the ScriptApp.invalidateAuth() function
Make a HTTP GET request to the OAuth2 revoke endpoint with the users access token ("https://accounts.google.com/o/oauth2/revoke?token={token}")
Eric Koleda's GAS OAuth2 library does a great job of managing the entire OAuth2 flow (including automatically refreshing access tokens) so be sure to check that out.
Related
Does anyone know how to obtain a refresh token for a G Suite Add on, considering that the authorization flow to obtain the user consent was handled by Apps Script?
I know how to obtain the access token with the method
ScriptApp.getOAuthToken()
But I don't know how to obtain the refresh token for a particular user.
Context:
I have an add-on that allows users to schedule recurring meetings. The user grants the app access to their calendar when they install the add-on from the G Suite Marketplace, so I can read and write on their calendars when they are running the script.
I want to send them email reminders a day before a meeting, so I want to read their calendars in the background using a cloud function when they are not running the script.
In order to do that I would need to have the refresh token for that user since the access token expires.
I'm really stuck on this for many days now. Any help would be much appreciated!
Thanks a lot.
You have two options:
Either you get the refresh token with Url fetch as shown in the Generate an OAuth2 refresh token documentation
Or you install the apps-script-oauth2 library
If you set access to offline (.setParam('access_type', 'offline')) it is enough if the user grants you permission once.
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 am attempting to remotely execute a google apps script function using the apps script api through a postman request.
I first get credentials from the Google API Playground
Then they are inputted into postman as OAuth2 credentials, and inputted in the headers like this:
Then the appropriate link is requested:
Then the body prepared:
And then the response after making the request:
Postman is returning an error 403, which, according to Google's documentation
indicates that "the Cloud Platform project used to authorize the request is not the same as the one used by the script." An error 403 is an authorization error and can mean many things, but let's assume that the error is what google proclaims it to be.
I have full control over the GCP project utilized by my script, but I'm not aware of where to find the project that was used to authorize the request.
Where might I gain access to this GCP project so I can assign my script to this project, thereby eliminating the 403 error?
Thanks!
To use OAuth Access Tokens from the OAuth Playground with Apps Script, you need to specify the correct Client ID and Client Secret from the same project.
In the OAuth Playground. Click on the gear icon (top right). Select "Use your own OAuth credentials". Then enter the Client ID and Secret ID created in the same project as Apps Script.
To query a Google URL using a valid Access token with Postman you can log the access token from apps script and use it after.
In your apps script after validating scope, i.e. running a first time the script, log the token :
function logToken(){
Logger.log(ScriptApp.getOAuthToken());
}
Then in Postman query the Google URL by setting in the header the access token :
"Authorization": "Bearer THE_ACCESS_TOKEN"
Security warning : for security reason I have to tell you that an access token is valid 1 hour so technically if you grant full drive scope to your app with this access token we can browse all your Drive.
I have two GAS scripts. One of them is GSheets add-on another is Web App. The web app has doGet and doPost implemented. doGet returns custom HTML login screen. This screen is called from the GSheet script. After user logs into the web app I execute a doPost call to web app from the GSheet script to transform some data and return it as a JSON content.
The Web App is published with the following permissions
I need to run it as the "User accessing the web app" since I access user's drive and UserProperties service from the Web App.
When the doGet is executed for the first time from the Gsheets script the web app script request users permissions with the following OAuth scopes
"https://www.googleapis.com/auth/script.external_request"
"https://www.googleapis.com/auth/script.storage"
"https://www.googleapis.com/auth/drive"
User grants it and the Login page is rendered and works great.
When I execute the doPost call from the GSheet script I use:
UrlFetchApp.fetch('https://script.google.com/macros/s/<...>/exec', options)
The options object contains the Authorization: 'Bearer ' + ScriptApp.getOAuthToken(); basically user authorization of the GSheets script.
Everything was working great until yesterday. Now when I do the doPost call from the GSheets script I get the following HTML as a response.
Any ideas what is wrong here and why it stopped working without any changes? Perhaps some GAS update?
If I share the source code with the user who runs the scripts the issue is gone. I believe the issue could be related.
Over the last few months I have become familiar with OAuth2.0 authorization within google apps script but a recent anomaly has me confuzzled. I have a standalone web-app that acts as a front-end to a fusion table. The web-app is set to 'execute as' the script and fusion table owner and the fusion table grants view access to external users. The program detects authorization, prompts if needed, and uses refresh tokens if they're available. When run from the account that owns the script and fusion table, all is well.
Once I published the web-app, I tested it from an external account and it worked fine. The refresh token was then removed from the script-owner's UserProperties. When the app was run again from an external account, it prompted for authorization and authorized correctly (saving the new tokens in the UserProperties of the script-owner). However, the next POST call to the API received a 403-Forbidden error. At this point the app will continue to receive 403-Forbidden errors from any account (including the script-owner's account) until the tokens are manually cleared and reauthorized as the script-owner.
Why is it that these tokens are not valid? I would expect that since the app is being 'executed as' the script/fusion owner, any tokens received programmatically would be as valid as those those authorized by the script/fusion owner. If my expectations are incorrect, how can I protect against this situation for multiple users?
Update
I've gained some more traction on this and have identified some related issues that I wanted to share here. First of all, I was manually deleting the tokens (refresh and access) to test the ability of the app to authorize. Subsequent authorizations were not returning a refresh token (which caused excessive prompting). I found out that this is an intentional outcome from the google API. Aside from the requisite request parameters to return a refresh token, I found out that a refresh token is only returned on the first user-prompt and authorization(ref. here). In order to get another refresh token, you need to either revoke-acess and re-authorize or force the approval prompt in the request. Once I fixed that it became much clearer that the token was 'attached' to the user making the initial authorization request. As long as I retained the refresh token acquired by the script/table owner, then the script could be used (and reauthorized programmatically using the refresh token) by any external user. Which brings me back to the point. If I lose the refresh token, I need to manually delete all remaining token scraps, login as script/table owner, revoke access to the app, then reauthorize.
Per Zig's answer below, that is just how it is. Is there no way I can programmatically prevent that very manual process?
Script executing as owner will save owner tokens automatically through appscript builtin authentication.
However if you are doing the oauth flow yourself, it will save the user's token and thus wont have access to your data.