How to add authentication to a Google apps script without it expiring? - google-apps-script

I have a Google apps script project which I use as a web application to save some data gathered from a web page (JavaScript only) to My Google sheets (think of it as a simple database for just me).
It's no need to use auth for anyone else other than my account, because, I don't use their accounts/data for anything at all. I just need to use my account only, so, when I deploy it, I make it execute as me, and accessible to anyone:
.
When I click deploy after previous screen, it asks for my permission (the consent dialogue) to access my account's data, which I grant it, and after that everything is good, and the HTTP requests to this script works just fine.
.
The problem is:
This authentication expires after maybe 7 days (I'm not sure) and the script stops working, I discover that when the HTTP requests to it return error 403
To fix that and make it work again, I need to run the script again from the Google apps script editor which asks for the permissions (consent) again:
.
I can't use it like that and the web page stop working when the authentication gets revoked!
I'm not publishing the script (I don't want/don't need to). Do I?
My question is, how can I add the authentication in a way that makes it lasts and stops asking me for it over and over again?
The script on Google apps script works like this:
function doPost(request) {
return checkRequest(request);
}
function checkRequest(request) {
//check the request & save the sent data to a google sheet here;
//...
return sendResponse({
success: true,
data: {result: 'Saved the data!' }
});
}
function sendResponse(response) {
return ContentService
.createTextOutput(JSON.stringify(response))
.setMimeType(ContentService.MimeType.JSON);
}
And I call it from the web page using Ajax HTTP POST request, like this:
jQuery.ajax({
url: 'https://script.google.com/macros/s/{script-id}/exec',
method: 'POST',
dataType: "json",
data: {key: 'value'},
success: function (response) {
console.log(response);
},
error: function (response) {
console.error(response);
}
});
And this is the response the script returns after few days when the authentication expires:

This has been reported to Google
There is already a report on Google's Issue Tracker which detail the same kind of behaviour:
Random Deauthorizations for script since Editor Update with no changes to code
Google does seem to know about this issue. From the issue tracker link, a response was given:
[...] reviewing the documentation, it reads:
Authorizations by a test user will expire seven days from the time of consent.
Source
So I'm not sure we should expect anything different from these tests.
Also re-reading this thread, in your first comment you said that this was affecting projects that already published. Though I understand that you fixed the original projects that were having issues by un-linking the GCP projects.
A possible fix would be filling the scopes manually in the manifest for these types of issues. This is because Apps Script tries to infer the scopes needed at runtime, and sometimes this can result in Apps Script trying to gain more permissive scope than what is authorized.
See Setting Explicit Scopes
However, token expiry in around 7 days is to be expected for projects in "testing" state.
Which seems to be the case for the users in the report thread.
There are multiple reasons that this expiration may occur, as explained in the Using OAuth 2.0 to Access Google APIs page.
That being said, if it's causing problems you can file your own bug about it here in the Google Apps Script component.
References:
Random Deauthorizations for script since Editor Update with no changes to code
Using OAuth 2.0 to Access Google APIs | Google Identity
Setting up your OAuth consent screen - Google Cloud Platform Console Help
Authorization Scopes - Setting explicit scopes | Apps Script | Google Developers

Related

OAuth Bug in Apps Script using YouTube Data API

I have used Apps Script successfully on many occasions, and one of the reasons I like it, especially for personal enhancements or projects related to Google Services is just how seamlessly it integrates auth. However, when trying to integrate the YouTube Data API into one of my Google Sheets' Apps Scripts (I am trying to use the sheet to manage a YouTube playlist), I encountered an error that I have never encountered before.
The code is very simple, I am just trying to get some data from a playlist to return to the logger in the context of my Google Sheets Apps Script. Note that this Apps Script belongs to the same account as the YouTube playlist. The OAuth Client Verification docs specifically state:
Note: Verification is not required for Apps Script projects whose
owner and users belong to the same Google Workspace domain or
customer.
However, when I run my script, the OAuth screen says the app is unverified (this has never happened when I have used any other APIs accessing my own account in Apps Script), and even though I authenticate and it says "Authentication Successful", the script is blocked and it repeatedly (as in forever, in an endless loop) asks me to authenticate again.
Completely at a loss for what is going on. 1.) I shouldn't have to verify this script per the docs I referenced above, and I have never had to before for accessing my own content. 2.) The successful authentication but then failing and repeatedly asking me to authenticate again is driving me mad.
Please advise!
Code is very simple, just trying to get this to return ANYTHING:
const syncVideos = () => {
let response = YouTube.PlaylistItems.list('snippet,contentDetails', {'playlistId': '<REDACTED>'});
Logger.log(response);
}
Answer: This turned out to not be a code or OAuth issue really, but more of an unintuitive procedure when authenticating, i.e. when authenticating with Google to access one of your channel's data through the YouTube Data API, authenticate with your main channel, even if requesting data from other channels connected to your account.

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

Google Apps Script returns Sorry, unable to open the file at this time. Please check the address and try again

I am trying to POST a request to a simple app script.
Here's the code:
var BACKEND_URL = "https://script.google.com/macros/s/AKfycbyYAWMv6O8Xld1EvqPuBk9EgxgfpVNly3dyX3JkSc3h/dev";
data = {"obj1": "data1", "obj2": "data2"};
function doGet() {
var resp = UrlFetchApp.fetch(BACKEND_URL, {
'method': 'post',
'contentType': 'application/json',
'payload': JSON.stringify(data),
'muteHttpExceptions': true
});
Logger.log(resp);
}
And here's the code for the server which should accept POST requests:
function doGet() {
}
function doPost(e) {
Logger.log("posted");
return;
}
NOTE: there's no user part in the URL, so this question isn't the same as a similar one posted in SO.
As a response a get an html page where it's said that sorry, Sorry, unable to open the file at this time. Please check the address and try again.
I've re-checked the page and it does exist.
In your case, the project with doGet() including UrlFetchApp.fetch() is the different from the project with doPost(e). And you want to call doPost() by running doGet() including UrlFetchApp.fetch(). If my understanding your situation is correct, can you confirm the following points.
BACKEND_URL you are using includes dev. This is used for using the latest code. But there is a limitation as follows. So please try to use Current web app URL like https://script.google.com/macros/s/#####/exec.
The second is the link labeled latest code and ends in /dev. This URL can only be accessed by users who have edit access to the script. This instance of the app always runs the most recently saved code — not necessarily a formal version — and is intended for quick testing during development.
At Web Apps, when there are no return messages, the error message of The script completed but did not return anything. is returned. If you want to get the return message, please set it using Content Service.
When the script at Web Apps side was modified, please redeploy the Web Apps as a new version. By this, the latest script is reflected to Web Apps.
References :
Deploying a script as a web app
Content Service
Edit :
When you deploy Web Apps, please try the following settings.
"Me" for "Execute the app as:"
"Anyone, even anonymous" for "Who has access to the app:"

Authentication issue with Google Apps Script deployed as a web app

I am facing HTTP 401 errors while trying to call a deployed Apps Script (as a web app, accessible to "anyone") from a second GAS with UrlFetch and a bearer in authorization header. The scripts were working fine for months until around two weeks ago.
Here are two small scripts to reproduce the error.
Script A - Deployed as a web app, accessible to "Anyone".
function doGet(e) {
var params = e.parameter.params;
console.info("Parameters : " + JSON.stringify(e.parameter));
return ContentService.createTextOutput("Success");
}
Script B - Calling the script A via UrlFetch
function callURL() {
var param = {
method : "get",
headers : {"Authorization": "Bearer " + ScriptApp.getOAuthToken()},
followRedirects : true,
muteHttpExceptions:true,
};
var url = "https://script.google.com/macros/s/<script_A_deployed_url>/exec?param1=test";
var resp = UrlFetchApp.fetch(url,param);
if(resp.getContentText() != "Success"){
console.info(resp.getContentText());
throw resp.getContentText();
}
}
Can you confirm the following points again?
For the client side, are there some functions except for your script of the client side in the project? If there is only the script in the project, the scope for accessing to Web Apps is not enough. The scopes for Drive API are required to be included in the scope of access token.
You can see the current scopes at File -> Project properties -> Scopes.
For example, those are the following scopes.
https://www.googleapis.com/auth/drive.readonly
https://www.googleapis.com/auth/drive.file
https://www.googleapis.com/auth/drive
In my log, when Who has access to the app: is installed as Anyone, I confirmed that from April 11, 2018, it is required to be shared the project to access to Web Apps. This might be due to the update of Google.
Please share the project of Web Apps with users and try again.
For the Web Apps server side, if you set User accessing the web app for Execute the app as:, please authorize the scopes using own browser. This authorization is required to do only one time.
If these were not useful for your situation, I'm sorry.
Tanaike pointed me in the right direction. Apparently, some internal rules recently changed in the authentication mechanism for Apps Script deployed as a web app.
For B script, the default scope with UrlFetch is https://www.googleapis.com/auth/script.external_request, but it looks like we now need at least read access to A script, which means we also need Drive scopes.
In order to achieve that, you can for example have this function in B script to authorize them.
function setScope() {
DriveApp.getRootFolder();
}

Correct scope for Google App Script Execution API?

I'm hoping to automate some HR work by running a Google App Script via the Execution API. Without getting too much into the details, I'd like to pass employee evaluation data as a parameter into the App Script. The script will then use this data to compile an "Employee Review" GDoc.
So far, I have ran a simple test App Script using the Execution API. For example, I can successfully run a simple function which logs a string or interacts with spreadsheets. So far so good.
But I run into problems when trying to write to a GDoc (which is unfortunately integral to my task). Here's my paired down script:
// TODO: Eventually, we'll pass these variables as arguments
var docId = "MY-DOC-ID";
// Find the team member review doc
var doc = DocumentApp.openById(docId);
// Replace placeholder text
var docBody = doc.getActiveSection();
docBody.replaceText('{{DATE}}', "Date set by App Script!!!");
doc.saveAndClose();
This script works when I press the "Run" button in the App Scripts web UI. But when I try to run via the Execution API, I get:
{
"error": "unauthorized_client",
"error_description": "Unauthorized client or scope in request."
}
So apparently I haven't provided the correct scope? Following the docs, I can find the necessary scope(s) in Project Properties > Scopes which says:
But when I try adding that scope, it wont work. As I said other scopes (e.g. https://www.googleapis.com/auth/spreadsheets) work just fine. Perhaps the auth/documents scope is no longer supported or there's a bug in their API?
Questions
What is the correct scope? I can see a big list here but I don't see https://www.googleapis.com/auth/documents, so?
Any other suggestions? For example, is it possible to write to a Google Doc using the Google Client API directly (i.e. without using App Scripts)?
Doh. I figured out the solution to my problem. While it was a dumb mistake, it's nevertheless worth posting as it may save others confusion in the future.
First, a little context about my setup. I'm authenticating to the Google Client API using a Service Account. Furthermore, as is common when using a service account setup, I am impersonating a user within our organization (specifically my own account).
My missing step (obvious in hindsight)...
Log into the App Script web UI as the person you are impersonating.
Manually run the script by pressing the play button
If the impersonated user has not already granted permissions to access the required scopes, you will be prompted to do so.
After granting access (specifically for the https://www.googleapis.com/auth/documents scope), my authorization error disappeared.
So the lesson: Make sure the account you are impersonating has granted access for all the scopes which your script requires.